├── .gitignore ├── client ├── robots.txt ├── audio │ └── pop.ogg ├── img │ ├── favicon.png │ ├── logo-64.png │ ├── touch-icon-192x192.png │ ├── apple-touch-icon-120x120.png │ ├── logo.svg │ └── logo-dark.svg ├── themes │ ├── example.css │ ├── crypto.css │ ├── morning.css │ └── zenburn.css ├── css │ └── fonts │ │ ├── fontawesome.woff │ │ ├── inconsolatag.ttf │ │ ├── inconsolatag.woff │ │ ├── Lato-700 │ │ ├── Lato-700.eot │ │ ├── Lato-700.ttf │ │ ├── Lato-700.woff │ │ ├── Lato-700.woff2 │ │ └── LICENSE.txt │ │ ├── Lato-regular │ │ ├── Lato-regular.eot │ │ ├── Lato-regular.ttf │ │ ├── Lato-regular.woff │ │ ├── Lato-regular.woff2 │ │ └── LICENSE.txt │ │ ├── Open-Sans-300 │ │ ├── Open-Sans-300.eot │ │ ├── Open-Sans-300.ttf │ │ ├── Open-Sans-300.woff │ │ ├── Open-Sans-300.woff2 │ │ └── LICENSE.txt │ │ ├── Open-Sans-700 │ │ ├── Open-Sans-700.eot │ │ ├── Open-Sans-700.ttf │ │ ├── Open-Sans-700.woff │ │ ├── Open-Sans-700.woff2 │ │ └── LICENSE.txt │ │ └── Open-Sans-regular │ │ ├── Open-Sans-regular.eot │ │ ├── Open-Sans-regular.ttf │ │ ├── Open-Sans-regular.woff │ │ ├── Open-Sans-regular.woff2 │ │ └── LICENSE.txt ├── js │ └── libs │ │ ├── handlebars │ │ ├── stringcolor.js │ │ ├── users.js │ │ ├── diff.js │ │ ├── tz.js │ │ ├── equal.js │ │ ├── modes.js │ │ └── parse.js │ │ ├── string.contains.js │ │ ├── jquery │ │ ├── stickyscroll.js │ │ ├── inputhistory.js │ │ ├── cookie.js │ │ ├── tabcomplete.js │ │ └── tse.js │ │ ├── stringcolor.js │ │ └── notification.js ├── views │ ├── network.tpl │ ├── chan.tpl │ ├── msg_action.tpl │ ├── toggle.tpl │ ├── user.tpl │ ├── msg.tpl │ └── chat.tpl └── index.html ├── docker-compose.yml ├── index.js ├── .eslintignore ├── .gitattributes ├── test ├── fixtures │ └── .shout │ │ └── config.js ├── util.js └── plugins │ └── link.js ├── src ├── models │ ├── user.js │ ├── msg.js │ ├── chan.js │ └── network.js ├── plugins │ ├── inputs │ │ ├── nick.js │ │ ├── join.js │ │ ├── kick.js │ │ ├── invite.js │ │ ├── whois.js │ │ ├── raw.js │ │ ├── connect.js │ │ ├── topic.js │ │ ├── quit.js │ │ ├── part.js │ │ ├── mode.js │ │ ├── services.js │ │ ├── msg.js │ │ ├── notice.js │ │ └── action.js │ └── irc-events │ │ ├── motd.js │ │ ├── names.js │ │ ├── error.js │ │ ├── welcome.js │ │ ├── ctcp.js │ │ ├── invite.js │ │ ├── topic.js │ │ ├── notice.js │ │ ├── quit.js │ │ ├── mode.js │ │ ├── join.js │ │ ├── part.js │ │ ├── whois.js │ │ ├── kick.js │ │ ├── nick.js │ │ ├── message.js │ │ └── link.js ├── helper.js ├── command-line │ ├── config.js │ ├── list.js │ ├── edit.js │ ├── remove.js │ ├── index.js │ ├── reset.js │ ├── start.js │ └── add.js ├── identd.js ├── log.js ├── clientManager.js ├── server.js └── client.js ├── .travis.yml ├── .editorconfig ├── README.md ├── Dockerfile ├── .eslintrc ├── Gruntfile.js ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── defaults └── config.js └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /client/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | shout: 2 | build: . 3 | ports: 4 | - "9000:9000" 5 | -------------------------------------------------------------------------------- /client/audio/pop.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/audio/pop.ogg -------------------------------------------------------------------------------- /client/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/img/favicon.png -------------------------------------------------------------------------------- /client/img/logo-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/img/logo-64.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | process.chdir(__dirname); 3 | require("./src/command-line"); 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | client/js/libs.min.js 2 | client/js/libs/**/*.js 3 | client/js/shout.templates.js 4 | -------------------------------------------------------------------------------- /client/themes/example.css: -------------------------------------------------------------------------------- 1 | /** 2 | * This is just an empty theme. 3 | */ 4 | 5 | body { 6 | } 7 | -------------------------------------------------------------------------------- /client/css/fonts/fontawesome.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/fontawesome.woff -------------------------------------------------------------------------------- /client/css/fonts/inconsolatag.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/inconsolatag.ttf -------------------------------------------------------------------------------- /client/css/fonts/inconsolatag.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/inconsolatag.woff -------------------------------------------------------------------------------- /client/img/touch-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/img/touch-icon-192x192.png -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | *.eot binary 3 | *.ttf binary 4 | *.woff binary 5 | *.woff2 binary 6 | *.png binary 7 | -------------------------------------------------------------------------------- /client/css/fonts/Lato-700/Lato-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Lato-700/Lato-700.eot -------------------------------------------------------------------------------- /client/css/fonts/Lato-700/Lato-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Lato-700/Lato-700.ttf -------------------------------------------------------------------------------- /client/css/fonts/Lato-700/Lato-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Lato-700/Lato-700.woff -------------------------------------------------------------------------------- /client/css/fonts/Lato-700/Lato-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Lato-700/Lato-700.woff2 -------------------------------------------------------------------------------- /client/img/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/img/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /client/css/fonts/Lato-regular/Lato-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Lato-regular/Lato-regular.eot -------------------------------------------------------------------------------- /client/css/fonts/Lato-regular/Lato-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Lato-regular/Lato-regular.ttf -------------------------------------------------------------------------------- /client/css/fonts/Lato-regular/Lato-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Lato-regular/Lato-regular.woff -------------------------------------------------------------------------------- /client/css/fonts/Lato-regular/Lato-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Lato-regular/Lato-regular.woff2 -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-300/Open-Sans-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-300/Open-Sans-300.eot -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-300/Open-Sans-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-300/Open-Sans-300.ttf -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-300/Open-Sans-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-300/Open-Sans-300.woff -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-300/Open-Sans-300.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-300/Open-Sans-300.woff2 -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-700/Open-Sans-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-700/Open-Sans-700.eot -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-700/Open-Sans-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-700/Open-Sans-700.ttf -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-700/Open-Sans-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-700/Open-Sans-700.woff -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-700/Open-Sans-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-700/Open-Sans-700.woff2 -------------------------------------------------------------------------------- /client/js/libs/handlebars/stringcolor.js: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper( 2 | "stringcolor", function(str) { 3 | return stringcolor(str); 4 | } 5 | ); 6 | -------------------------------------------------------------------------------- /test/fixtures/.shout/config.js: -------------------------------------------------------------------------------- 1 | var config = require("../../../defaults/config.js"); 2 | 3 | config.prefetch = true; 4 | 5 | module.exports = config; 6 | -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-regular/Open-Sans-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-regular/Open-Sans-regular.eot -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-regular/Open-Sans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-regular/Open-Sans-regular.ttf -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff -------------------------------------------------------------------------------- /client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/erming/shout/HEAD/client/css/fonts/Open-Sans-regular/Open-Sans-regular.woff2 -------------------------------------------------------------------------------- /client/js/libs/handlebars/users.js: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper( 2 | "users", function(count) { 3 | return count + " " + (count == 1 ? "user" : "users"); 4 | } 5 | ); 6 | -------------------------------------------------------------------------------- /client/views/network.tpl: -------------------------------------------------------------------------------- 1 | {{#each networks}} 2 |
3 | {{partial "chan"}} 4 |
5 | {{/each}} 6 | -------------------------------------------------------------------------------- /src/models/user.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | module.exports = User; 4 | 5 | function User(attr) { 6 | _.merge(this, _.extend({ 7 | mode: "", 8 | name: "" 9 | }, attr)); 10 | } 11 | -------------------------------------------------------------------------------- /src/plugins/inputs/nick.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "nick") { 3 | return; 4 | } 5 | if (args.length !== 0) { 6 | var irc = network.irc; 7 | irc.nick(args[0]); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/plugins/inputs/join.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "join") { 3 | return; 4 | } 5 | if (args.length !== 0) { 6 | var irc = network.irc; 7 | irc.join(args[0], args[1]); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/plugins/inputs/kick.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "kick") { 3 | return; 4 | } 5 | if (args.length !== 0) { 6 | var irc = network.irc; 7 | irc.kick(chan.name, args[0]); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/plugins/inputs/invite.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "invite") { 3 | return; 4 | } 5 | var irc = network.irc; 6 | if (args.length === 2) { 7 | irc.invite(args[0], args[1]); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/plugins/inputs/whois.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "whois" && cmd !== "query") { 3 | return; 4 | } 5 | if (args.length !== 0) { 6 | var irc = network.irc; 7 | irc.whois(args[0]); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /client/js/libs/handlebars/diff.js: -------------------------------------------------------------------------------- 1 | var diff; 2 | 3 | Handlebars.registerHelper( 4 | "diff", function(a, opt) { 5 | if (a != diff) { 6 | diff = a; 7 | return opt.fn(this); 8 | } else { 9 | return opt.inverse(this); 10 | } 11 | } 12 | ); 13 | -------------------------------------------------------------------------------- /client/js/libs/handlebars/tz.js: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper( 2 | "tz", function(time) { 3 | if (time) { 4 | var utc = moment.utc(time, "HH:mm:ss").toDate(); 5 | return moment(utc).format("HH:mm"); 6 | } else { 7 | return ""; 8 | } 9 | } 10 | ); 11 | -------------------------------------------------------------------------------- /client/js/libs/handlebars/equal.js: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper( 2 | "equal", function(a, b, opt) { 3 | a = a.toString(); 4 | b = b.toString(); 5 | if (a == b) { 6 | return opt.fn(this); 7 | } else { 8 | return opt.inverse(this); 9 | } 10 | } 11 | ); 12 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | module.exports = { 4 | HOME: (process.env.HOME || process.env.USERPROFILE) + "/.shout", 5 | getConfig: getConfig 6 | }; 7 | 8 | function getConfig() { 9 | return require(path.resolve(this.HOME) + "/config"); 10 | } 11 | -------------------------------------------------------------------------------- /src/plugins/inputs/raw.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "raw" && cmd !== "send" && cmd !== "quote") { 3 | return; 4 | } 5 | if (args.length !== 0) { 6 | var irc = network.irc; 7 | irc.write(args.join(" ")); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/plugins/inputs/connect.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "connect" && cmd !== "server") { 3 | return; 4 | } 5 | if (args.length !== 0) { 6 | var client = this; 7 | client.connect({ 8 | host: args[0] 9 | }); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /client/js/libs/handlebars/modes.js: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper( 2 | "modes", function(mode) { 3 | var modes = { 4 | "~": "owner", 5 | "&": "admin", 6 | "@": "op", 7 | "%": "half-op", 8 | "+": "voice", 9 | "" : "normal" 10 | }; 11 | return modes[mode]; 12 | } 13 | ); 14 | -------------------------------------------------------------------------------- /src/plugins/inputs/topic.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "topic") { 3 | return; 4 | } 5 | 6 | var msg = "TOPIC"; 7 | msg += " " + chan.name; 8 | msg += args[0] ? (" :" + args.join(" ")) : ""; 9 | 10 | var irc = network.irc; 11 | irc.write(msg); 12 | }; 13 | -------------------------------------------------------------------------------- /client/views/chan.tpl: -------------------------------------------------------------------------------- 1 | {{#each channels}} 2 |
3 | {{#if unread}}{{unread}}{{/if}} 4 | 5 | {{name}} 6 |
7 | {{/each}} 8 | -------------------------------------------------------------------------------- /client/views/msg_action.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{tz time}} 4 | 5 | 6 | 7 | {{mode}}{{from}} 8 | {{formattedAction}} 9 | {{{parse text}}} 10 | 11 |
12 | -------------------------------------------------------------------------------- /src/command-line/config.js: -------------------------------------------------------------------------------- 1 | var program = require("commander"); 2 | var child = require("child_process"); 3 | var Helper = require("../helper"); 4 | 5 | program 6 | .command("config") 7 | .description("Edit config: '" + Helper.HOME + "/config.js'") 8 | .action(function() { 9 | child.spawn( 10 | process.env.EDITOR || "vi", 11 | [Helper.HOME + "/config.js"], 12 | {stdio: "inherit"} 13 | ); 14 | }); 15 | -------------------------------------------------------------------------------- /client/js/libs/string.contains.js: -------------------------------------------------------------------------------- 1 | // 2 | // Check if string contains any of the supplied words. 3 | // 4 | // Usage: 5 | // "".contains(a, b, ...); 6 | // 7 | // Returns [true|false] 8 | // 9 | String.prototype.contains = function() { 10 | var args = arguments; 11 | for (var i in args) { 12 | var str = args[i]; 13 | if (typeof str === "string" && this.indexOf(str) > -1) { 14 | return true; 15 | } 16 | } 17 | return false; 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | - '0.12' 5 | - '4.0' 6 | - '4.1' 7 | - '4.2' 8 | sudo: false 9 | deploy: 10 | provider: npm 11 | email: jeremie@astori.fr 12 | api_key: 13 | secure: dbfoL5w4SuXZJxQJ6bIlb5dXLdafJt4n9nOgTsAaFTkzBbf/L9JDTWAsSXqnelwVFgu+jNqFN5l9CyMpQ0o9IBdWEaryh3FzFeaNGIGV4+2StYKoxx2c4ZUBejbr++HVa0Ha9HWZCWkpIGiLI1W52hEu+QuFnoAbeQvG+lyhQsY= 14 | on: 15 | node: '0.12' 16 | tags: true 17 | repo: erming/shout 18 | -------------------------------------------------------------------------------- /client/views/toggle.tpl: -------------------------------------------------------------------------------- 1 | {{#toggle}} 2 |
3 | {{#equal type "image"}} 4 | 5 | 6 | 7 | {{else}} 8 | 9 | {{#if thumb}} 10 | 11 | {{/if}} 12 |
{{{parse head}}}
13 |
14 | {{body}} 15 |
16 |
17 | {{/equal}} 18 |
19 | {{/toggle}} 20 | -------------------------------------------------------------------------------- /src/plugins/inputs/quit.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | module.exports = function(network, chan, cmd, args) { 4 | if (cmd !== "quit" && cmd !== "disconnect") { 5 | return; 6 | } 7 | 8 | var client = this; 9 | var irc = network.irc; 10 | var quitMessage = args[0] ? args.join(" ") : ""; 11 | 12 | client.networks = _.without(client.networks, network); 13 | client.save(); 14 | client.emit("quit", { 15 | network: network.id 16 | }); 17 | 18 | irc.quit(quitMessage); 19 | }; 20 | -------------------------------------------------------------------------------- /src/plugins/irc-events/motd.js: -------------------------------------------------------------------------------- 1 | var Msg = require("../../models/msg"); 2 | 3 | module.exports = function(irc, network) { 4 | var client = this; 5 | irc.on("motd", function(data) { 6 | var lobby = network.channels[0]; 7 | data.motd.forEach(function(text) { 8 | var msg = new Msg({ 9 | type: Msg.Type.MOTD, 10 | text: text 11 | }); 12 | lobby.messages.push(msg); 13 | client.emit("msg", { 14 | chan: lobby.id, 15 | msg: msg 16 | }); 17 | }); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | indent_style = tab 9 | 10 | end_of_line = lf 11 | charset = utf-8 12 | trim_trailing_whitespace = true 13 | insert_final_newline = true 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.{json,yml}] 19 | indent_style = space 20 | indent_size = 2 21 | 22 | [.eslintrc] 23 | indent_style = space 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /src/plugins/inputs/part.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | module.exports = function(network, chan, cmd, args) { 4 | if (cmd !== "part" && cmd !== "leave" && cmd !== "close") { 5 | return; 6 | } 7 | var client = this; 8 | if (chan.type === "query") { 9 | network.channels = _.without(network.channels, chan); 10 | client.emit("part", { 11 | chan: chan.id 12 | }); 13 | } else { 14 | var irc = network.irc; 15 | if (args.length === 0) { 16 | args.push(chan.name); 17 | } 18 | irc.part(args); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /client/views/user.tpl: -------------------------------------------------------------------------------- 1 | {{#if users.length}} 2 |
3 | 4 |
5 | {{/if}} 6 |
7 |
8 | {{#diff "reset"}}{{/diff}} 9 | {{#each users}} 10 | {{#diff mode}} 11 | {{#unless @first}} 12 |
13 | {{/unless}} 14 |
15 | {{/diff}} 16 | 17 | {{/each}} 18 |
19 |
20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (~~Shout~~) DEPRECATED 2 | 3 | Use this very active fork instead: https://github.com/thelounge/thelounge 4 | 5 | ## Install 6 | 7 | ``` 8 | sudo npm install -g shout 9 | ``` 10 | 11 | ## Usage 12 | 13 | When the install is complete, go ahead and run this in your terminal: 14 | 15 | ``` 16 | shout --help 17 | ``` 18 | 19 | ## Official forks 20 | 21 | Check out [The Lounge](https://github.com/thelounge) which is an actively maintained fork: https://github.com/thelounge 22 | 23 | ## License 24 | 25 | Available under the [MIT License](http://mths.be/mit). 26 | -------------------------------------------------------------------------------- /src/plugins/irc-events/names.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var User = require("../../models/user"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("names", function(data) { 7 | var chan = _.findWhere(network.channels, {name: data.channel}); 8 | if (typeof chan === "undefined") { 9 | return; 10 | } 11 | chan.users = []; 12 | _.each(data.names, function(u) { 13 | chan.users.push(new User(u)); 14 | }); 15 | chan.sortUsers(); 16 | client.emit("users", { 17 | chan: chan.id, 18 | users: chan.users 19 | }); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/plugins/irc-events/error.js: -------------------------------------------------------------------------------- 1 | var Msg = require("../../models/msg"); 2 | 3 | module.exports = function(irc, network) { 4 | var client = this; 5 | irc.on("errors", function(data) { 6 | var lobby = network.channels[0]; 7 | var msg = new Msg({ 8 | type: Msg.Type.ERROR, 9 | text: data.message, 10 | }); 11 | client.emit("msg", { 12 | chan: lobby.id, 13 | msg: msg 14 | }); 15 | if (!network.connected) { 16 | if (data.cmd === "ERR_NICKNAMEINUSE") { 17 | var random = irc.me + Math.floor(10 + (Math.random() * 89)); 18 | irc.nick(random); 19 | } 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /src/command-line/list.js: -------------------------------------------------------------------------------- 1 | var ClientManager = new require("../clientManager"); 2 | var program = require("commander"); 3 | 4 | program 5 | .command("list") 6 | .description("List all users") 7 | .action(function() { 8 | var users = new ClientManager().getUsers(); 9 | if (!users.length) { 10 | console.log(""); 11 | console.log("No users found!"); 12 | console.log(""); 13 | } else { 14 | console.log(""); 15 | console.log("Users:"); 16 | for (var i = 0; i < users.length; i++) { 17 | console.log(" " + (i + 1) + ". " + users[i]); 18 | } 19 | console.log(""); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /client/views/msg.tpl: -------------------------------------------------------------------------------- 1 |
2 | 3 | {{tz time}} 4 | 5 | 6 | {{#if from}} 7 | {{mode}}{{from}} 8 | {{/if}} 9 | 10 | 11 | {{#equal type "toggle"}} 12 |
13 | 14 |
15 | {{#if toggle}} 16 | {{partial "toggle"}} 17 | {{/if}} 18 | {{else}} 19 | {{{parse text}}} 20 | {{/equal}} 21 |
22 |
23 | -------------------------------------------------------------------------------- /src/plugins/irc-events/welcome.js: -------------------------------------------------------------------------------- 1 | var Msg = require("../../models/msg"); 2 | 3 | module.exports = function(irc, network) { 4 | var client = this; 5 | irc.on("welcome", function(data) { 6 | network.connected = true; 7 | irc.write("PING " + network.host); 8 | var lobby = network.channels[0]; 9 | var nick = data; 10 | var msg = new Msg({ 11 | text: "You're now known as " + nick 12 | }); 13 | lobby.messages.push(msg); 14 | client.emit("msg", { 15 | chan: lobby.id, 16 | msg: msg 17 | }); 18 | client.save(); 19 | client.emit("nick", { 20 | network: network.id, 21 | nick: nick 22 | }); 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /src/plugins/irc-events/ctcp.js: -------------------------------------------------------------------------------- 1 | var pkg = require(process.cwd() + "/package.json"); 2 | 3 | module.exports = function(irc/* , network */) { 4 | irc.on("message", function(data) { 5 | if (data.message.indexOf("\001") !== 0) { 6 | return; 7 | } 8 | var msg = data.message.replace(/\001/g, ""); 9 | var split = msg.split(" "); 10 | switch (split[0]) { 11 | case "VERSION": 12 | irc.ctcp( 13 | data.from, 14 | "VERSION " + pkg.name + " " + pkg.version 15 | ); 16 | break; 17 | case "PING": 18 | if (split.length === 2) { 19 | irc.ctcp(data.from, "PING " + split[1]); 20 | } 21 | break; 22 | } 23 | }); 24 | }; 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Thanks to @Xe for the Dockerfile template 3 | # https://github.com/Shuo-IRC/Shuo/pull/87/files 4 | # 5 | 6 | FROM node:4.0-onbuild 7 | 8 | # Create a non-root user for shout to run in. 9 | RUN useradd --create-home shout 10 | 11 | # Needed for setup of Node.js 12 | ENV HOME /home/shout 13 | 14 | # Customize this to specify where Shout puts its data. 15 | # To link a data container, have it expose /home/shout/data 16 | ENV SHOUT_HOME /home/shout/data 17 | 18 | # Expose HTTP 19 | EXPOSE 9000 20 | 21 | # Drop root. 22 | USER shout 23 | 24 | # Don't use an entrypoint here. It makes debugging difficult. 25 | CMD node index.js --home $SHOUT_HOME 26 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | root: true 4 | 5 | env: 6 | browser: true 7 | mocha: true 8 | node: true 9 | 10 | rules: 11 | comma-dangle: 0 12 | curly: [2, multi-line] 13 | eqeqeq: 2 14 | indent: [2, tab] 15 | linebreak-style: [2, unix] 16 | object-curly-spacing: [2, never] 17 | semi: [2, always] 18 | space-after-keywords: [2, always] 19 | space-before-function-paren: [2, never] 20 | spaced-comment: [2, always] 21 | no-console: 0 22 | no-trailing-spaces: 2 23 | quotes: [2, double, avoid-escape] 24 | 25 | globals: 26 | $: false 27 | Favico: false 28 | Handlebars: false 29 | io: false 30 | Mousetrap: false 31 | 32 | extends: eslint:recommended 33 | -------------------------------------------------------------------------------- /src/plugins/inputs/mode.js: -------------------------------------------------------------------------------- 1 | module.exports = function(network, chan, cmd, args) { 2 | if (cmd !== "mode" && cmd !== "op" && cmd !== "voice" && cmd !== "deop" && cmd !== "devoice") { 3 | return; 4 | } else if (args.length === 0) { 5 | return; 6 | } 7 | 8 | var mode; 9 | var user; 10 | if (cmd !== "mode") { 11 | user = args[0]; 12 | mode = { 13 | "op": "+o", 14 | "voice": "+v", 15 | "deop": "-o", 16 | "devoice": "-v" 17 | }[cmd]; 18 | } else if (args.length === 1) { 19 | return; 20 | } else { 21 | mode = args[0]; 22 | user = args[1]; 23 | } 24 | var irc = network.irc; 25 | irc.mode( 26 | chan.name, 27 | mode, 28 | user 29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /src/plugins/inputs/services.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | module.exports = function(network, chan, cmd, args) { 4 | if (cmd !== "ns" && cmd !== "cs" && cmd !== "hs") { 5 | return; 6 | } 7 | var target = ({ 8 | "ns": "nickserv", 9 | "cs": "chanserv", 10 | "hs": "hostserv", 11 | })[cmd]; 12 | if (!target || args.length === 0 || args[0] === "") { 13 | return; 14 | } 15 | var irc = network.irc; 16 | var msg = args.join(" "); 17 | irc.send(target, msg); 18 | var channel = _.find(network.channels, {name: target}); 19 | if (typeof channel !== "undefined") { 20 | irc.emit("message", { 21 | from: irc.me, 22 | to: channel.name, 23 | message: msg 24 | }); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /src/models/msg.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var moment = require("moment"); 3 | 4 | Msg.Type = { 5 | ACTION: "action", 6 | ERROR: "error", 7 | INVITE: "invite", 8 | JOIN: "join", 9 | KICK: "kick", 10 | MESSAGE: "message", 11 | MODE: "mode", 12 | MOTD: "motd", 13 | NICK: "nick", 14 | NOTICE: "notice", 15 | PART: "part", 16 | QUIT: "quit", 17 | TOGGLE: "toggle", 18 | TOPIC: "topic", 19 | WHOIS: "whois" 20 | }; 21 | 22 | module.exports = Msg; 23 | 24 | var id = 0; 25 | 26 | function Msg(attr) { 27 | _.merge(this, _.extend({ 28 | from: "", 29 | id: id++, 30 | text: "", 31 | time: moment().utc().format("HH:mm:ss"), 32 | type: Msg.Type.MESSAGE, 33 | self: false 34 | }, attr)); 35 | } 36 | -------------------------------------------------------------------------------- /src/command-line/edit.js: -------------------------------------------------------------------------------- 1 | var ClientManager = new require("../clientManager"); 2 | var program = require("commander"); 3 | var child = require("child_process"); 4 | var Helper = require("../helper"); 5 | 6 | program 7 | .command("edit ") 8 | .description("Edit user: '" + Helper.HOME + "/users/.json'") 9 | .action(function(name) { 10 | var users = new ClientManager().getUsers(); 11 | if (users.indexOf(name) === -1) { 12 | console.log(""); 13 | console.log("User '" + name + "' doesn't exist."); 14 | console.log(""); 15 | return; 16 | } 17 | child.spawn( 18 | process.env.EDITOR || "vi", 19 | [require("path").join(Helper.HOME, "users", name + ".json")], 20 | {stdio: "inherit"} 21 | ); 22 | }); 23 | -------------------------------------------------------------------------------- /src/plugins/inputs/msg.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | module.exports = function(network, chan, cmd, args) { 4 | if (cmd !== "say" && cmd !== "msg") { 5 | return; 6 | } 7 | if (args.length === 0 || args[0] === "") { 8 | return; 9 | } 10 | var irc = network.irc; 11 | var target = ""; 12 | if (cmd === "msg") { 13 | target = args.shift(); 14 | if (args.length === 0) { 15 | return; 16 | } 17 | } else { 18 | target = chan.name; 19 | } 20 | var msg = args.join(" "); 21 | irc.send(target, msg); 22 | var channel = _.find(network.channels, {name: target}); 23 | if (typeof channel !== "undefined") { 24 | irc.emit("message", { 25 | from: irc.me, 26 | to: channel.name, 27 | message: msg 28 | }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/plugins/irc-events/invite.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("invite", function(data) { 7 | var target = data.to; 8 | if (target.toLowerCase() === irc.me.toLowerCase()) { 9 | target = "you"; 10 | } 11 | 12 | var chan = _.findWhere(network.channels, {name: data.channel}); 13 | if (typeof chan === "undefined") { 14 | chan = network.channels[0]; 15 | } 16 | 17 | var msg = new Msg({ 18 | type: Msg.Type.INVITE, 19 | from: data.from, 20 | target: target, 21 | text: data.channel 22 | }); 23 | chan.messages.push(msg); 24 | client.emit("msg", { 25 | chan: chan.id, 26 | msg: msg 27 | }); 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /src/plugins/inputs/notice.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(network, chan, cmd, args) { 5 | if (cmd !== "notice" || !args[1]) { 6 | return; 7 | } 8 | 9 | var message = args.slice(1).join(" "); 10 | var irc = network.irc; 11 | irc.notice(args[0], message); 12 | 13 | var targetChan = _.findWhere(network.channels, {name: args[0]}); 14 | if (typeof targetChan === "undefined") { 15 | message = "{to " + args[0] + "} " + message; 16 | targetChan = chan; 17 | } 18 | 19 | var msg = new Msg({ 20 | type: Msg.Type.NOTICE, 21 | mode: targetChan.getMode(irc.me), 22 | from: irc.me, 23 | text: message 24 | }); 25 | targetChan.messages.push(msg); 26 | this.emit("msg", { 27 | chan: targetChan.id, 28 | msg: msg 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/plugins/inputs/action.js: -------------------------------------------------------------------------------- 1 | var Msg = require("../../models/msg"); 2 | 3 | module.exports = function(network, chan, cmd, args) { 4 | if (cmd !== "slap" && cmd !== "me") { 5 | return; 6 | } 7 | 8 | var client = this; 9 | var irc = network.irc; 10 | 11 | switch (cmd) { 12 | case "slap": 13 | var slap = "slaps " + args[0] + " around a bit with a large trout"; 14 | /* fall through */ 15 | case "me": 16 | if (args.length === 0) { 17 | break; 18 | } 19 | 20 | var text = slap || args.join(" "); 21 | irc.action( 22 | chan.name, 23 | text 24 | ); 25 | 26 | var msg = new Msg({ 27 | type: Msg.Type.ACTION, 28 | mode: chan.getMode(irc.me), 29 | from: irc.me, 30 | text: text 31 | }); 32 | chan.messages.push(msg); 33 | client.emit("msg", { 34 | chan: chan.id, 35 | msg: msg 36 | }); 37 | break; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/plugins/irc-events/topic.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("topic", function(data) { 7 | var chan = _.findWhere(network.channels, {name: data.channel}); 8 | if (typeof chan === "undefined") { 9 | return; 10 | } 11 | var from = data.nick || chan.name; 12 | var topic = data.topic; 13 | 14 | var msg = new Msg({ 15 | type: Msg.Type.TOPIC, 16 | mode: chan.getMode(from), 17 | from: from, 18 | text: topic, 19 | self: (from.toLowerCase() === irc.me.toLowerCase()) 20 | }); 21 | chan.messages.push(msg); 22 | client.emit("msg", { 23 | chan: chan.id, 24 | msg: msg 25 | }); 26 | chan.topic = topic; 27 | client.emit("topic", { 28 | chan: chan.id, 29 | topic: chan.topic 30 | }); 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/plugins/irc-events/notice.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("notice", function(data) { 7 | var target = data.to; 8 | if (target.toLowerCase() === irc.me.toLowerCase()) { 9 | target = data.from; 10 | } 11 | 12 | var chan = _.findWhere(network.channels, {name: target}); 13 | if (typeof chan === "undefined") { 14 | chan = network.channels[0]; 15 | } 16 | 17 | var from = data.from || ""; 18 | if (data.to === "*" || data.from.indexOf(".") !== -1) { 19 | from = ""; 20 | } 21 | var msg = new Msg({ 22 | type: Msg.Type.NOTICE, 23 | mode: chan.getMode(from), 24 | from: from, 25 | text: data.message 26 | }); 27 | chan.messages.push(msg); 28 | client.emit("msg", { 29 | chan: chan.id, 30 | msg: msg 31 | }); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | var libs = "client/js/libs/**/*.js"; 3 | grunt.initConfig({ 4 | watch: { 5 | files: libs, 6 | tasks: ["uglify"] 7 | }, 8 | uglify: { 9 | options: { 10 | compress: false 11 | }, 12 | js: { 13 | files: { 14 | "client/js/libs.min.js": libs 15 | } 16 | } 17 | } 18 | }); 19 | grunt.loadNpmTasks("grunt-contrib-uglify"); 20 | grunt.loadNpmTasks("grunt-contrib-watch"); 21 | grunt.registerTask( 22 | "build", 23 | function() { 24 | grunt.util.spawn({ 25 | cmd: "node", 26 | args: [ 27 | "node_modules/handlebars/bin/handlebars", 28 | "client/views/", 29 | "-e", "tpl", 30 | "-f", "client/js/shout.templates.js" 31 | ] 32 | }, function(err) { 33 | if (err) console.log(err); 34 | }); 35 | } 36 | ); 37 | grunt.registerTask( 38 | "default", 39 | ["uglify", "build"] 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require("events").EventEmitter; 2 | var util = require("util"); 3 | var _ = require("lodash"); 4 | var express = require("express"); 5 | 6 | function MockClient(opts) { 7 | this.me = "test-user"; 8 | 9 | for (var k in opts) { 10 | this[k] = opts[k]; 11 | } 12 | } 13 | util.inherits(MockClient, EventEmitter); 14 | 15 | MockClient.prototype.createMessage = function(opts) { 16 | 17 | var message = _.extend({ 18 | message: "dummy message", 19 | from: "test-user", 20 | to: "test-channel" 21 | }, opts); 22 | 23 | this.emit("message", message); 24 | }; 25 | 26 | module.exports = { 27 | createClient: function() { 28 | return new MockClient(); 29 | }, 30 | createNetwork: function() { 31 | return { 32 | channels: [{ 33 | name: "test-channel", 34 | messages: [] 35 | }] 36 | }; 37 | }, 38 | createWebserver: function() { 39 | return express(); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/plugins/irc-events/quit.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("quit", function(data) { 7 | network.channels.forEach(function(chan) { 8 | var from = data.nick; 9 | var user = _.findWhere(chan.users, {name: from}); 10 | if (typeof user === "undefined") { 11 | return; 12 | } 13 | chan.users = _.without(chan.users, user); 14 | client.emit("users", { 15 | chan: chan.id, 16 | users: chan.users 17 | }); 18 | var reason = data.message || ""; 19 | if (reason.length > 0) { 20 | reason = "(" + reason + ")"; 21 | } 22 | var msg = new Msg({ 23 | type: Msg.Type.QUIT, 24 | mode: chan.getMode(from), 25 | text: reason, 26 | from: from 27 | }); 28 | chan.messages.push(msg); 29 | client.emit("msg", { 30 | chan: chan.id, 31 | msg: msg 32 | }); 33 | }); 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /src/plugins/irc-events/mode.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("mode", function(data) { 7 | var chan = _.findWhere(network.channels, {name: data.target}); 8 | if (typeof chan !== "undefined") { 9 | setTimeout(function() { 10 | irc.write("NAMES " + data.target); 11 | }, 200); 12 | var from = data.nick; 13 | if (from.indexOf(".") !== -1) { 14 | from = data.target; 15 | } 16 | var self = false; 17 | if (from.toLowerCase() === irc.me.toLowerCase()) { 18 | self = true; 19 | } 20 | var msg = new Msg({ 21 | type: Msg.Type.MODE, 22 | mode: chan.getMode(from), 23 | from: from, 24 | text: data.mode + " " + (data.client || ""), 25 | self: self 26 | }); 27 | chan.messages.push(msg); 28 | client.emit("msg", { 29 | chan: chan.id, 30 | msg: msg, 31 | }); 32 | } 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/command-line/remove.js: -------------------------------------------------------------------------------- 1 | var ClientManager = new require("../clientManager"); 2 | var fs = require("fs"); 3 | var program = require("commander"); 4 | var Helper = require("../helper"); 5 | 6 | program 7 | .command("remove ") 8 | .description("Remove an existing user") 9 | .action(function(name) { 10 | try { 11 | var path = Helper.HOME + "/users"; 12 | var test = path + "/.test"; 13 | fs.mkdirSync(test); 14 | fs.rmdirSync(test); 15 | } catch (e) { 16 | console.log(""); 17 | console.log("You have no permissions to delete from " + path); 18 | console.log("Try running the command as sudo."); 19 | console.log(""); 20 | return; 21 | } 22 | var manager = new ClientManager(); 23 | if (manager.removeUser(name)) { 24 | console.log(""); 25 | console.log("Removed '" + name + "'."); 26 | console.log(""); 27 | } else { 28 | console.log(""); 29 | console.log("User '" + name + "' doesn't exist."); 30 | console.log(""); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /test/plugins/link.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | 3 | var util = require("../util"); 4 | var link = require("../../src/plugins/irc-events/link.js"); 5 | 6 | describe("Link plugin", function() { 7 | before(function(done) { 8 | this.app = util.createWebserver(); 9 | this.connection = this.app.listen(9002, done); 10 | }); 11 | 12 | after(function(done) { 13 | this.connection.close(done); 14 | }); 15 | 16 | beforeEach(function() { 17 | this.irc = util.createClient(); 18 | this.network = util.createNetwork(); 19 | }); 20 | 21 | it("should be able to fetch basic information about URLs", function(done) { 22 | link.call(this.irc, this.irc, this.network); 23 | 24 | this.app.get("/basic", function(req, res) { 25 | res.send("test"); 26 | }); 27 | 28 | this.irc.createMessage({ 29 | message: "http://localhost:9002/basic" 30 | }); 31 | 32 | this.irc.once("toggle", function(data) { 33 | assert.equal(data.head, "test"); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /client/views/chat.tpl: -------------------------------------------------------------------------------- 1 | {{#each channels}} 2 |
3 |
4 | 5 | 6 |
7 | 18 |
19 | {{name}} 20 | {{{parse topic}}} 21 |
22 |
23 |
24 | 27 |
28 |
29 |
30 | 35 |
36 | {{/each}} 37 | -------------------------------------------------------------------------------- /src/command-line/index.js: -------------------------------------------------------------------------------- 1 | var program = require("commander"); 2 | var pkg = require("../../package.json"); 3 | var fs = require("fs"); 4 | var mkdirp = require("mkdirp"); 5 | var Helper = require("../helper"); 6 | 7 | program.version(pkg.version, "-v, --version"); 8 | program.option(""); 9 | program.option(" --home " , "home path"); 10 | 11 | require("./start"); 12 | require("./config"); 13 | require("./list"); 14 | require("./add"); 15 | require("./remove"); 16 | require("./reset"); 17 | require("./edit"); 18 | 19 | var argv = program.parseOptions(process.argv); 20 | if (program.home) { 21 | Helper.HOME = program.home; 22 | } 23 | 24 | var config = Helper.HOME + "/config.js"; 25 | if (!fs.existsSync(config)) { 26 | mkdirp.sync(Helper.HOME); 27 | fs.writeFileSync( 28 | config, 29 | fs.readFileSync(__dirname + "/../../defaults/config.js") 30 | ); 31 | console.log("Config created:"); 32 | console.log(config); 33 | } 34 | 35 | program.parse(argv.args); 36 | 37 | if (!program.args.length) { 38 | program.parse(process.argv.concat("start")); 39 | } 40 | -------------------------------------------------------------------------------- /src/plugins/irc-events/join.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Chan = require("../../models/chan"); 3 | var Msg = require("../../models/msg"); 4 | var User = require("../../models/user"); 5 | 6 | module.exports = function(irc, network) { 7 | var client = this; 8 | irc.on("join", function(data) { 9 | var chan = _.find(network.channels, {name: data.channel}); 10 | if (typeof chan === "undefined") { 11 | chan = new Chan({ 12 | name: data.channel 13 | }); 14 | network.channels.push(chan); 15 | client.save(); 16 | client.emit("join", { 17 | network: network.id, 18 | chan: chan 19 | }); 20 | } 21 | var users = chan.users; 22 | users.push(new User({name: data.nick})); 23 | chan.sortUsers(); 24 | client.emit("users", { 25 | chan: chan.id, 26 | users: users 27 | }); 28 | var self = false; 29 | if (data.nick.toLowerCase() === irc.me.toLowerCase()) { 30 | self = true; 31 | } 32 | var msg = new Msg({ 33 | from: data.nick, 34 | type: Msg.Type.JOIN, 35 | self: self 36 | }); 37 | chan.messages.push(msg); 38 | client.emit("msg", { 39 | chan: chan.id, 40 | msg: msg 41 | }); 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/identd.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var net = require("net"); 3 | 4 | var users = {}; 5 | 6 | module.exports.start = function(port) { 7 | net.createServer(init).listen(port || 113); 8 | }; 9 | 10 | module.exports.hook = function(stream, user) { 11 | var id = ""; 12 | var socket = stream.socket || stream; 13 | socket.on("connect", function() { 14 | var ports = _.pick(socket, "localPort", "remotePort"); 15 | id = _.values(ports).join(", "); 16 | users[id] = user; 17 | }); 18 | socket.on("close", function() { 19 | delete users[id]; 20 | }); 21 | }; 22 | 23 | function init(socket) { 24 | socket.on("data", function(data) { 25 | respond(socket, data); 26 | }); 27 | } 28 | 29 | function respond(socket, data) { 30 | var id = parse(data); 31 | var response = id + " : "; 32 | if (users[id]) { 33 | response += "USERID : UNIX : " + users[id]; 34 | } else { 35 | response += "ERROR : NO-USER"; 36 | } 37 | response += "\r\n"; 38 | socket.write(response); 39 | socket.end(); 40 | } 41 | 42 | function parse(data) { 43 | data = data.toString(); 44 | data = data.split(","); 45 | return parseInt(data[0]) + ", " + parseInt(data[1]); 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/plugins/irc-events/part.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("part", function(data) { 7 | var chan = _.findWhere(network.channels, {name: data.channels[0]}); 8 | if (typeof chan === "undefined") { 9 | return; 10 | } 11 | var from = data.nick; 12 | if (from === irc.me) { 13 | network.channels = _.without(network.channels, chan); 14 | client.save(); 15 | client.emit("part", { 16 | chan: chan.id 17 | }); 18 | } else { 19 | var user = _.findWhere(chan.users, {name: from}); 20 | chan.users = _.without(chan.users, user); 21 | client.emit("users", { 22 | chan: chan.id, 23 | users: chan.users 24 | }); 25 | var reason = data.message || ""; 26 | if (reason.length > 0) { 27 | reason = "(" + reason + ")"; 28 | } 29 | var msg = new Msg({ 30 | type: Msg.Type.PART, 31 | mode: chan.getMode(from), 32 | text: reason, 33 | from: from 34 | }); 35 | chan.messages.push(msg); 36 | client.emit("msg", { 37 | chan: chan.id, 38 | msg: msg 39 | }); 40 | } 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Mattias Erming and contributors 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/command-line/reset.js: -------------------------------------------------------------------------------- 1 | var bcrypt = require("bcrypt-nodejs"); 2 | var ClientManager = new require("../clientManager"); 3 | var fs = require("fs"); 4 | var program = require("commander"); 5 | var Helper = require("../helper"); 6 | 7 | program 8 | .command("reset ") 9 | .description("Reset user password") 10 | .action(function(name) { 11 | var users = new ClientManager().getUsers(); 12 | if (users.indexOf(name) === -1) { 13 | console.log(""); 14 | console.log("User '" + name + "' doesn't exist."); 15 | console.log(""); 16 | return; 17 | } 18 | var file = Helper.HOME + "/users/" + name + ".json"; 19 | var user = require(file); 20 | require("read")({ 21 | prompt: "Password: ", 22 | silent: true 23 | }, function(err, password) { 24 | console.log(""); 25 | if (err) { 26 | return; 27 | } 28 | var salt = bcrypt.genSaltSync(8); 29 | var hash = bcrypt.hashSync(password, salt); 30 | user.password = hash; 31 | fs.writeFileSync( 32 | file, 33 | JSON.stringify(user, null, " "), 34 | {mode: "0777"} 35 | ); 36 | console.log("Successfully reset password for '" + name + "'."); 37 | console.log(""); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /src/command-line/start.js: -------------------------------------------------------------------------------- 1 | var ClientManager = new require("../clientManager"); 2 | var program = require("commander"); 3 | var shout = require("../server"); 4 | var Helper = require("../helper"); 5 | 6 | program 7 | .option("-H, --host " , "host") 8 | .option("-P, --port " , "port") 9 | .option("-B, --bind " , "bind") 10 | .option(" --public" , "mode") 11 | .option(" --private" , "mode") 12 | .command("start") 13 | .description("Start the server") 14 | .action(function() { 15 | var users = new ClientManager().getUsers(); 16 | var config = Helper.getConfig(); 17 | var mode = config.public; 18 | if (program.public) { 19 | mode = true; 20 | } else if (program.private) { 21 | mode = false; 22 | } 23 | if (!mode && !users.length) { 24 | console.log(""); 25 | console.log("No users found!"); 26 | console.log("Create a new user with 'shout add '."); 27 | console.log(""); 28 | } else { 29 | shout({ 30 | host: program.host || process.env.IP || config.host, 31 | port: program.port || process.env.PORT || config.port, 32 | bind: program.bind || config.bind, 33 | public: mode 34 | }); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /src/plugins/irc-events/whois.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Chan = require("../../models/chan"); 3 | var Msg = require("../../models/msg"); 4 | 5 | module.exports = function(irc, network) { 6 | var client = this; 7 | irc.on("whois", function(err, data) { 8 | if (data === null) { 9 | return; 10 | } 11 | var chan = _.findWhere(network.channels, {name: data.nickname}); 12 | if (typeof chan === "undefined") { 13 | chan = new Chan({ 14 | type: Chan.Type.QUERY, 15 | name: data.nickname 16 | }); 17 | network.channels.push(chan); 18 | client.emit("join", { 19 | network: network.id, 20 | chan: chan 21 | }); 22 | } 23 | var prefix = { 24 | hostname: "from", 25 | realname: "is", 26 | channels: "on", 27 | server: "using" 28 | }; 29 | for (var k in data) { 30 | var key = prefix[k]; 31 | if (!key || data[k].toString() === "") { 32 | continue; 33 | } 34 | var msg = new Msg({ 35 | type: Msg.Type.WHOIS, 36 | from: data.nickname, 37 | text: key + " " + data[k] 38 | }); 39 | chan.messages.push(msg); 40 | client.emit("msg", { 41 | chan: chan.id, 42 | msg: msg 43 | }); 44 | } 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /src/models/chan.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | 3 | module.exports = Chan; 4 | 5 | Chan.Type = { 6 | CHANNEL: "channel", 7 | LOBBY: "lobby", 8 | QUERY: "query" 9 | }; 10 | 11 | var id = 0; 12 | 13 | function Chan(attr) { 14 | _.merge(this, _.extend({ 15 | id: id++, 16 | messages: [], 17 | name: "", 18 | topic: "", 19 | type: Chan.Type.CHANNEL, 20 | unread: 0, 21 | users: [] 22 | }, attr)); 23 | } 24 | 25 | Chan.prototype.sortUsers = function() { 26 | this.users = _.sortBy( 27 | this.users, 28 | function(u) { return u.name.toLowerCase(); } 29 | ); 30 | var modes = [ 31 | "~", 32 | "&", 33 | "@", 34 | "%", 35 | "+", 36 | ].reverse(); 37 | modes.forEach(function(mode) { 38 | this.users = _.remove( 39 | this.users, 40 | function(u) { return u.mode === mode; } 41 | ).concat(this.users); 42 | }, this); 43 | }; 44 | 45 | Chan.prototype.getMode = function(name) { 46 | var user = _.find(this.users, {name: name}); 47 | if (user) { 48 | return user.mode; 49 | } else { 50 | return ""; 51 | } 52 | }; 53 | 54 | Chan.prototype.toJSON = function() { 55 | var clone = _.clone(this); 56 | clone.messages = clone.messages.slice(-100); 57 | return clone; 58 | }; 59 | -------------------------------------------------------------------------------- /src/plugins/irc-events/kick.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("kick", function(data) { 7 | var from = data.nick; 8 | var chan = _.findWhere(network.channels, {name: data.channel}); 9 | var mode = chan.getMode(from); 10 | 11 | if (typeof chan === "undefined") { 12 | return; 13 | } 14 | 15 | if (data.client === irc.me) { 16 | chan.users = []; 17 | } else { 18 | chan.users = _.without(chan.users, _.findWhere(chan.users, {name: data.client})); 19 | } 20 | 21 | client.emit("users", { 22 | chan: chan.id, 23 | users: chan.users 24 | }); 25 | 26 | var self = false; 27 | if (data.nick.toLowerCase() === irc.me.toLowerCase()) { 28 | self = true; 29 | } 30 | var reason = data.message || ""; 31 | if (reason.length > 0) { 32 | reason = " (" + reason + ")"; 33 | } 34 | var msg = new Msg({ 35 | type: Msg.Type.KICK, 36 | mode: mode, 37 | from: from, 38 | text: data.client + reason, 39 | self: self 40 | }); 41 | chan.messages.push(msg); 42 | client.emit("msg", { 43 | chan: chan.id, 44 | msg: msg 45 | }); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/plugins/irc-events/nick.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Msg = require("../../models/msg"); 3 | 4 | module.exports = function(irc, network) { 5 | var client = this; 6 | irc.on("nick", function(data) { 7 | var self = false; 8 | var nick = data["new"]; 9 | if (nick === irc.me) { 10 | var lobby = network.channels[0]; 11 | var msg = new Msg({ 12 | text: "You're now known as " + nick, 13 | }); 14 | lobby.messages.push(msg); 15 | client.emit("msg", { 16 | chan: lobby.id, 17 | msg: msg 18 | }); 19 | self = true; 20 | client.save(); 21 | client.emit("nick", { 22 | network: network.id, 23 | nick: nick 24 | }); 25 | } 26 | network.channels.forEach(function(chan) { 27 | var user = _.findWhere(chan.users, {name: data.nick}); 28 | if (typeof user === "undefined") { 29 | return; 30 | } 31 | user.name = nick; 32 | chan.sortUsers(); 33 | client.emit("users", { 34 | chan: chan.id, 35 | users: chan.users 36 | }); 37 | var msg = new Msg({ 38 | type: Msg.Type.NICK, 39 | from: data.nick, 40 | text: nick, 41 | self: self 42 | }); 43 | chan.messages.push(msg); 44 | client.emit("msg", { 45 | chan: chan.id, 46 | msg: msg 47 | }); 48 | }); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shout", 3 | "description": "The self-hosted Web IRC client", 4 | "version": "0.53.0", 5 | "author": "Mattias Erming", 6 | "preferGlobal": true, 7 | "bin": { 8 | "shout": "index.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/erming/shout.git" 13 | }, 14 | "scripts": { 15 | "start": "node index", 16 | "build": "grunt", 17 | "test": "HOME=test/fixtures mocha test/**/*.js && npm run lint", 18 | "lint": "eslint index.js Gruntfile.js src/ test/ client/ defaults/" 19 | }, 20 | "keywords": [ 21 | "browser", 22 | "web", 23 | "chat", 24 | "client", 25 | "irc", 26 | "server" 27 | ], 28 | "license": "MIT", 29 | "dependencies": { 30 | "bcrypt-nodejs": "0.0.3", 31 | "cheerio": "^0.17.0", 32 | "commander": "^2.3.0", 33 | "event-stream": "^3.1.7", 34 | "express": "^4.9.5", 35 | "lodash": "~2.4.1", 36 | "mkdirp": "^0.5.0", 37 | "moment": "~2.7.0", 38 | "read": "^1.0.5", 39 | "request": "^2.51.0", 40 | "slate-irc": "~0.8.1", 41 | "socket.io": "~1.0.6" 42 | }, 43 | "devDependencies": { 44 | "eslint": "^1.5.1", 45 | "grunt": "~0.4.5", 46 | "grunt-cli": "^0.1.13", 47 | "grunt-contrib-uglify": "~0.5.0", 48 | "grunt-contrib-watch": "^0.6.1", 49 | "handlebars": "^2.0.0", 50 | "mocha": "~2.0.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | var fs = require("fs"); 2 | var mkdirp = require("mkdirp"); 3 | var moment = require("moment"); 4 | var Helper = require("./helper"); 5 | 6 | module.exports.write = function(user, network, chan, msg) { 7 | try { 8 | var path = Helper.HOME + "/logs/" + user + "/" + network; 9 | mkdirp.sync(path); 10 | } catch (e) { 11 | console.log(e); 12 | return; 13 | } 14 | 15 | var config = Helper.getConfig(); 16 | var format = (config.logs || {}).format || "YYYY-MM-DD HH:mm:ss"; 17 | var tz = (config.logs || {}).timezone || "UTC+00:00"; 18 | 19 | var time = moment().zone(tz).format(format); 20 | var line = "[" + time + "] "; 21 | 22 | var type = msg.type.trim(); 23 | if (type === "message" || type === "highlight") { 24 | // Format: 25 | // [2014-01-01 00:00:00] Put that cookie down.. Now!! 26 | line += "<" + msg.from + "> " + msg.text; 27 | } else { 28 | // Format: 29 | // [2014-01-01 00:00:00] * Arnold quit 30 | line += "* " + msg.from + " " + msg.type; 31 | if (msg.text) { 32 | line += " " + msg.text; 33 | } 34 | } 35 | 36 | fs.appendFile( 37 | // Quick fix to escape pre-escape channel names that contain % using %%, 38 | // and / using %. **This does not escape all reserved words** 39 | path + "/" + chan.replace(/%/g, "%%").replace(/\//g, "%") + ".log", 40 | line + "\n", 41 | function(e) { 42 | if (e) { 43 | console.log("Log#write():\n" + e); 44 | } 45 | } 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /client/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/img/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/models/network.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Chan = require("./chan"); 3 | 4 | module.exports = Network; 5 | 6 | var id = 0; 7 | 8 | function Network(attr) { 9 | _.merge(this, _.extend({ 10 | name: "", 11 | host: "", 12 | port: 6667, 13 | tls: false, 14 | password: "", 15 | commands: [], 16 | username: "", 17 | realname: "", 18 | channels: [], 19 | connected: false, 20 | id: id++, 21 | irc: null, 22 | }, attr)); 23 | this.name = attr.name || prettify(attr.host); 24 | this.channels.unshift( 25 | new Chan({ 26 | name: this.name, 27 | type: Chan.Type.LOBBY 28 | }) 29 | ); 30 | } 31 | 32 | Network.prototype.toJSON = function() { 33 | var json = _.extend(this, {nick: (this.irc || {}).me || ""}); 34 | return _.omit(json, "irc", "password"); 35 | }; 36 | 37 | Network.prototype.export = function() { 38 | var network = _.pick(this, [ 39 | "name", 40 | "host", 41 | "port", 42 | "tls", 43 | "password", 44 | "username", 45 | "realname", 46 | "commands" 47 | ]); 48 | network.nick = (this.irc || {}).me; 49 | network.join = _.pluck( 50 | _.where(this.channels, {type: "channel"}), 51 | "name" 52 | ).join(","); 53 | return network; 54 | }; 55 | 56 | function prettify(host) { 57 | var name = capitalize(host.split(".")[1]); 58 | if (!name) { 59 | name = host; 60 | } 61 | return name; 62 | } 63 | 64 | function capitalize(str) { 65 | if (typeof str === "string") { 66 | return str.charAt(0).toUpperCase() + str.slice(1); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/js/libs/jquery/stickyscroll.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * stickyscroll 3 | * https://github.com/erming/stickyscroll 4 | * v2.2.0 5 | */ 6 | (function($) { 7 | $.fn.sticky = function() { 8 | if (this.size() > 1) { 9 | return this.each(function() { 10 | $(this).sticky(options); 11 | }); 12 | } 13 | 14 | var isBottom = false; 15 | var self = this; 16 | 17 | this.unbind(".sticky"); 18 | this.on("beforeAppend.sticky", function() { 19 | isBottom = isScrollBottom.call(self); 20 | }); 21 | 22 | this.on("afterAppend.sticky", function() { 23 | if (isBottom) { 24 | self.scrollBottom(); 25 | } 26 | }); 27 | 28 | var overflow = this.css("overflow-y"); 29 | if (overflow == "visible") { 30 | overflow = "auto"; 31 | } 32 | this.css({ 33 | "overflow-y": overflow 34 | }); 35 | 36 | $(window).unbind(".sticky"); 37 | $(window).on("resize.sticky", function() { 38 | self.scrollBottom(); 39 | }); 40 | 41 | this.scrollBottom(); 42 | return this; 43 | }; 44 | 45 | $.fn.scrollBottom = function() { 46 | return this.each(function() { 47 | $(this).animate({scrollTop: this.scrollHeight}, 0); 48 | }); 49 | }; 50 | 51 | $.fn.isScrollBottom = isScrollBottom; 52 | 53 | function isScrollBottom() { 54 | if ((this.scrollTop() + this.outerHeight() + 1) >= this.prop("scrollHeight")) { 55 | return true; 56 | } else { 57 | return false; 58 | } 59 | }; 60 | 61 | var append = $.fn.append; 62 | $.fn.append = function() { 63 | this.trigger("beforeAppend"); 64 | append.apply(this, arguments).trigger("afterAppend") 65 | return this; 66 | }; 67 | })(jQuery); 68 | -------------------------------------------------------------------------------- /client/js/libs/jquery/inputhistory.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * inputhistory 3 | * https://github.com/erming/inputhistory 4 | * v0.3.1 5 | */ 6 | (function($) { 7 | $.inputhistory = {}; 8 | $.inputhistory.defaultOptions = { 9 | history: [], 10 | preventSubmit: false 11 | }; 12 | 13 | $.fn.history = // Alias 14 | $.fn.inputhistory = function(options) { 15 | options = $.extend( 16 | $.inputhistory.defaultOptions, 17 | options 18 | ); 19 | 20 | var self = this; 21 | if (self.size() > 1) { 22 | return self.each(function() { 23 | $(this).history(options); 24 | }); 25 | } 26 | 27 | var history = options.history; 28 | history.push(""); 29 | 30 | var i = 0; 31 | self.on("keydown", function(e) { 32 | var key = e.which; 33 | switch (key) { 34 | case 13: // Enter 35 | if (self.val() != "") { 36 | i = history.length; 37 | history[i - 1] = self.val(); 38 | history.push(""); 39 | if (history[i - 1] == history[i - 2]) { 40 | history.splice(-2, 1); 41 | i--; 42 | } 43 | } 44 | if (!options.preventSubmit) { 45 | self.parents("form").eq(0).submit(); 46 | } 47 | self.val(""); 48 | break; 49 | 50 | case 38: // Up 51 | case 40: // Down 52 | // NOTICE: This is specific to the Shout client. 53 | if (e.ctrlKey || e.metaKey) { 54 | break; 55 | } 56 | history[i] = self.val(); 57 | if (key == 38 && i != 0) { 58 | i--; 59 | } else if (key == 40 && i < history.length - 1) { 60 | i++; 61 | } 62 | self.val(history[i]); 63 | break; 64 | 65 | default: 66 | return; 67 | } 68 | 69 | e.preventDefault(); 70 | }); 71 | 72 | return this; 73 | } 74 | })(jQuery); 75 | -------------------------------------------------------------------------------- /src/command-line/add.js: -------------------------------------------------------------------------------- 1 | var ClientManager = new require("../clientManager"); 2 | var bcrypt = require("bcrypt-nodejs"); 3 | var fs = require("fs"); 4 | var program = require("commander"); 5 | var mkdirp = require("mkdirp"); 6 | var Helper = require("../helper"); 7 | 8 | program 9 | .command("add ") 10 | .description("Add a new user") 11 | .action(function(name/* , password */) { 12 | var path = Helper.HOME + "/users"; 13 | try { 14 | mkdirp.sync(path); 15 | } catch (e) { 16 | console.log(""); 17 | console.log("Could not create " + path); 18 | console.log("Try running the command as sudo."); 19 | console.log(""); 20 | return; 21 | } 22 | try { 23 | var test = path + "/.test"; 24 | fs.mkdirSync(test); 25 | fs.rmdirSync(test); 26 | } catch (e) { 27 | console.log(""); 28 | console.log("You have no permissions to write to " + path); 29 | console.log("Try running the command as sudo."); 30 | console.log(""); 31 | return; 32 | } 33 | var manager = new ClientManager(); 34 | var users = manager.getUsers(); 35 | if (users.indexOf(name) !== -1) { 36 | console.log(""); 37 | console.log("User '" + name + "' already exists."); 38 | console.log(""); 39 | return; 40 | } 41 | require("read")({ 42 | prompt: "Password: ", 43 | silent: true 44 | }, function(err, password) { 45 | if (!err) add(manager, name, password); 46 | }); 47 | }); 48 | 49 | function add(manager, name, password) { 50 | console.log(""); 51 | var salt = bcrypt.genSaltSync(8); 52 | var hash = bcrypt.hashSync(password, salt); 53 | manager.addUser( 54 | name, 55 | hash 56 | ); 57 | console.log("User '" + name + "' created:"); 58 | console.log(Helper.HOME + "/users/" + name + ".json"); 59 | console.log(""); 60 | } 61 | -------------------------------------------------------------------------------- /src/plugins/irc-events/message.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var Chan = require("../../models/chan"); 3 | var Msg = require("../../models/msg"); 4 | 5 | module.exports = function(irc, network) { 6 | var client = this; 7 | irc.on("message", function(data) { 8 | if (data.message.indexOf("\u0001") === 0 && data.message.substring(0, 7) !== "\u0001ACTION") { 9 | // Hide ctcp messages. 10 | return; 11 | } 12 | 13 | var target = data.to; 14 | if (target.toLowerCase() === irc.me.toLowerCase()) { 15 | target = data.from; 16 | } 17 | 18 | var chan = _.findWhere(network.channels, {name: target}); 19 | if (typeof chan === "undefined") { 20 | chan = new Chan({ 21 | type: Chan.Type.QUERY, 22 | name: data.from 23 | }); 24 | network.channels.push(chan); 25 | client.emit("join", { 26 | network: network.id, 27 | chan: chan 28 | }); 29 | } 30 | 31 | var type = ""; 32 | var text = data.message; 33 | if (text.split(" ")[0] === "\u0001ACTION") { 34 | type = Msg.Type.ACTION; 35 | text = text.replace(/^\u0001ACTION|\u0001$/g, ""); 36 | } 37 | 38 | text.split(" ").forEach(function(w) { 39 | if (w.replace(/^@/, "").toLowerCase().indexOf(irc.me.toLowerCase()) === 0) type += " highlight"; 40 | }); 41 | 42 | var self = false; 43 | if (data.from.toLowerCase() === irc.me.toLowerCase()) { 44 | self = true; 45 | } 46 | 47 | if (chan.id !== client.activeChannel) { 48 | chan.unread++; 49 | } 50 | 51 | var name = data.from; 52 | var msg = new Msg({ 53 | type: type || Msg.Type.MESSAGE, 54 | mode: chan.getMode(name), 55 | from: name, 56 | text: text, 57 | self: self 58 | }); 59 | chan.messages.push(msg); 60 | client.emit("msg", { 61 | chan: chan.id, 62 | msg: msg 63 | }); 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /client/js/libs/stringcolor.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * stringcolor 3 | * Generate a consistent color from any string. 4 | * 5 | * Source: 6 | * https://github.com/erming/stringcolor 7 | * 8 | * Version 0.2.0 9 | */ 10 | (function($) { 11 | /** 12 | * Generate hex color code from a string. 13 | * 14 | * @param {String} string 15 | */ 16 | $.stringcolor = function(string) { 17 | return "#" + stringcolor(string); 18 | }; 19 | 20 | /** 21 | * Set one or more CSS properties for the set of matched elements. 22 | * 23 | * @param {String|Array} property 24 | * @param {String} string 25 | */ 26 | $.fn.stringcolor = function(property, string) { 27 | if (!property || !string) { 28 | throw new Error("$(selector).string_to_color() takes 2 arguments"); 29 | } 30 | return this.each(function() { 31 | var props = [].concat(property); 32 | var $this = $(this); 33 | $.map(props, function(p) { 34 | $this.css(p, $.stringcolor(string)); 35 | }); 36 | }); 37 | }; 38 | })(jQuery); 39 | 40 | /*! 41 | * Name: string_to_color 42 | * Author: Brandon Corbin [code@icorbin.com] 43 | * Website: http://icorbin.com 44 | */ 45 | function string_to_color(str) { 46 | // Generate a Hash for the String 47 | var hash = function(word) { 48 | var h = 0; 49 | for (var i = 0; i < word.length; i++) { 50 | h = word.charCodeAt(i) + ((h << 5) - h); 51 | } 52 | return h; 53 | }; 54 | 55 | // Change the darkness or lightness 56 | var shade = function(color, prc) { 57 | var num = parseInt(color, 16), 58 | amt = Math.round(2.55 * prc), 59 | R = (num >> 16) + amt, 60 | G = (num >> 8 & 0x00FF) + amt, 61 | B = (num & 0x0000FF) + amt; 62 | return (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 + 63 | (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 + 64 | (B < 255 ? B < 1 ? 0 : B : 255)) 65 | .toString(16) 66 | .slice(1); 67 | }; 68 | 69 | // Convert init to an RGBA 70 | var int_to_rgba = function(i) { 71 | var color = ((i >> 24) & 0xFF).toString(16) + 72 | ((i >> 16) & 0xFF).toString(16) + 73 | ((i >> 8) & 0xFF).toString(16) + 74 | (i & 0xFF).toString(16); 75 | return color; 76 | }; 77 | 78 | return shade( 79 | int_to_rgba(hash(str)), 80 | -10 81 | ); 82 | } 83 | 84 | var cache = {}; 85 | function stringcolor(str) { 86 | return cache[str] = cache[str] || string_to_color(str); 87 | } 88 | -------------------------------------------------------------------------------- /client/themes/crypto.css: -------------------------------------------------------------------------------- 1 | /* 2 | Crypto theme for Shout. 3 | 4 | Author: Aynik 5 | GitHub: https://github.com/aynik 6 | */ 7 | 8 | @font-face { 9 | font-family: Inconsolata-g; 10 | src: url("fonts/inconsolatag.woff") format("woff"), url("fonts/inconsolatag.ttf") format("ttf"); 11 | } 12 | 13 | body { 14 | background: #000; 15 | font: 16px Inconsolata-g, monospace; 16 | } 17 | 18 | a, #chat a { 19 | color: #00FF0E; 20 | } 21 | 22 | a:hover, #chat a:hover { 23 | color: #3EFF48; 24 | } 25 | 26 | #windows .window h2 { 27 | color: #666; 28 | font: regular 14px Leto, sans-serif; 29 | border-bottom: none; 30 | } 31 | 32 | #main { 33 | right: 0px; 34 | bottom: 0px; 35 | top: 0px; 36 | border-radius: 0px; 37 | } 38 | 39 | .container { 40 | margin: 40px auto; 41 | } 42 | 43 | #sign-in label { 44 | font: 14px Lato, sans-serif; 45 | color: #666; 46 | } 47 | 48 | #sign-in label input { 49 | margin-top: 10px !important; 50 | font: 14px Inconsolata-g, monospace; 51 | } 52 | 53 | .btn { 54 | border-color: #00FF0E; 55 | color: #00FF0E; 56 | } 57 | 58 | .btn:disabled, .btn:hover { 59 | background: #00FF0E; 60 | } 61 | 62 | #windows .window:before, #windows .chan:before { 63 | content: none; 64 | } 65 | 66 | #settings .opt { 67 | line-height: 20px; 68 | font-size: 12px; 69 | } 70 | 71 | #sign-in .remember { 72 | font: 12px Inconsolata-g, monospace; 73 | line-height: 30px; 74 | } 75 | 76 | #sidebar .chan:first-child { 77 | color: #00FF0E; 78 | } 79 | 80 | #sidebar button, 81 | #sidebar .chan, 82 | #sidebar .sign-out, 83 | #chat .time, 84 | #chat .count:before, 85 | #sidebar .empty { 86 | color: #666; 87 | } 88 | 89 | #sidebar .active { 90 | color: #fff; 91 | } 92 | 93 | #chat, 94 | #windows .header { 95 | font: 12px Inconsolata-g, monospace; 96 | line-height: 1.8; 97 | } 98 | 99 | #chat .chat, 100 | #chat .sidebar { 101 | top: 48px; 102 | } 103 | 104 | #chat.no-colors .from button, #chat.no-colors .sidebar button { 105 | color: #000 !important; 106 | font-weight: bold 107 | } 108 | 109 | #form .input { 110 | font: 12px Inconsolata-g, monospace; 111 | } 112 | 113 | #footer .icon { 114 | color: #666; 115 | } 116 | 117 | @media (max-width: 768px) { 118 | #main { 119 | left: 0px; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/clientManager.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var fs = require("fs"); 3 | var Client = require("./client"); 4 | var mkdirp = require("mkdirp"); 5 | var Helper = require("./helper"); 6 | 7 | module.exports = ClientManager; 8 | 9 | function ClientManager() { 10 | this.clients = []; 11 | } 12 | 13 | ClientManager.prototype.findClient = function(name) { 14 | for (var i in this.clients) { 15 | var client = this.clients[i]; 16 | if (client.name === name) { 17 | return client; 18 | } 19 | } 20 | return false; 21 | }; 22 | 23 | ClientManager.prototype.loadUsers = function() { 24 | var users = this.getUsers(); 25 | for (var i in users) { 26 | this.loadUser(users[i]); 27 | } 28 | }; 29 | 30 | ClientManager.prototype.loadUser = function(name) { 31 | try { 32 | var json = fs.readFileSync( 33 | Helper.HOME + "/users/" + name + ".json", 34 | "utf-8" 35 | ); 36 | json = JSON.parse(json); 37 | } catch (e) { 38 | console.log(e); 39 | return; 40 | } 41 | if (!this.findClient(name)) { 42 | this.clients.push(new Client( 43 | this.sockets, 44 | name, 45 | json 46 | )); 47 | console.log( 48 | "User '" + name + "' loaded." 49 | ); 50 | } 51 | }; 52 | 53 | ClientManager.prototype.getUsers = function() { 54 | var users = []; 55 | var path = Helper.HOME + "/users"; 56 | mkdirp.sync(path); 57 | try { 58 | var files = fs.readdirSync(path); 59 | files.forEach(function(file) { 60 | if (file.indexOf(".json") !== -1) { 61 | users.push(file.replace(".json", "")); 62 | } 63 | }); 64 | } catch (e) { 65 | console.log(e); 66 | return; 67 | } 68 | return users; 69 | }; 70 | 71 | ClientManager.prototype.addUser = function(name, password) { 72 | var users = this.getUsers(); 73 | if (users.indexOf(name) !== -1) { 74 | return false; 75 | } 76 | try { 77 | var path = Helper.HOME + "/users"; 78 | var user = { 79 | user: name, 80 | password: password || "", 81 | log: false, 82 | networks: [] 83 | }; 84 | mkdirp.sync(path); 85 | fs.writeFileSync( 86 | path + "/" + name + ".json", 87 | JSON.stringify(user, null, " "), 88 | {mode: "0777"} 89 | ); 90 | } catch (e) { 91 | throw e; 92 | } 93 | return true; 94 | }; 95 | 96 | ClientManager.prototype.removeUser = function(name) { 97 | var users = this.getUsers(); 98 | if (users.indexOf(name) === -1) { 99 | return false; 100 | } 101 | try { 102 | var path = Helper.HOME + "/users/" + name + ".json"; 103 | fs.unlinkSync(path); 104 | } catch (e) { 105 | throw e; 106 | } 107 | return true; 108 | }; 109 | 110 | ClientManager.prototype.autoload = function(/* sockets */) { 111 | var self = this; 112 | setInterval(function() { 113 | var loaded = _.pluck( 114 | self.clients, 115 | "name" 116 | ); 117 | var added = _.difference(self.getUsers(), loaded); 118 | _.each(added, function(name) { 119 | self.loadUser(name); 120 | }); 121 | var removed = _.difference(loaded, self.getUsers()); 122 | _.each(removed, function(name) { 123 | var client = _.find( 124 | self.clients, { 125 | name: name 126 | } 127 | ); 128 | if (client) { 129 | client.quit(); 130 | self.clients = _.without(self.clients, client); 131 | console.log( 132 | "User '" + name + "' disconnected." 133 | ); 134 | } 135 | }); 136 | }, 1000); 137 | }; 138 | 139 | -------------------------------------------------------------------------------- /client/js/libs/jquery/cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery Cookie Plugin v1.4.1 3 | * https://github.com/carhartl/jquery-cookie 4 | * 5 | * Copyright 2013 Klaus Hartl 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | // AMD 11 | define(['jquery'], factory); 12 | } else if (typeof exports === 'object') { 13 | // CommonJS 14 | factory(require('jquery')); 15 | } else { 16 | // Browser globals 17 | factory(jQuery); 18 | } 19 | }(function ($) { 20 | 21 | var pluses = /\+/g; 22 | 23 | function encode(s) { 24 | return config.raw ? s : encodeURIComponent(s); 25 | } 26 | 27 | function decode(s) { 28 | return config.raw ? s : decodeURIComponent(s); 29 | } 30 | 31 | function stringifyCookieValue(value) { 32 | return encode(config.json ? JSON.stringify(value) : String(value)); 33 | } 34 | 35 | function parseCookieValue(s) { 36 | if (s.indexOf('"') === 0) { 37 | // This is a quoted cookie as according to RFC2068, unescape... 38 | s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); 39 | } 40 | 41 | try { 42 | // Replace server-side written pluses with spaces. 43 | // If we can't decode the cookie, ignore it, it's unusable. 44 | // If we can't parse the cookie, ignore it, it's unusable. 45 | s = decodeURIComponent(s.replace(pluses, ' ')); 46 | return config.json ? JSON.parse(s) : s; 47 | } catch(e) {} 48 | } 49 | 50 | function read(s, converter) { 51 | var value = config.raw ? s : parseCookieValue(s); 52 | return $.isFunction(converter) ? converter(value) : value; 53 | } 54 | 55 | var config = $.cookie = function (key, value, options) { 56 | 57 | // Write 58 | 59 | if (value !== undefined && !$.isFunction(value)) { 60 | options = $.extend({}, config.defaults, options); 61 | 62 | if (typeof options.expires === 'number') { 63 | var days = options.expires, t = options.expires = new Date(); 64 | t.setTime(+t + days * 864e+5); 65 | } 66 | 67 | return (document.cookie = [ 68 | encode(key), '=', stringifyCookieValue(value), 69 | options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE 70 | options.path ? '; path=' + options.path : '', 71 | options.domain ? '; domain=' + options.domain : '', 72 | options.secure ? '; secure' : '' 73 | ].join('')); 74 | } 75 | 76 | // Read 77 | 78 | var result = key ? undefined : {}; 79 | 80 | // To prevent the for loop in the first place assign an empty array 81 | // in case there are no cookies at all. Also prevents odd result when 82 | // calling $.cookie(). 83 | var cookies = document.cookie ? document.cookie.split('; ') : []; 84 | 85 | for (var i = 0, l = cookies.length; i < l; i++) { 86 | var parts = cookies[i].split('='); 87 | var name = decode(parts.shift()); 88 | var cookie = parts.join('='); 89 | 90 | if (key && key === name) { 91 | // If second argument (value) is a function it's a converter... 92 | result = read(cookie, value); 93 | break; 94 | } 95 | 96 | // Prevent storing a cookie that we couldn't decode. 97 | if (!key && (cookie = read(cookie)) !== undefined) { 98 | result[name] = cookie; 99 | } 100 | } 101 | 102 | return result; 103 | }; 104 | 105 | config.defaults = {}; 106 | 107 | $.removeCookie = function (key, options) { 108 | if ($.cookie(key) === undefined) { 109 | return false; 110 | } 111 | 112 | // Must not alter options, thus extending a fresh object... 113 | $.cookie(key, '', $.extend({}, options, { expires: -1 })); 114 | return !$.cookie(key); 115 | }; 116 | 117 | })); 118 | -------------------------------------------------------------------------------- /client/themes/morning.css: -------------------------------------------------------------------------------- 1 | /* 2 | Morning theme for Shout. 3 | Has a bit more eye-friendly color scheme. 4 | 5 | Author: Riku Rouvila 6 | GitHub: https://github.com/rikukissa 7 | */ 8 | 9 | /* 10 | BACKGROUND #333c4a 11 | INPUT BACKGROUND #2e3642 12 | PRIMARY #fefefe 13 | SECONDARY #99a2b4 14 | BORDERS #2a323d 15 | QUIT #d0907d 16 | */ 17 | 18 | #main, 19 | #chat .sidebar, 20 | #windows .chan, 21 | #windows .window { 22 | background: #333c4a; 23 | } 24 | 25 | #main #chat, 26 | #main #form, 27 | #form .input, 28 | #chat, 29 | #windows .header { 30 | font-family: 'Open Sans', sans-serif !important; 31 | font-size: 13px; 32 | } 33 | 34 | #settings, #sign-in, #connect { 35 | color: #cccccc; 36 | } 37 | 38 | #chat .count { 39 | background-color: #2e3642; 40 | } 41 | 42 | #chat .search { 43 | color: #cccccc; 44 | padding: 15px 16px; 45 | } 46 | 47 | #chat .search::-webkit-input-placeholder { 48 | color: #99a2b4; 49 | opacity: 0.5; 50 | } 51 | 52 | /* Borders */ 53 | #chat .from, #windows .header, 54 | #chat .user-mode:before, 55 | #chat .sidebar { 56 | border-color: #2a323d; 57 | } 58 | 59 | 60 | /* Attach chat to window borders */ 61 | #windows .window:before, #windows .chan:before { 62 | display: none; 63 | } 64 | 65 | #footer { 66 | left: 0; 67 | bottom: 0; 68 | width: 220px; 69 | } 70 | 71 | #main { 72 | top: 0; 73 | bottom: 0; 74 | right: 0; 75 | border-radius: 0; 76 | } 77 | 78 | #chat .chat, #chat .sidebar { 79 | top: 48px; 80 | } 81 | 82 | /* User list */ 83 | #chat .user-mode { 84 | color: #fefefe; 85 | } 86 | 87 | /* Nicknames */ 88 | #chat.no-colors .from button, 89 | #chat.no-colors .sidebar button { 90 | color: #b0bacf !important; 91 | } 92 | 93 | #chat.no-colors .from button:hover, 94 | #chat.no-colors .sidebar button:hover { 95 | color: #fefefe !important; 96 | } 97 | 98 | #chat a { 99 | color: #428bca; 100 | } 101 | 102 | #chat button:hover { 103 | opacity: 1; 104 | } 105 | 106 | /* Message form */ 107 | #form { 108 | background: #2a323d; 109 | border-color: #242a33; 110 | } 111 | 112 | #form #input { 113 | background-color: #2e3642; 114 | border-color: #242a33; 115 | color: #cccccc; 116 | } 117 | 118 | #form #nick { 119 | background: #242a33; 120 | color: #CCC; 121 | } 122 | 123 | /* Buttons */ 124 | #chat .show-more-button, 125 | #form #submit, 126 | #windows .header .button { 127 | background: #2e3642; 128 | border-color: #242a33; 129 | color: #CCC; 130 | } 131 | 132 | #chat .show-more-button:hover, 133 | #form #submit:hover, 134 | #windows .header .button:hover { 135 | color: #FFF; 136 | } 137 | 138 | 139 | #chat .header { 140 | color: #99a2b4; 141 | } 142 | 143 | /* Setup text colors */ 144 | #chat .msg { 145 | color: #f3f3f3; 146 | } 147 | #chat .message { 148 | color: #fefefe; 149 | } 150 | 151 | #chat .self .text { 152 | color: #99a2b4; 153 | } 154 | 155 | #chat .error, 156 | #chat .error .from, 157 | #chat .highlight, 158 | #chat .highlight .from { 159 | color: #f92772; 160 | } 161 | 162 | #chat .highlight:not(.self) { 163 | font-size: 1.2em; 164 | } 165 | 166 | #chat .msg.quit .time, 167 | #chat .msg.quit .from button { 168 | color: #d0907d !important; 169 | } 170 | 171 | #chat .msg.topic { 172 | color: #fefefe; 173 | } 174 | 175 | #chat .msg.join .time, 176 | #chat .msg.join .from button { 177 | color: #84ce88 !important; 178 | } 179 | /* Embeds */ 180 | #chat .toggle-content, 181 | #chat .toggle-button { 182 | background: #242a33; 183 | color: #f3f3f3; 184 | } 185 | #chat .toggle-content img { 186 | float: left; 187 | margin-right: 0.5em; 188 | } 189 | 190 | #chat .toggle-content .body { 191 | color: #99a2b4; 192 | } 193 | 194 | @media (max-width: 768px) { 195 | #main { 196 | left: 0; 197 | } 198 | #footer { 199 | left: -220px; 200 | width: 225px; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/plugins/irc-events/link.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var cheerio = require("cheerio"); 3 | var Msg = require("../../models/msg"); 4 | var request = require("request"); 5 | var Helper = require("../../helper"); 6 | var es = require("event-stream"); 7 | 8 | process.setMaxListeners(0); 9 | 10 | module.exports = function(irc, network) { 11 | var client = this; 12 | irc.on("message", function(data) { 13 | var config = Helper.getConfig(); 14 | if (!config.prefetch) { 15 | return; 16 | } 17 | 18 | var links = []; 19 | var split = data.message.split(" "); 20 | _.each(split, function(w) { 21 | var match = w.indexOf("http://") === 0 || w.indexOf("https://") === 0; 22 | if (match) { 23 | links.push(w); 24 | } 25 | }); 26 | 27 | if (links.length === 0) { 28 | return; 29 | } 30 | 31 | var self = data.to.toLowerCase() === irc.me.toLowerCase(); 32 | var chan = _.findWhere(network.channels, {name: self ? data.from : data.to}); 33 | if (typeof chan === "undefined") { 34 | return; 35 | } 36 | 37 | var msg = new Msg({ 38 | type: Msg.Type.TOGGLE, 39 | time: "" 40 | }); 41 | chan.messages.push(msg); 42 | client.emit("msg", { 43 | chan: chan.id, 44 | msg: msg 45 | }); 46 | 47 | var link = links[0]; 48 | fetch(link, function(res) { 49 | parse(msg, link, res, client); 50 | }); 51 | }); 52 | }; 53 | 54 | function parse(msg, url, res, client) { 55 | var config = Helper.getConfig(); 56 | var toggle = msg.toggle = { 57 | id: msg.id, 58 | type: "", 59 | head: "", 60 | body: "", 61 | thumb: "", 62 | link: url 63 | }; 64 | 65 | if (!config.prefetchMaxImageSize) { 66 | config.prefetchMaxImageSize = 512; 67 | } 68 | switch (res.type) { 69 | case "text/html": 70 | var $ = cheerio.load(res.text); 71 | toggle.type = "link"; 72 | toggle.head = $("title").text(); 73 | toggle.body = 74 | $("meta[name=description]").attr("content") 75 | || $("meta[property=\"og:description\"]").attr("content") 76 | || "No description found."; 77 | toggle.thumb = 78 | $("meta[property=\"og:image\"]").attr("content") 79 | || $("meta[name=\"twitter:image:src\"]").attr("content") 80 | || ""; 81 | break; 82 | 83 | case "image/png": 84 | case "image/gif": 85 | case "image/jpg": 86 | case "image/jpeg": 87 | if (res.size < (config.prefetchMaxImageSize * 1024)) { 88 | toggle.type = "image"; 89 | } 90 | else { 91 | return; 92 | } 93 | break; 94 | 95 | default: 96 | return; 97 | } 98 | 99 | client.emit("toggle", toggle); 100 | } 101 | 102 | function fetch(url, cb) { 103 | try { 104 | var req = request.get({ 105 | url: url, 106 | headers: { 107 | "User-Agent": "Mozilla/5.0 (compatible; Shout IRC Client; +https://github.com/erming/shout)" 108 | } 109 | }); 110 | } catch (e) { 111 | return; 112 | } 113 | var length = 0; 114 | var limit = 1024 * 10; 115 | req 116 | .on("response", function(res) { 117 | if (!(/(text\/html|application\/json)/.test(res.headers["content-type"]))) { 118 | res.req.abort(); 119 | } 120 | }) 121 | .on("error", function() {}) 122 | .pipe(es.map(function(data, next) { 123 | length += data.length; 124 | if (length > limit) { 125 | req.response.req.abort(); 126 | } 127 | next(null, data); 128 | })) 129 | .pipe(es.wait(function(err, data) { 130 | if (err) return; 131 | var body; 132 | var type; 133 | var size = req.response.headers["content-length"]; 134 | try { 135 | body = JSON.parse(data); 136 | } catch (e) { 137 | body = {}; 138 | } 139 | try { 140 | type = req.response.headers["content-type"].split(/ *; */).shift(); 141 | } catch (e) { 142 | type = {}; 143 | } 144 | data = { 145 | text: data, 146 | body: body, 147 | type: type, 148 | size: size 149 | }; 150 | cb(data); 151 | })); 152 | } 153 | -------------------------------------------------------------------------------- /client/js/libs/handlebars/parse.js: -------------------------------------------------------------------------------- 1 | Handlebars.registerHelper( 2 | "parse", function(text) { 3 | var wrap = wraplong(text); 4 | text = escape(text); 5 | text = colors(text); 6 | text = uri(text); 7 | if (wrap) { 8 | return "" + text + ""; 9 | } else { 10 | return text; 11 | } 12 | } 13 | ); 14 | 15 | function wraplong(text) { 16 | var wrap = false; 17 | var split = text.split(" "); 18 | for (var i in split) { 19 | if (split[i].length > 40) { 20 | wrap = true; 21 | } 22 | } 23 | return wrap; 24 | } 25 | 26 | function escape(text) { 27 | var e = { 28 | "<": "<", 29 | ">": ">", 30 | "'": "'" 31 | }; 32 | return text.replace(/[<>']/g, function (c) { 33 | return e[c]; 34 | }); 35 | } 36 | 37 | function uri(text) { 38 | return URI.withinString(text, function(url, start, end, source) { 39 | if (url.indexOf("javascript:") === 0) { 40 | return url; 41 | } 42 | var split = url.split("<"); 43 | url = "" + split[0] + ""; 44 | if (split.length > 1) { 45 | url += "<" + split.slice(1).join("<"); 46 | } 47 | return url; 48 | }); 49 | } 50 | 51 | 52 | /** 53 | * MIRC compliant colour and style parser 54 | * Unfortuanately this is a non trivial operation 55 | * See this branch for source and tests 56 | * https://github.com/megawac/irc-style-parser/tree/shout 57 | */ 58 | var styleCheck_Re = /[\x00-\x1F]/, 59 | back_re = /^([0-9]{1,2})(,([0-9]{1,2}))?/, 60 | colourKey = "\x03", 61 | // breaks all open styles ^O (\x0F) 62 | styleBreak = "\x0F"; 63 | 64 | 65 | function styleTemplate(settings) { 66 | return "" + settings.text + ""; 67 | } 68 | 69 | var styles = [ 70 | ["normal", "\x00", ""], ["underline", "\x1F"], 71 | ["bold", "\x02"], ["italic", "\x1D"] 72 | ].map(function(style) { 73 | var escaped = encodeURI(style[1]).replace("%", "\\x"); 74 | return { 75 | name: style[0], 76 | style: style[2] != null ? style[2] : "irc-" + style[0], 77 | key: style[1], 78 | keyregex: new RegExp(escaped + "(.*?)(" + escaped + "|$)") 79 | }; 80 | }); 81 | 82 | function colors(line) { 83 | // http://www.mirc.com/colors.html 84 | // http://www.aviran.org/stripremove-irc-client-control-characters/ 85 | // https://github.com/perl6/mu/blob/master/examples/rules/Grammar-IRC.pm 86 | // regexs are cruel to parse this thing 87 | 88 | // already done? 89 | if (!styleCheck_Re.test(line)) return line; 90 | 91 | // split up by the irc style break character ^O 92 | if (line.indexOf(styleBreak) >= 0) { 93 | return line.split(styleBreak).map(colors).join(""); 94 | } 95 | 96 | var result = line; 97 | var parseArr = result.split(colourKey); 98 | var text, match, colour, background = ""; 99 | for (var i = 0; i < parseArr.length; i++) { 100 | text = parseArr[i]; 101 | match = text.match(back_re); 102 | if (!match) { 103 | // ^C (no colour) ending. Escape current colour and carry on 104 | background = ""; 105 | continue; 106 | } 107 | colour = "irc-fg" + +match[1]; 108 | // set the background colour 109 | if (match[3]) { 110 | background = " irc-bg" + +match[3]; 111 | } 112 | // update the parsed text result 113 | result = result.replace(colourKey + text, styleTemplate({ 114 | style: colour + background, 115 | text: text.slice(match[0].length) 116 | })); 117 | } 118 | 119 | // Matching styles (italics/bold/underline) 120 | // if only colours were this easy... 121 | styles.forEach(function(style) { 122 | if (result.indexOf(style.key) < 0) return; 123 | result = result.replace(style.keyregex, function(match, text) { 124 | return styleTemplate({ 125 | "style": style.style, 126 | "text": text 127 | }); 128 | }); 129 | }); 130 | 131 | return result; 132 | } 133 | -------------------------------------------------------------------------------- /client/themes/zenburn.css: -------------------------------------------------------------------------------- 1 | /* 2 | Zenburn theme for Shout. 3 | Based on the Morning Theme by Riku Rouvila 4 | 5 | Author: JP Smith 6 | GitHub: https://github.com/japesinator 7 | */ 8 | 9 | /* 10 | BACKGROUND #3f3f3f 11 | INPUT BACKGROUND #434443 12 | PRIMARY #dcdccc 13 | SECONDARY #d2d39b 14 | BORDERS #333333 15 | QUIT #bc6c4c 16 | */ 17 | 18 | body { 19 | background: #2b2b2b; 20 | } 21 | 22 | #main, 23 | #chat .sidebar, 24 | #windows .chan, 25 | #windows .window { 26 | background: #3f3f3f; 27 | } 28 | 29 | #main #chat, 30 | #main #form, 31 | #form .input, 32 | #chat, 33 | #windows .header { 34 | font-family: 'Open Sans', sans-serif !important; 35 | font-size: 13px; 36 | } 37 | 38 | #settings, #sign-in, #connect { 39 | color: #dcdccc; 40 | } 41 | 42 | #settings, #sign-in, #connect .title { 43 | color: #88b090; 44 | } 45 | 46 | #settings, #sign-in, #connect .opt { 47 | color: #dcdccc; 48 | } 49 | 50 | #sidebar { 51 | background: #2b2b2b; 52 | bottom: 48px; 53 | } 54 | 55 | #footer { 56 | background: #33332f; 57 | border-top: 1px solid #000; 58 | } 59 | 60 | #chat .count { 61 | background-color: #434443; 62 | } 63 | 64 | #chat .search { 65 | color: #88b090; 66 | padding: 15px 16px; 67 | } 68 | 69 | #chat .search::-webkit-input-placeholder { 70 | color: #d2d39b; 71 | opacity: 0.5; 72 | } 73 | 74 | /* Borders */ 75 | #chat .from, #windows .header, 76 | #chat .user-mode:before, 77 | #chat .sidebar { 78 | border-color: #333333; 79 | } 80 | 81 | /* Attach chat to window borders */ 82 | #windows .window:before, #windows .chan:before { 83 | display: none; 84 | } 85 | 86 | #footer { 87 | left: 0; 88 | bottom: 0; 89 | width: 220px; 90 | } 91 | 92 | #main { 93 | top: 0; 94 | bottom: 0; 95 | right: 0; 96 | border-radius: 0; 97 | } 98 | 99 | #chat .chat, #chat .sidebar { 100 | top: 48px; 101 | } 102 | 103 | /* User list */ 104 | #chat .user-mode { 105 | color: #dcdccc; 106 | } 107 | 108 | /* Nicknames */ 109 | #chat.no-colors .from button, 110 | #chat.no-colors .sidebar button { 111 | color: #bc8cbc !important; 112 | } 113 | 114 | #chat.no-colors .from button:hover, 115 | #chat.no-colors .sidebar button:hover { 116 | color: #dcdccc !important; 117 | } 118 | 119 | #chat a { 120 | color: #8c8cbc; 121 | } 122 | 123 | #chat button:hover { 124 | opacity: 1; 125 | } 126 | 127 | /* Message form */ 128 | #form { 129 | background: #333333; 130 | border-color: #101010; 131 | } 132 | 133 | #form #input { 134 | background-color: #434443; 135 | border-color: #101010; 136 | color: #dcdccc; 137 | } 138 | 139 | #form #nick { 140 | background: #101010; 141 | color: #dcdccc; 142 | } 143 | 144 | /* Buttons */ 145 | #chat .show-more-button, 146 | #form #submit, 147 | #windows .header .button { 148 | background: #434443; 149 | border-color: #101010; 150 | color: #dcdccc; 151 | } 152 | 153 | #chat .show-more-button:hover, 154 | #form #submit:hover, 155 | #windows .header .button:hover { 156 | color: #FFF; 157 | } 158 | 159 | 160 | #chat .header { 161 | color: #d2d39b; 162 | } 163 | 164 | /* Setup text colors */ 165 | #chat .msg { 166 | color: #ffcfaf; 167 | } 168 | #chat .message { 169 | color: #dcdccc; 170 | } 171 | 172 | #chat .self .text { 173 | color: #d2d39b; 174 | } 175 | 176 | #chat .error, 177 | #chat .error .from, 178 | #chat .highlight, 179 | #chat .highlight .from { 180 | color: #bc6c4c; 181 | } 182 | 183 | #chat .msg.quit .time, 184 | #chat .msg.quit .from button { 185 | color: #bc6c9c !important; 186 | } 187 | 188 | #chat .msg.topic { 189 | color: #dcdccc; 190 | } 191 | 192 | #chat .msg.join .time, 193 | #chat .msg.join .from button { 194 | color: #8cd0d3 !important; 195 | } 196 | /* Embeds */ 197 | #chat .toggle-content, 198 | #chat .toggle-button { 199 | background: #93b3a3; 200 | color: #dcdccc; 201 | } 202 | #chat .toggle-content img { 203 | float: left; 204 | margin-right: 0.5em; 205 | } 206 | 207 | #chat .toggle-content .body { 208 | color: #d2d39b; 209 | } 210 | 211 | @media (max-width: 768px) { 212 | #main { 213 | left: 0; 214 | } 215 | #footer { 216 | left: -220px; 217 | width: 225px; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | var _ = require("lodash"); 2 | var bcrypt = require("bcrypt-nodejs"); 3 | var Client = require("./client"); 4 | var ClientManager = require("./clientManager"); 5 | var express = require("express"); 6 | var fs = require("fs"); 7 | var io = require("socket.io"); 8 | var Helper = require("./helper"); 9 | var config = {}; 10 | 11 | var sockets = null; 12 | var manager = new ClientManager(); 13 | 14 | module.exports = function(options) { 15 | config = Helper.getConfig(); 16 | config = _.extend(config, options); 17 | 18 | var app = express() 19 | .use(index) 20 | .use(express.static("client")); 21 | 22 | app.enable("trust proxy"); 23 | 24 | var server = null; 25 | var https = config.https || {}; 26 | var protocol = https.enable ? "https" : "http"; 27 | var port = config.port; 28 | var host = config.host; 29 | var transports = config.transports || ["websocket", "polling"]; 30 | 31 | if (!https.enable){ 32 | server = require("http"); 33 | server = server.createServer(app).listen(port, host); 34 | } else { 35 | server = require("https"); 36 | server = server.createServer({ 37 | key: fs.readFileSync(https.key), 38 | cert: fs.readFileSync(https.certificate) 39 | }, app).listen(port, host); 40 | } 41 | 42 | if ((config.identd || {}).enable) { 43 | require("./identd").start(config.identd.port); 44 | } 45 | 46 | sockets = io(server, { 47 | transports: transports 48 | }); 49 | 50 | sockets.on("connect", function(socket) { 51 | if (config.public) { 52 | auth.call(socket); 53 | } else { 54 | init(socket); 55 | } 56 | }); 57 | 58 | manager.sockets = sockets; 59 | 60 | console.log(""); 61 | console.log("Shout is now running on " + protocol + "://" + config.host + ":" + config.port + "/"); 62 | console.log("Press ctrl-c to stop"); 63 | console.log(""); 64 | 65 | if (!config.public) { 66 | manager.loadUsers(); 67 | if (config.autoload) { 68 | manager.autoload(); 69 | } 70 | } 71 | }; 72 | 73 | function index(req, res, next) { 74 | if (req.url.split("?")[0] !== "/") return next(); 75 | return fs.readFile("client/index.html", "utf-8", function(err, file) { 76 | var data = _.merge( 77 | require("../package.json"), 78 | config 79 | ); 80 | res.setHeader("Content-Type", "text/html"); 81 | res.writeHead(200); 82 | res.end(_.template( 83 | file, 84 | data 85 | )); 86 | }); 87 | } 88 | 89 | function init(socket, client, token) { 90 | if (!client) { 91 | socket.emit("auth"); 92 | socket.on("auth", auth); 93 | } else { 94 | socket.on( 95 | "input", 96 | function(data) { 97 | client.input(data); 98 | } 99 | ); 100 | socket.on( 101 | "more", 102 | function(data) { 103 | client.more(data); 104 | } 105 | ); 106 | socket.on( 107 | "conn", 108 | function(data) { 109 | client.connect(data); 110 | } 111 | ); 112 | socket.on( 113 | "open", 114 | function(data) { 115 | client.open(data); 116 | } 117 | ); 118 | socket.on( 119 | "sort", 120 | function(data) { 121 | client.sort(data); 122 | } 123 | ); 124 | socket.join(client.id); 125 | socket.emit("init", { 126 | active: client.activeChannel, 127 | networks: client.networks, 128 | token: token || "" 129 | }); 130 | } 131 | } 132 | 133 | function auth(data) { 134 | var socket = this; 135 | if (config.public) { 136 | var client = new Client(sockets); 137 | manager.clients.push(client); 138 | socket.on("disconnect", function() { 139 | manager.clients = _.without(manager.clients, client); 140 | client.quit(); 141 | }); 142 | init(socket, client); 143 | } else { 144 | var success = false; 145 | _.each(manager.clients, function(client) { 146 | if (data.token) { 147 | if (data.token === client.token) { 148 | success = true; 149 | } 150 | } else if (client.config.user === data.user) { 151 | if (bcrypt.compareSync(data.password || "", client.config.password)) { 152 | success = true; 153 | } 154 | } 155 | if (success) { 156 | var token; 157 | if (data.remember || data.token) { 158 | token = client.token; 159 | } 160 | init(socket, client, token); 161 | return false; 162 | } 163 | }); 164 | if (!success) { 165 | socket.emit("auth"); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /client/css/fonts/Lato-700/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /client/css/fonts/Lato-regular/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2014 by tyPoland Lukasz Dziedzic (team@latofonts.com) with Reserved Font Name "Lato" 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Welcome to the Shout community, it's great to have you here! We thank you in 4 | advance for your contributions. 5 | 6 | ### I have a question 7 | 8 | Find us on the #shout-irc channel. You might not get an answer right away, but 9 | this channel is filled with nice people who will be happy to help you. 10 | 11 | ### I want to report a bug 12 | 13 | First of all, look at the 14 | [open issues](https://github.com/erming/shout/issues) and [closed 15 | issues](https://github.com/erming/shout/issues?q=is%3Aissue+is%3Aclosed) 16 | to see if this was not alredy discussed before. 17 | 18 | ### I want to contribute to the code 19 | 20 | A good starting point if you want to help us but do not have a clear idea of 21 | what you can do specifically is to 22 | look at the open issues labeled as [*quick and 23 | easy*](https://github.com/erming/shout/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3A%22quick+and+easy%22) 24 | or [*help 25 | wanted*](https://github.com/erming/shout/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3A%22help+wanted%22). 26 | 27 | When you submit some code, make sure it respects the overall coding style that 28 | is currently in place. If you do not, our reviewers will surely let you know you 29 | should :smile: (that is, until an automated checker takes over the yelling). 30 | 31 | Also, make sure that your PRs do not contain unnecessary commits. If you think 32 | some of your commits should be merged into a single one, feel free to [squash 33 | them](https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History). 34 | 35 | Please [rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing) outdated 36 | PRs on master to help with the reviews (rebasing is preferred over merging to 37 | keep a clean history in a branch/PR). 38 | 39 | Additionally, give extra care to your commit messages, as they will help us 40 | review your PRs as well as help other contributors in the future, when exploring 41 | the history. The general rules are to [use the imperative present 42 | tense](https://git-scm.com/book/ch5-2.html#Commit-Guidelines), to start with a 43 | single concise line, followed by a blank line and a more detailed explanation 44 | when necessary. Tim Pope wrote an [excellent 45 | article](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 46 | on how one should format their commit messages. 47 | 48 | When you send a PR, expect two different reviews from the [project 49 | maintainers](https://github.com/erming/shout/blob/master/CONTRIBUTING.md#project-maintainers). 50 | If necessary, they will make comments and ask for changes. When everything looks 51 | good to them, they will both express their consent by commenting your PR with a 52 | :+1:. Typically, the first reviewer will give a thorough report and exchange 53 | with you, give his :+1:, then ask the second reviewer to confirm the changes. 54 | When this happens (when you get your second required :+1:), then your PR can be 55 | merged. 56 | 57 | Please document any relevant changes in the shout documentation that can be 58 | found [in its own repository](https://github.com/erming/shout-website). 59 | 60 | ### Labels 61 | 62 | When you open an [issue](https://github.com/erming/shout/issues) or send us a 63 | [PR](https://github.com/erming/shout/pulls), it will most likely be given one or 64 | several labels. Here is what they mean: 65 | 66 | - **bug**: Issues that report and PRs that solve any defects that cause 67 | unexpected behaviors. 68 | - **documentation**: Tickets that mention a lack of documentation, suggest their 69 | improvement, or PRs that address these. 70 | - **duplicate**: Tickets already solved in the past or already open. Such 71 | tickets should always link to the previous one on the subject. 72 | - **enhancement**: Tickets that describe a desired feature or PRs that add them 73 | to the project. 74 | - **help wanted**: Tickets that we would like the community to help us with, by 75 | either answering questions or send us PRs. 76 | - **priority**: Tickets that the core team deemed critical and PRs that the core 77 | team should look at before others. 78 | - **question**: Tickets that are actually support cases. 79 | - **quick and easy**: Tickets that should be fairly simple to implement, even 80 | for developers not yet involved in the project. 81 | - **second review needed**: A first reviewer gave his :+1: but now expects a 82 | second reviewer to step in before this PR can be merged. 83 | - **security**: Tickets that describe a security concern or PRs that must be 84 | reviewed with extra care regarding security. 85 | - **wontfix**: Tickets that, after discussion and explanation, will not be fixed 86 | or implemented. 87 | 88 | ### Project maintainers 89 | 90 | - [Mattias Erming](https://github.com/erming) (`erming` on IRC) 91 | - [Jocelyn Delalande](https://github.com/JocelynDelalande) (`JocelynD` on IRC) 92 | - [Jérémie Astori](https://github.com/astorije) (`astorije` on IRC) 93 | - [Paul Friederichsen](https://github.com/floogulinc) (`floogulinc` on IRC) 94 | -------------------------------------------------------------------------------- /defaults/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // 3 | // Set the server mode. 4 | // Public servers does not require authentication. 5 | // 6 | // Set to 'false' to enable users. 7 | // 8 | // @type boolean 9 | // @default false 10 | // 11 | public: true, 12 | 13 | // 14 | // Allow connections from this host. 15 | // 16 | // @type string 17 | // @default "0.0.0.0" 18 | // 19 | host: "0.0.0.0", 20 | 21 | // 22 | // Set the port to listen on. 23 | // 24 | // @type int 25 | // @default 9000 26 | // 27 | port: 9000, 28 | 29 | // 30 | // Set the local IP to bind to. 31 | // 32 | // @type string 33 | // @default "0.0.0.0" 34 | // 35 | bind: undefined, 36 | 37 | // 38 | // Set the default theme. 39 | // 40 | // @type string 41 | // @default "themes/example.css" 42 | // 43 | theme: "themes/example.css", 44 | 45 | // 46 | // Autoload users 47 | // 48 | // When this setting is enabled, your 'users/' folder will be monitored. This is useful 49 | // if you want to add/remove users while the server is running. 50 | // 51 | // @type boolean 52 | // @default true 53 | // 54 | autoload: true, 55 | 56 | // 57 | // Prefetch URLs 58 | // 59 | // If enabled, Shout will try to load thumbnails and site descriptions from 60 | // URLs posted in channels. 61 | // 62 | // @type boolean 63 | // @default true 64 | // 65 | prefetch: false, 66 | 67 | // 68 | // Prefetch URLs Image Preview size limit 69 | // 70 | // If prefetch is enabled, Shout will only display content under the maximum size. 71 | // Default value is 512 (in kB) 72 | // 73 | // @type int 74 | // @default 512 75 | // 76 | prefetchMaxImageSize: 512, 77 | 78 | // 79 | // Display network 80 | // 81 | // If set to false Shout will not expose network settings in login 82 | // form, limiting client to connect to the configured network. 83 | // 84 | // @type boolean 85 | // @default true 86 | // 87 | displayNetwork: true, 88 | 89 | // 90 | // Log settings 91 | // 92 | // Logging has to be enabled per user. If enabled, logs will be stored in 93 | // the '/users//logs/' folder. 94 | // 95 | // @type object 96 | // @default {} 97 | // 98 | logs: { 99 | // 100 | // Timestamp format 101 | // 102 | // @type string 103 | // @default "YYYY-MM-DD HH:mm:ss" 104 | // 105 | format: "YYYY-MM-DD HH:mm:ss", 106 | 107 | // 108 | // Timezone 109 | // 110 | // @type string 111 | // @default "UTC+00:00" 112 | // 113 | timezone: "UTC+00:00" 114 | }, 115 | 116 | // 117 | // Default values for the 'Connect' form. 118 | // 119 | // @type object 120 | // @default {} 121 | // 122 | defaults: { 123 | // 124 | // Name 125 | // 126 | // @type string 127 | // @default "Freenode" 128 | // 129 | name: "Freenode", 130 | 131 | // 132 | // Host 133 | // 134 | // @type string 135 | // @default "irc.freenode.org" 136 | // 137 | host: "irc.freenode.org", 138 | 139 | // 140 | // Port 141 | // 142 | // @type int 143 | // @default 6697 144 | // 145 | port: 6697, 146 | 147 | // 148 | // Password 149 | // 150 | // @type string 151 | // @default "" 152 | // 153 | password: "", 154 | 155 | // 156 | // Enable TLS/SSL 157 | // 158 | // @type boolean 159 | // @default true 160 | // 161 | tls: true, 162 | 163 | // 164 | // Nick 165 | // 166 | // @type string 167 | // @default "shout-user" 168 | // 169 | nick: "shout-user", 170 | 171 | // 172 | // Username 173 | // 174 | // @type string 175 | // @default "shout-user" 176 | // 177 | username: "shout-user", 178 | 179 | // 180 | // Real Name 181 | // 182 | // @type string 183 | // @default "Shout User" 184 | // 185 | realname: "Shout User", 186 | 187 | // 188 | // Channels 189 | // 190 | // @type string 191 | // @default "#foo, #shout-irc" 192 | // 193 | join: "#foo, #shout-irc" 194 | }, 195 | 196 | // 197 | // Set socket.io transports 198 | // 199 | // @type array 200 | // @default ["polling', "websocket"] 201 | // 202 | transports: ["polling", "websocket"], 203 | 204 | // 205 | // Run Shout with HTTPS support. 206 | // 207 | // @type object 208 | // @default {} 209 | // 210 | https: { 211 | // 212 | // Enable HTTPS support. 213 | // 214 | // @type boolean 215 | // @default false 216 | // 217 | enable: false, 218 | 219 | // 220 | // Path to the key. 221 | // 222 | // @type string 223 | // @example "sslcert/key.pem" 224 | // @default "" 225 | // 226 | key: "", 227 | 228 | // 229 | // Path to the certificate. 230 | // 231 | // @type string 232 | // @example "sslcert/key-cert.pem" 233 | // @default "" 234 | // 235 | certificate: "" 236 | }, 237 | 238 | // 239 | // Run Shout with identd support. 240 | // 241 | // @type object 242 | // @default {} 243 | // 244 | identd: { 245 | // 246 | // Run the identd daemon on server start. 247 | // 248 | // @type boolean 249 | // @default false 250 | // 251 | enable: false, 252 | 253 | // 254 | // Port to listen for ident requests. 255 | // 256 | // @type int 257 | // @default 113 258 | // 259 | port: 113 260 | } 261 | }; 262 | -------------------------------------------------------------------------------- /client/js/libs/notification.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Notification JS 3 | * Shims up the Notification API 4 | * 5 | * @author Andrew Dodson 6 | * @website http://adodson.com/notification.js/ 7 | */ 8 | 9 | // 10 | // Does the browser support the the Notification API? 11 | // .. and does it have a permission property? 12 | // 13 | 14 | (function(window, document){ 15 | 16 | var PERMISSION_GRANTED = 'granted', 17 | PERMISSION_DENIED = 'denied', 18 | PERMISSION_UNKNOWN = 'unknown'; 19 | 20 | var a = [], iv, i=0; 21 | 22 | // 23 | // Swap the document.title with the notification 24 | // 25 | function swaptitle(title){ 26 | 27 | if(a.length===0){ 28 | a = [document.title]; 29 | } 30 | 31 | a.push(title); 32 | 33 | if(!iv){ 34 | iv = setInterval(function(){ 35 | 36 | // has document.title changed externally? 37 | if(a.indexOf(document.title) === -1 ){ 38 | // update the default title 39 | a[0] = document.title; 40 | } 41 | 42 | document.title = a[++i%a.length]; 43 | }, 1000); 44 | } 45 | } 46 | 47 | function swapTitleCancel(){ 48 | 49 | // dont do any more if we haven't got anything open 50 | if(a.length===0){ 51 | return; 52 | } 53 | 54 | // if an IE overlay is present, kill it 55 | if("external" in window && "msSiteModeClearIconOverlay" in window.external ){ 56 | window.external.msSiteModeClearIconOverlay(); 57 | } 58 | 59 | clearInterval(iv); 60 | 61 | iv = false; 62 | document.title = a[0]; 63 | a = []; 64 | } 65 | 66 | // 67 | // Add aevent handlers 68 | function addEvent(el,name,func){ 69 | if(name.match(" ")){ 70 | var a = name.split(' '); 71 | for(var i=0;i