├── .gitignore ├── public_html ├── img │ ├── homer.gif │ ├── blackorchid.png │ ├── grey_wash_wall.png │ ├── simple_dashed.png │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── js │ ├── jquery.smooth-scroll.min.js │ ├── status.js │ ├── bootstrap.min.js │ └── sparkline.js ├── index.html └── css │ ├── bootstrap-responsive.min.css │ └── bootstrap-responsive.css ├── README.md ├── config.json ├── package.json ├── LICENSE ├── ingests.js ├── routes.js ├── twitchstatus.js ├── http.js ├── chat-servers.js ├── tmi-connection-handler.js └── chat.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /public_html/img/homer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/night/TwitchStatus/HEAD/public_html/img/homer.gif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TwitchStatus 2 | ============ 3 | 4 | Feel free to submit pull requests to add or improve features. -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "irc": { 3 | "username": "", 4 | "client_id": "", 5 | "access_token": "" 6 | } 7 | } -------------------------------------------------------------------------------- /public_html/img/blackorchid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/night/TwitchStatus/HEAD/public_html/img/blackorchid.png -------------------------------------------------------------------------------- /public_html/img/grey_wash_wall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/night/TwitchStatus/HEAD/public_html/img/grey_wash_wall.png -------------------------------------------------------------------------------- /public_html/img/simple_dashed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/night/TwitchStatus/HEAD/public_html/img/simple_dashed.png -------------------------------------------------------------------------------- /public_html/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/night/TwitchStatus/HEAD/public_html/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /public_html/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/night/TwitchStatus/HEAD/public_html/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitchstatus", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "Unofficial Twitch service status", 6 | "license": "MIT", 7 | "dependencies": { 8 | "async": "^2.0.0-rc.1", 9 | "express": "~4.13.4", 10 | "express-toobusy": "0.0.3", 11 | "irc-message": "^3.0.1", 12 | "node-media-server": "^2.1.9", 13 | "request": ">=2.69.0", 14 | "ws": "^7.2.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 NightDev, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ingests.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | var config = require("./config.json"); 3 | 4 | module.exports = function (callback) { 5 | request( 6 | { 7 | url: "https://ingest.twitch.tv/ingests", 8 | headers: { 9 | "Client-ID": config.irc.client_id, 10 | }, 11 | json: true, 12 | timeout: 60000, 13 | }, 14 | function (error, response, data) { 15 | if (error || !data.ingests) { 16 | callback([]); 17 | return; 18 | } 19 | 20 | var servers = []; 21 | data.ingests.forEach(function (ingest) { 22 | var host = ingest.url_template.split("/")[2], 23 | port = 1935; 24 | 25 | var server = { 26 | name: host 27 | .replace(/^live/, "Live") 28 | .replace(".justin.", ".twitch.") 29 | .replace(".twitch.", ".Twitch.") 30 | .replace(".tv", ".TV"), 31 | type: "ingest", 32 | description: ingest.name 33 | .replace("Midwest", "Central") 34 | .replace("Asia", "AS") 35 | .replace("Australia", "AU"), 36 | host: host, 37 | port: port, 38 | }; 39 | 40 | servers.push(server); 41 | }); 42 | 43 | servers.sort(function (a, b) { 44 | return a.description.localeCompare(b.description); 45 | }); 46 | 47 | callback(servers); 48 | } 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /routes.js: -------------------------------------------------------------------------------- 1 | module.exports = function (main) { 2 | var app = main.app, 3 | servers = main.servers; 4 | 5 | var getAlerts = function (type) { 6 | var alerts = []; 7 | 8 | Object.keys(servers).forEach(function (name) { 9 | var server = servers[name]; 10 | 11 | if (server.type !== type) return; 12 | 13 | if (server.alerts.length > 0) { 14 | server.alerts.forEach(function (alert) { 15 | alerts.push({ 16 | server: name, 17 | type: alert.type, 18 | message: alert.message, 19 | }); 20 | }); 21 | } 22 | }); 23 | 24 | return alerts; 25 | }; 26 | 27 | var formatServers = function (type) { 28 | var formatted = []; 29 | 30 | Object.keys(servers).forEach(function (name) { 31 | var server = servers[name]; 32 | 33 | if (server.type !== type) return; 34 | 35 | formatted.push({ 36 | server: name, 37 | cluster: server.cluster, 38 | host: server.type !== "chat" ? server.host : undefined, 39 | ip: server.type === "chat" ? server.host : undefined, 40 | secure: server.secure, 41 | port: server.port, 42 | protocol: server.protocol, 43 | description: server.description, 44 | status: server.status, 45 | loadTime: server.type !== "chat" ? server.lag : undefined, 46 | errors: server.errors ? server.errors.total : undefined, 47 | lag: server.type === "chat" ? server.lag : undefined, 48 | pings: server.type === "chat" ? server.pings : undefined, 49 | }); 50 | }); 51 | 52 | return formatted; 53 | }; 54 | 55 | app.get("/api/status", function (req, res) { 56 | switch (req.query.type) { 57 | case "web": 58 | case "ingest": 59 | case "chat": 60 | res.jsonp(formatServers(req.query.type)); 61 | break; 62 | default: 63 | res.jsonp({ 64 | web: { 65 | alerts: [], 66 | servers: formatServers("web"), 67 | }, 68 | ingest: { 69 | alerts: [], 70 | servers: formatServers("ingest"), 71 | }, 72 | chat: { 73 | alerts: getAlerts("chat"), 74 | servers: formatServers("chat"), 75 | }, 76 | }); 77 | return; 78 | } 79 | }); 80 | 81 | // Catch-all not found 82 | app.get("*", function (req, res) { 83 | res.status(404).send("404 Not Found"); 84 | }); 85 | }; 86 | -------------------------------------------------------------------------------- /public_html/js/jquery.smooth-scroll.min.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Smooth Scroll - v1.4.10 - 2013-03-02 4 | * https://github.com/kswedberg/jquery-smooth-scroll 5 | * Copyright (c) 2013 Karl Swedberg 6 | * Licensed MIT (https://github.com/kswedberg/jquery-smooth-scroll/blob/master/LICENSE-MIT) 7 | */ 8 | (function(l){function t(l){return l.replace(/(:|\.)/g,"\\$1")}var e="1.4.10",o={exclude:[],excludeWithin:[],offset:0,direction:"top",scrollElement:null,scrollTarget:null,beforeScroll:function(){},afterScroll:function(){},easing:"swing",speed:400,autoCoefficent:2},r=function(t){var e=[],o=!1,r=t.dir&&"left"==t.dir?"scrollLeft":"scrollTop";return this.each(function(){if(this!=document&&this!=window){var t=l(this);t[r]()>0?e.push(this):(t[r](1),o=t[r]()>0,o&&e.push(this),t[r](0))}}),e.length||this.each(function(){"BODY"===this.nodeName&&(e=[this])}),"first"===t.el&&e.length>1&&(e=[e[0]]),e};l.fn.extend({scrollable:function(l){var t=r.call(this,{dir:l});return this.pushStack(t)},firstScrollable:function(l){var t=r.call(this,{el:"first",dir:l});return this.pushStack(t)},smoothScroll:function(e){e=e||{};var o=l.extend({},l.fn.smoothScroll.defaults,e),r=l.smoothScroll.filterPath(location.pathname);return this.unbind("click.smoothscroll").bind("click.smoothscroll",function(e){var n=this,s=l(this),c=o.exclude,i=o.excludeWithin,a=0,f=0,h=!0,u={},d=location.hostname===n.hostname||!n.hostname,m=o.scrollTarget||(l.smoothScroll.filterPath(n.pathname)||r)===r,p=t(n.hash);if(o.scrollTarget||d&&m&&p){for(;h&&c.length>a;)s.is(t(c[a++]))&&(h=!1);for(;h&&i.length>f;)s.closest(i[f++]).length&&(h=!1)}else h=!1;h&&(e.preventDefault(),l.extend(u,o,{scrollTarget:o.scrollTarget||p,link:n}),l.smoothScroll(u))}),this}}),l.smoothScroll=function(t,e){var o,r,n,s,c=0,i="offset",a="scrollTop",f={},h={};"number"==typeof t?(o=l.fn.smoothScroll.defaults,n=t):(o=l.extend({},l.fn.smoothScroll.defaults,t||{}),o.scrollElement&&(i="position","static"==o.scrollElement.css("position")&&o.scrollElement.css("position","relative"))),o=l.extend({link:null},o),a="left"==o.direction?"scrollLeft":a,o.scrollElement?(r=o.scrollElement,c=r[a]()):r=l("html, body").firstScrollable(),o.beforeScroll.call(r,o),n="number"==typeof t?t:e||l(o.scrollTarget)[i]()&&l(o.scrollTarget)[i]()[o.direction]||0,f[a]=n+c+o.offset,s=o.speed,"auto"===s&&(s=f[a]||r.scrollTop(),s/=o.autoCoefficent),h={duration:s,easing:o.easing,complete:function(){o.afterScroll.call(o.link,o)}},o.step&&(h.step=o.step),r.length?r.stop().animate(f,h):o.afterScroll.call(o.link,o)},l.smoothScroll.version=e,l.smoothScroll.filterPath=function(l){return l.replace(/^\//,"").replace(/(index|default).[a-zA-Z]{3,4}$/,"").replace(/\/$/,"")},l.fn.smoothScroll.defaults=o})(jQuery); -------------------------------------------------------------------------------- /twitchstatus.js: -------------------------------------------------------------------------------- 1 | var chat = require("./chat"), 2 | chatServers = require("./chat-servers"), 3 | config = require("./config.json"), 4 | express = require("express"), 5 | http = require("./http"), 6 | ingests = require("./ingests"); 7 | 8 | TwitchStatus = function () { 9 | this.app = express(); 10 | 11 | process.on("uncaughtException", function (err) { 12 | console.log("Caught exception: " + err); 13 | console.log(err.stack); 14 | }); 15 | 16 | this.app.listen(8080, "localhost"); 17 | this.app.disable("x-powered-by"); 18 | this.app.use(express.static(__dirname + "/public_html")); 19 | this.app.use("/", express.static(__dirname + "/public_html/index.html")); 20 | 21 | this._servers = [ 22 | { 23 | name: "Twitch.TV", 24 | type: "web", 25 | description: "Twitch's main website", 26 | host: "www.twitch.tv", 27 | path: "/", 28 | port: 443, 29 | }, 30 | { 31 | name: "API.Twitch.TV", 32 | type: "web", 33 | description: "Twitch's external endpoint for data retrieval", 34 | host: "api.twitch.tv", 35 | path: "/helix/streams", 36 | port: 443, 37 | }, 38 | ]; 39 | this.servers = {}; 40 | 41 | this.reports = []; 42 | this.lostMessages = []; 43 | 44 | this.chat = new chat(this); 45 | this.http = new http(this); 46 | 47 | var _self = this; 48 | ingests(function (servers) { 49 | _self._servers = _self._servers.concat(servers); 50 | 51 | chatServers(function (servers) { 52 | _self._servers = _self._servers.concat(servers); 53 | 54 | _self.setup(); 55 | }); 56 | }); 57 | 58 | setInterval(function () { 59 | _self.cleanup(); 60 | }, 30000); 61 | }; 62 | 63 | TwitchStatus.prototype.setup = function () { 64 | // Setup server monitoring 65 | for (var i = 0; i < this._servers.length; i++) { 66 | var server = this._servers[i]; 67 | 68 | this.servers[server.name] = { 69 | name: server.name, 70 | type: server.type, 71 | description: server.description, 72 | host: server.host.toLowerCase(), 73 | port: server.port, 74 | path: server.path, 75 | cluster: server.cluster || undefined, 76 | channel: server.channel || undefined, 77 | protocol: server.protocol || undefined, 78 | secure: server.secure || false, 79 | status: "unknown", 80 | lag: 999999, 81 | }; 82 | 83 | if (server.type === "chat") { 84 | this.chat.setup(server); 85 | } 86 | } 87 | 88 | this.http.checkStatus(); 89 | 90 | // Setup express routes 91 | require("./routes")(this); 92 | }; 93 | 94 | TwitchStatus.prototype.cleanup = function () { 95 | var current = Math.round(Date.now() / 1000), 96 | past5 = (current - 300) * 1000, 97 | limit = new Date(past5); 98 | 99 | for (var i = this.reports.length - 1; i >= 0; i--) { 100 | if (this.reports[i].logged < limit) { 101 | this.reports.splice(i, 1); 102 | } 103 | } 104 | 105 | for (var i = this.lostMessages.length - 1; i >= 0; i--) { 106 | if (this.lostMessages[i].sent < limit) { 107 | this.lostMessages.splice(i, 1); 108 | } 109 | } 110 | }; 111 | 112 | new TwitchStatus(); 113 | -------------------------------------------------------------------------------- /http.js: -------------------------------------------------------------------------------- 1 | var request = require("request"); 2 | var config = require("./config.json"); 3 | var NodeRtmpClient = require("node-media-server/node_rtmp_client"); 4 | 5 | var HTTP = function (main) { 6 | this.servers = main.servers; 7 | 8 | var _self = this; 9 | setInterval(function () { 10 | _self.checkStatus(); 11 | }, 30000); 12 | }; 13 | 14 | HTTP.prototype.checkStatus = function () { 15 | var servers = this.servers, 16 | t = 0; 17 | 18 | var _self = this; 19 | Object.keys(servers).forEach(function (name) { 20 | var server = servers[name]; 21 | if (server.type === "chat") return; 22 | 23 | if (server.type === "ingest") { 24 | setTimeout(function () { 25 | _self.checkIngestAddress.call( 26 | _self, 27 | server.name, 28 | server.host, 29 | server.port 30 | ); 31 | }, (t += 1000)); 32 | return; 33 | } 34 | 35 | setTimeout(function () { 36 | _self.checkWebAddress.call( 37 | _self, 38 | server.name, 39 | server.host, 40 | server.port, 41 | server.path 42 | ); 43 | }, (t += 1000)); 44 | }); 45 | }; 46 | 47 | HTTP.prototype.checkWebAddress = function (name, host, port, path) { 48 | var servers = this.servers, 49 | startTime = Date.now(), 50 | path = path || "/", 51 | url = "http" + (port === 443 ? "s" : "") + "://" + host + ":" + port + path, 52 | isAPI = host.toLowerCase() === "api.twitch.tv"; 53 | 54 | request.get( 55 | { 56 | url: url, 57 | qs: { 58 | kappa: Math.random(), 59 | }, 60 | headers: isAPI 61 | ? { 62 | "Client-ID": config.irc.client_id, 63 | Authorization: "Bearer " + config.irc.access_token, 64 | } 65 | : undefined, 66 | timeout: 30000, 67 | }, 68 | function (err, res, body) { 69 | if (!err && res.statusCode === 200) { 70 | servers[name].lag = Date.now() - startTime; 71 | servers[name].status = "online"; 72 | 73 | // I consider >= 3000 slow because of CDNs 74 | // 1935 is an ingest port, but some ingests are across the world 75 | if (servers[name].lag >= 3000 && port !== 1935) { 76 | servers[name].status = "slow"; 77 | } 78 | } else { 79 | servers[name].lag = 999999; 80 | servers[name].status = "offline"; 81 | } 82 | } 83 | ); 84 | }; 85 | 86 | HTTP.prototype.checkIngestAddress = function (name, host, port) { 87 | var servers = this.servers; 88 | var startTime = Date.now(); 89 | var client = new NodeRtmpClient( 90 | "rtmp://" + host + ":" + port + "/app/fakestreamkey" 91 | ); 92 | 93 | // we destroy the connection before attempting to read from a stream key 94 | client.rtmpSendCreateStream = function () {}; 95 | 96 | client.on("status", function (info) { 97 | switch (info.code) { 98 | case "NetConnection.Connect.Success": 99 | servers[name].lag = Date.now() - startTime; 100 | servers[name].status = "online"; 101 | client.socket.destroy(); 102 | break; 103 | default: 104 | console.log("unhandled ingest info", info); 105 | break; 106 | } 107 | }); 108 | 109 | client.startPull(); 110 | client.socket.on("error", () => { 111 | servers[name].lag = 999999; 112 | servers[name].status = "offline"; 113 | }); 114 | }; 115 | 116 | module.exports = HTTP; 117 | -------------------------------------------------------------------------------- /chat-servers.js: -------------------------------------------------------------------------------- 1 | var config = require("./config.json"), 2 | request = require("request"), 3 | dns = require("dns"), 4 | async = require("async"); 5 | 6 | String.prototype.capitalize = function () { 7 | return this.charAt(0).toUpperCase() + this.slice(1); 8 | }; 9 | 10 | var parseServers = function (res, callback) { 11 | var uniqueHosts = []; 12 | var servers = []; 13 | 14 | async.each( 15 | ["servers", "websockets_servers"], 16 | function (type, callback) { 17 | if (!res[type]) return callback(); 18 | 19 | async.each( 20 | res[type], 21 | function (server, callback) { 22 | var subServers = []; 23 | 24 | async.waterfall( 25 | [ 26 | function (callback) { 27 | var host = server.split(":")[0]; 28 | var port = parseInt(server.split(":")[1]); 29 | 30 | if (/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(host)) { 31 | subServers.push({ 32 | host: host, 33 | port: port, 34 | }); 35 | return callback(); 36 | } 37 | 38 | dns.lookup( 39 | host, 40 | { 41 | all: true, 42 | }, 43 | function (err, hosts) { 44 | if (err) return callback(); 45 | 46 | subServers = subServers.concat( 47 | hosts.map(function (h) { 48 | return { 49 | host: h.address, 50 | port: port, 51 | }; 52 | }) 53 | ); 54 | 55 | callback(); 56 | } 57 | ); 58 | }, 59 | function (callback) { 60 | subServers.forEach(function (server) { 61 | if (server.port === 6667) { 62 | subServers.push({ 63 | host: server.host, 64 | port: 6697, 65 | }); 66 | } else if (server.port === 80) { 67 | subServers.push({ 68 | host: server.host, 69 | port: 443, 70 | }); 71 | } 72 | }); 73 | subServers.forEach(function (server) { 74 | var hostPort = server.host + ":" + server.port; 75 | 76 | if (uniqueHosts.indexOf(hostPort) > -1) return; 77 | uniqueHosts.push(hostPort); 78 | 79 | servers.push({ 80 | name: hostPort, 81 | type: "chat", 82 | cluster: "aws", 83 | protocol: type === "websockets_servers" ? "ws_irc" : "irc", 84 | description: "Chat Server", 85 | host: server.host, 86 | port: server.port, 87 | secure: [443, 6697].indexOf(server.port) > -1, 88 | }); 89 | }); 90 | 91 | callback(); 92 | }, 93 | ], 94 | callback 95 | ); 96 | }, 97 | callback 98 | ); 99 | }, 100 | function () { 101 | callback(servers); 102 | } 103 | ); 104 | }; 105 | 106 | var chatServers = function (callback) { 107 | request( 108 | { 109 | url: "https://tmi.twitch.tv/servers", 110 | qs: { 111 | channel: config.irc.username, 112 | kappa: Math.random(), 113 | }, 114 | json: true, 115 | timeout: 60000, 116 | }, 117 | function (error, response, data) { 118 | if (error || response.statusCode !== 200) { 119 | callback([]); 120 | return; 121 | } 122 | 123 | parseServers(data, function (newServers) { 124 | callback(newServers); 125 | }); 126 | } 127 | ); 128 | }; 129 | 130 | module.exports = function (callback) { 131 | chatServers(function (servers) { 132 | servers.sort(function (a, b) { 133 | return a.port - b.port || a.host.localeCompare(b.host); 134 | }); 135 | 136 | callback(servers); 137 | }); 138 | }; 139 | -------------------------------------------------------------------------------- /tmi-connection-handler.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require("events").EventEmitter; 2 | var net = require("net"); 3 | var tls = require("tls"); 4 | var parse = require("irc-message").parse; 5 | var util = require("util"); 6 | var ws = require("ws"); 7 | 8 | var IRC = function (options) { 9 | EventEmitter.call(this); 10 | 11 | this.options = options; 12 | this._socket = null; 13 | this._connected = false; 14 | this._buffer = null; 15 | 16 | if (!this.options.host) { 17 | throw new Error("No host configured"); 18 | return; 19 | } 20 | 21 | if (!this.options.port) { 22 | throw new Error("No port configured"); 23 | return; 24 | } 25 | 26 | if (!this.options.nick) { 27 | throw new Error("No nick configured"); 28 | return; 29 | } 30 | 31 | if (!this.options.protocol) { 32 | throw new Error("No protocol configured"); 33 | return; 34 | } 35 | }; 36 | 37 | util.inherits(IRC, EventEmitter); 38 | 39 | // IRC Connections 40 | 41 | IRC.prototype._wsConnect = function () { 42 | var _self = this; 43 | var protocol = this.options.secure ? "wss:" : "ws:"; 44 | var socket = new ws( 45 | protocol + "//" + this.options.host + ":" + this.options.port, 46 | "irc", 47 | { 48 | localAddress: this.options.localAddress, 49 | rejectUnauthorized: false, 50 | } 51 | ); 52 | 53 | socket.on("open", function () { 54 | _self._onOpen(); 55 | }); 56 | socket.on("message", function (data) { 57 | _self._onData(data); 58 | }); 59 | socket.on("close", function () { 60 | _self._onClose(); 61 | }); 62 | socket.on("error", function () { 63 | _self._onClose(); 64 | }); 65 | 66 | this._socket = socket; 67 | }; 68 | 69 | IRC.prototype._ircConnect = function () { 70 | var _self = this; 71 | var protocol = this.options.secure ? tls : net; 72 | var socket = protocol.connect( 73 | { 74 | host: this.options.host, 75 | port: this.options.port, 76 | localAddress: this.options.localAddress, 77 | rejectUnauthorized: false, 78 | }, 79 | function () { 80 | _self._onOpen(); 81 | } 82 | ); 83 | 84 | socket.on("data", function (data) { 85 | _self._onData(data); 86 | }); 87 | socket.on("end", function () { 88 | _self._onClose(); 89 | }); 90 | socket.on("error", function () { 91 | _self._onClose(); 92 | }); 93 | socket.on("timeout", function () { 94 | _self._onTimeout(); 95 | }); 96 | 97 | this._socket = socket; 98 | }; 99 | 100 | IRC.prototype.connect = function () { 101 | if (this._socket) return this.reconnect(); 102 | return this.options.protocol === "irc" 103 | ? this._ircConnect() 104 | : this._wsConnect(); 105 | }; 106 | 107 | IRC.prototype.disconnect = function () { 108 | if (!this._connected) return; 109 | 110 | this._connected = false; 111 | this._closeSocket(); 112 | }; 113 | 114 | IRC.prototype.reconnect = function () { 115 | this._closeSocket(); 116 | this.connect(); 117 | }; 118 | 119 | IRC.prototype._closeSocket = function () { 120 | try { 121 | if (this.options.protocol === "irc") { 122 | this._socket.destroy(); 123 | } else { 124 | this._socket.close(); 125 | } 126 | } catch (e) {} 127 | 128 | delete this._socket; 129 | }; 130 | 131 | // IRC Parser 132 | 133 | IRC.prototype._onOpen = function () { 134 | this._connected = true; 135 | if (this.options.pass) this.raw("PASS", this.options.pass); 136 | this.raw("NICK", this.options.nick); 137 | this.raw("CAP", "REQ", ":twitch.tv/commands twitch.tv/tags"); 138 | this.emit("connected"); 139 | }; 140 | 141 | IRC.prototype._onData = function (data) { 142 | var lines = data.toString().split("\r\n"); 143 | 144 | if (this._buffer) { 145 | lines[0] = this._buffer + lines[0]; 146 | this._buffer = null; 147 | } 148 | 149 | if (lines[lines.length - 1] !== "") { 150 | this._buffer = lines.pop(); 151 | } 152 | 153 | for (var i = 0; i < lines.length; i++) { 154 | this._parse(lines[i]); 155 | } 156 | }; 157 | 158 | IRC.prototype._parse = function (message) { 159 | message = parse(message); 160 | 161 | if (!message) return; 162 | 163 | if (message.command === "PING") { 164 | this.raw("PONG", message.params.join(" ")); 165 | return; 166 | } 167 | 168 | var data = { 169 | target: message.params.shift(), 170 | nick: message.prefix 171 | ? message.prefix.split("@")[0].split("!")[0] 172 | : undefined, 173 | tags: message.tags, 174 | message: message.params.shift(), 175 | raw: message.raw, 176 | }; 177 | 178 | this.emit(message.command.toLowerCase(), data); 179 | }; 180 | 181 | IRC.prototype._onClose = function () { 182 | if (!this._connected) return; 183 | this._connected = false; 184 | this.emit("disconnected"); 185 | }; 186 | 187 | IRC.prototype._onTimeout = function () { 188 | this._connected = false; 189 | this.emit("disconnected"); 190 | }; 191 | 192 | // IRC Commands 193 | 194 | IRC.prototype._send = function () { 195 | if (!this._connected || !this._socket) return; 196 | 197 | if (this.options.protocol === "irc") { 198 | this._socket.write(Array.prototype.join.call(arguments, " ") + "\r\n"); 199 | } else { 200 | this._socket.send(Array.prototype.join.call(arguments, " ") + "\r\n"); 201 | } 202 | }; 203 | 204 | IRC.prototype.raw = IRC.prototype._send; 205 | 206 | IRC.prototype.join = function (channel) { 207 | this._send("JOIN", channel); 208 | }; 209 | 210 | IRC.prototype.part = function (channel) { 211 | this._send("PART", channel); 212 | }; 213 | 214 | IRC.prototype.privmsg = function (channel, message) { 215 | this._send("PRIVMSG", channel, ":" + message); 216 | }; 217 | 218 | module.exports = IRC; 219 | -------------------------------------------------------------------------------- /public_html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |Twitch sometimes has "twitches" in its infrastructure, and their response time to outages can be lacking. This page serves as a tool to unofficially check on service problems at Twitch.
72 |
This site monitors the ingest servers, the chat servers, and the web services over at Twitch remotely. The statistics generated on this site will be gathered by a single server. We will attempt to provide accurate data measurements, but they could be inaccurate. Service statistics are updated every 30 seconds.
80 || Service | 101 |Description | 102 |Status | 103 |Load Time | 104 |
|---|
| Server | 127 |Location | 128 |Status | 129 |
|---|
| Server | 156 |Protocol | 157 |Status | 158 |Errors (5 min) | 159 |Lag (Avg) | 160 |Lag Graph | 161 |
|---|
r&&(r=u)}this.stacked=H,this.regionShapes={},this.barWidth=k,this.barSpacing=l,this.totalBarWidth=k+l,this.width=e=c.length*k+(c.length-1)*l,this.initTarget(),o&&(F=m===undefined?-Infinity:m,G=n===undefined?Infinity:n),x=[],w=H?[]:x;var Q=[],R=[];for(y=0,z=c.length;y q&&a.drawCircle((r-h)*u+j,f/2,d.get("spotRadius"),d.get("outlierLineColor"),d.get("outlierFillColor")).append()),a.drawRect(Math.round((n-h)*u+j),Math.round(f*.1),Math.round((p-n)*u),Math.round(f*.8),d.get("boxLineColor"),d.get("boxFillColor")).append(),a.drawLine(Math.round((k-h)*u+j),Math.round(f/2),Math.round((n-h)*u+j),Math.round(f/2),d.get("lineColor")).append(),a.drawLine(Math.round((k-h)*u+j),Math.round(f/4),Math.round((k-h)*u+j),Math.round(f-f/4),d.get("whiskerColor")).append(),a.drawLine(Math.round((q-h)*u+j),Math.round(f/2),Math.round((p-h)*u+j),Math.round(f/2),d.get("lineColor")).append(),a.drawLine(Math.round((q-h)*u+j),Math.round(f/4),Math.round((q-h)*u+j),Math.round(f-f/4),d.get("whiskerColor")).append(),a.drawLine(Math.round((o-h)*u+j),Math.round(f*.1),Math.round((o-h)*u+j),Math.round(f*.9),d.get("medianColor")).append(),d.get("target")&&(t=Math.ceil(d.get("spotRadius")),a.drawLine(Math.round((d.get("target")-h)*u+j),Math.round(f/2-t),Math.round((d.get("target")-h)*u+j),Math.round(f/2+t),d.get("targetColor")).append(),a.drawLine(Math.round((d.get("target")-h)*u+j-t),Math.round(f/2),Math.round((d.get("target")-h)*u+j+t),Math.round(f/2),d.get("targetColor")).append()),a.render()}}),function(){document.namespaces&&!document.namespaces.v?(a.fn.sparkline.hasVML=!0,document.namespaces.add("v","urn:schemas-microsoft-com:vml","#default#VML")):a.fn.sparkline.hasVML=!1;var b=document.createElement("canvas");a.fn.sparkline.hasCanvas=!!b.getContext&&!!b.getContext("2d")}(),D=d({init:function(a,b,c,d){this.target=a,this.id=b,this.type=c,this.args=d},append:function(){return this.target.appendShape(this),this}}),E=d({_pxregex:/(\d+)(px)?\s*$/i,init:function(b,c,d){if(!b)return;this.width=b,this.height=c,this.target=d,this.lastShapeId=null,d[0]&&(d=d[0]),a.data(d,"_jqs_vcanvas",this)},drawLine:function(a,b,c,d,e,f){return this.drawShape([[a,b],[c,d]],e,f)},drawShape:function(a,b,c,d){return this._genShape("Shape",[a,b,c,d])},drawCircle:function(a,b,c,d,e,f){return this._genShape("Circle",[a,b,c,d,e,f])},drawPieSlice:function(a,b,c,d,e,f,g){return this._genShape("PieSlice",[a,b,c,d,e,f,g])},drawRect:function(a,b,c,d,e,f){return this._genShape("Rect",[a,b,c,d,e,f])},getElement:function(){return this.canvas},getLastShapeId:function(){return this.lastShapeId},reset:function(){alert("reset not implemented")},_insert:function(b,c){a(c).html(b)},_calculatePixelDims:function(b,c,d){var e;e=this._pxregex.exec(c),e?this.pixelHeight=e[1]:this.pixelHeight=a(d).height(),e=this._pxregex.exec(b),e?this.pixelWidth=e[1]:this.pixelWidth=a(d).width()},_genShape:function(a,b){var c=I++;return b.unshift(c),new D(this,c,a,b)},appendShape:function(a){alert("appendShape not implemented")},replaceWithShape:function(a,b){alert("replaceWithShape not implemented")},insertAfterShape:function(a,b){alert("insertAfterShape not implemented")},removeShapeId:function(a){alert("removeShapeId not implemented")},getShapeAt:function(a,b,c){alert("getShapeAt not implemented")},render:function(){alert("render not implemented")}}),F=d(E,{init:function(b,c,d,e){F._super.init.call(this,b,c,d),this.canvas=document.createElement("canvas"),d[0]&&(d=d[0]),a.data(d,"_jqs_vcanvas",this),a(this.canvas).css({display:"inline-block",width:b,height:c,verticalAlign:"top"}),this._insert(this.canvas,d),this._calculatePixelDims(b,c,this.canvas),this.canvas.width=this.pixelWidth,this.canvas.height=this.pixelHeight,this.interact=e,this.shapes={},this.shapeseq=[],this.currentTargetShapeId=undefined,a(this.canvas).css({width:this.pixelWidth,height:this.pixelHeight})},_getContext:function(a,b,c){var d=this.canvas.getContext("2d");return a!==undefined&&(d.strokeStyle=a),d.lineWidth=c===undefined?1:c,b!==undefined&&(d.fillStyle=b),d},reset:function(){var a=this._getContext();a.clearRect(0,0,this.pixelWidth,this.pixelHeight),this.shapes={},this.shapeseq=[],this.currentTargetShapeId=undefined},_drawShape:function(a,b,c,d,e){var f=this._getContext(c,d,e),g,h;f.beginPath(),f.moveTo(b[0][0]+.5,b[0][1]+.5);for(g=1,h=b.length;g