├── .gitignore ├── .gitmodules ├── README.md ├── UNLICENSE ├── data-default └── users │ └── content-default.json ├── dbschema.json ├── index.html └── js ├── ZeroChat.coffee ├── all.js ├── lib └── ZeroFrame.coffee └── utils ├── Text.coffee └── Time.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | # Hidden files 2 | .* 3 | !/.gitignore 4 | 5 | # Data dir 6 | data/* 7 | 8 | # ZeroNet content list 9 | content.json 10 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "frontend"] 2 | path = frontend 3 | url = git@github.com:xaxes/zeronet-relay-chat.git 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 0rc - 0net Relay Chat 2 | =================== 3 | 4 | 5 | An IRC-like internet chatroom hosted on [ZeroNet](https://github.com/hellozeronet/zeronet), a distributed network. 0rc is a fully distributed website, and messages are shared through peers using the ZeroNet stack. In addition, making a new chatroom website is a simple as downloading the files and cloning the site. 6 | 7 | ---------- 8 | 9 | 10 | Development Heading Forward 11 | ------------ 12 | In the issues of this repository are a list of things we plan to get done. Any help on them would be greatly appreciated. 13 | 14 | 15 | Installing / Hosting 0rc 16 | ------------- 17 | In order to start hosting 0rc, you must install ZeroNet. Instructions for doing that are available on its repository. However, once hosting the site once and getting enough peers, the site should forever be available through ZeroNet regardless of whether or not your node is running. 18 | 19 | There are two ways to start hosting 0rc. The first is visiting the 0rc dev chatroom (must have ZeroNet running) and cloning it directly through the ZeroHello homepage UI. The second includes downloading the files from GitHub and creating a new site through the ZeroNet command line. 20 | 21 | ### Using ZeroHello 22 | Make sure that ZeroNet is running and your client is able to contact other peers and visit sites. 23 | 24 | Visit the [0rc dev chatroom](http://127.0.0.1:43110/dev.0rc.bit) to cause ZeroNet to download the latest version of 0rc. This will download all of the needed files. 25 | 26 | Once the site is downloaded, head back to the ZeroNet homepage and scroll down on the site list until you find dev.0rc.bit, the site you just visited. Click on the ellipses on the right hand side of the site's entry, then click 'Clone'. This will create a new version of the chatroom entirely for you. 27 | 28 | All of the files for this site will be in `zeronet/data/[site address]/`, and you can edit them to change the site. However, after every edit, you must resign the site and publish it. More information is available on the [ZeroNet documentation](https://zeronet.readthedocs.org/en/latest/using_zeronet/create_new_site/#2-buildmodify-site). 29 | 30 | Editing the `content.json` file and changing the name, domain name, and description is recommended. 31 | 32 | ### Using git and a new, custom site 33 | This way is more complicated, but also allows easy integration with the latest versions of 0rc. In addition, it gives you more complete control over the site from the get go. 34 | 35 | Same as the previous method, make sure that ZeroNet is running and your client is able to contact other peers and visit sites. 36 | 37 | Follow the instructions on the ZeroNet documentation on how to [create a new site](https://zeronet.readthedocs.org/en/latest/using_zeronet/create_new_site/). 38 | 39 | > **Note:** 40 | > 41 | > - Make sure to save your private key, and also keep it safe. 42 | > - With this, anyone can sign a new version of your site and put it in place of yours. 43 | > - Without this, you will not be able to sign your site or make any changes to it. 44 | 45 | Next, download the files from this repository, either by using the [zip link](https://github.com/cgm616/0rc/archive/master.zip), or cloning it with git. Find the site folder, which will be in `zeronet/data/[site address]/`, and copy all of the files just downloaded or cloned into that directory. 46 | 47 | Next, edit the `content.json` file to have a personalized description and name of the site, and add a few lines to make 0rc work properly and not seed things it doesn't have to. Directly beneath: 48 | ``` 49 | "files": { 50 | "index.html": { 51 | "sha512": "f8557901b15e...e7d038f86c7b57", 52 | "size": 87836 53 | }, 54 | ... 55 | }, 56 | ``` 57 | add a new section with the following: 58 | ``` 59 | "ignore": ".git/|data/.*", 60 | "includes": { 61 | "data/users/content.json": { 62 | "signers": [], 63 | "signers_required": 1 64 | } 65 | }, 66 | ``` 67 | This will clean up the site and reduce the number of unneeded files seeded. 68 | 69 | Next, we have to establish a `data/` directory for user data. Rename `data-default/` to `data/`. Then, rename `data/content-default.json` to `data/content.json`. 70 | 71 | At this point, you should be all done. All that's left is to [sign and publish](https://zeronet.readthedocs.org/en/latest/using_zeronet/create_new_site/#2-buildmodify-site) your site, and then everyone on ZeroNet who has the address will be able to visit it. 72 | 73 | 74 | License for 0rc 75 | ------------- 76 | Due to the unique nature of ZeroNet and how it so tightly couples sharing site source code, and due to how this codebase evolved from the work of both nofish and meow, we chose to use the Unlicense for our work. Any contributions after commit cd03ba9 are under this license. 77 | 78 | The complete license can be found in the file `UNLICENSE` at the root of the repository. 79 | 80 | 81 | Contributors 82 | ------------- 83 | All of the contributors in this list with an @zeroid.bit name are referenced by their ZeroNet usernames. 84 | 85 | - nofish@zeroid.bit 86 | - meow@zeroid.bit 87 | - cgm616@zeroid.bit 88 | - hhes@zeroid.bit 89 | - whowaswho@zeroid.bit 90 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /data-default/users/content-default.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": {}, 3 | "ignore": ".*", 4 | "modified": 1437647722.815, 5 | "signs": { 6 | "1AvF5TpcaamRNtqvN1cnDEWzNmUtD47Npg": "G40E/+sEab6xpHg85q0VfNcJfn5LRBWUZoUR0ZAjSqLwpppSjFquUVbBoHQe5u+Wa1USioF5O8SqLkIdyP+O5PM=" 7 | }, 8 | "user_contents": { 9 | "cert_signers": { 10 | "zeroid.bit": [ "1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz" ] 11 | }, 12 | "permission_rules": { 13 | ".*": { 14 | "files_allowed": "data.json", 15 | "max_size": 100000 16 | }, 17 | "bitmsg/.*@zeroid.bit": { "max_size": 100000 } 18 | }, 19 | "permissions": { 20 | "bad@zeroid.bit": false, 21 | "cgm616@zeroid.bit": { "max_size": 100000 } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /dbschema.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "ZeroChat", 3 | "db_file": "data/zerochat.db", 4 | "version": 2, 5 | "maps": { 6 | "users/.+/data.json": { 7 | "to_table": [ "message" ] 8 | }, 9 | "users/.+/content.json": { 10 | "to_keyvalue": [ "cert_user_id" ] 11 | } 12 | }, 13 | "tables": { 14 | "message": { 15 | "cols": [ 16 | ["room", "TEXT"], 17 | ["type", "TEXT"], 18 | ["body", "TEXT"], 19 | ["date_added", "INTEGER"], 20 | ["json_id", "INTEGER REFERENCES json (json_id)"] 21 | ], 22 | "indexes": ["CREATE UNIQUE INDEX message_key ON message(json_id, date_added)"], 23 | "schema_changed": 1 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 25 | 26 | 27 | 28 | 29 | 40 |
41 | 42 | 43 |
44 | 45 |
46 |
47 | Select user: 48 | 0 Visitors 49 |
50 | 51 |
52 |
53 |
    54 |
  • Welcome to 0rc!
  • 55 |
56 |
57 |
58 |
  • null
59 |
60 |
61 | 62 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /js/ZeroChat.coffee: -------------------------------------------------------------------------------- 1 | class ZeroChat extends ZeroFrame 2 | init: -> 3 | @addLine "inited!" 4 | 5 | 6 | selectUser: => 7 | Page.cmd "certSelect", [["zeroid.bit"]] 8 | return false 9 | 10 | 11 | route: (cmd, message) -> 12 | if cmd == "setSiteInfo" 13 | if message.params.cert_user_id 14 | document.getElementById("select_user").innerHTML = message.params.cert_user_id 15 | else 16 | document.getElementById("select_user").innerHTML = "Select user" 17 | @site_info = message.params # Save site info data to allow access it later 18 | 19 | # Reload messages if new file arrives 20 | if message.params.event[0] == "file_done" 21 | @loadMessages() 22 | 23 | resetDebug: -> 24 | document.getElementById("list").innerHTML = "" 25 | 26 | debugMsg: (msg) -> 27 | newElement = document.createElement('li') 28 | newElement.innerHTML = msg 29 | document.getElementById("list").appendChild(newElement) 30 | 31 | resetInputField: => 32 | document.getElementById("message").disabled = false 33 | document.getElementById("message").value = "" # Reset the message input 34 | document.getElementById("message").focus() 35 | 36 | sendMessage: => 37 | if not Page.site_info.cert_user_id # No account selected, display error 38 | Page.cmd "wrapperNotification", ["info", "Please, select your account."] 39 | return false 40 | 41 | document.getElementById("message").disabled = true 42 | inner_path = "data/users/#{@site_info.auth_address}/data.json" # This is our data file 43 | 44 | # Load our current messages 45 | @cmd "fileGet", {"inner_path": inner_path, "required": false}, (data) => 46 | if data # Parse if already exits 47 | data = JSON.parse(data) 48 | else # Not exits yet, use default data 49 | data = { "message": [] } 50 | 51 | # // EMPTY MESSAGES 52 | msg = document.getElementById("message").value 53 | if msg == "" or msg == "/me" or msg == "/me " 54 | @debugMsg('empty message') 55 | @resetInputField() 56 | return false 57 | # Add the message to data 58 | data.message.push({ 59 | "body": document.getElementById("message").value, 60 | "date_added": (+new Date) 61 | }) 62 | 63 | # Encode data array to utf8 json text 64 | json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t'))) 65 | 66 | # Write file to disk 67 | @cmd "fileWrite", [inner_path, btoa(json_raw)], (res) => 68 | if res == "ok" 69 | # Publish the file to other users 70 | @cmd "sitePublish", {"inner_path": inner_path}, (res) => 71 | @resetInputField() 72 | @loadMessages() 73 | else 74 | @cmd "wrapperNotification", ["error", "File write error: #{res}"] 75 | document.getElementById("message").disabled = false 76 | 77 | return false 78 | 79 | 80 | replaceURLs: (body) -> 81 | # // REGEXES 82 | replacePattern0 = /(https?:\/\/(127.0.0.1|localhost):43110\/)/gi 83 | replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim 84 | replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim 85 | replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim 86 | replacePattern4 = /0net:\/\/([-a-zA-Z0-9+&.,:+_\/=?]*)/g 87 | replacePattern5 = /(([a-zA-Z0-9\-\_\.])+)\/\/0mail/gim 88 | 89 | # // url rewriting 127.0.0.1:43110 to 0net:// so other replacements don't break and localhost:43110 to 0net:// so they aren't scheme://ip:port dependent 90 | replacedText = body.replace(replacePattern0, '0net://') 91 | replacedText = replacedText.replace('@zeroid.bit', '//0mail') 92 | 93 | # // URLs starting with http://, https://, or ftp:// 94 | replacedText = replacedText.replace(replacePattern1, '$1') 95 | 96 | # // URLs starting with "www." (without // before it, or it'd re-link the ones done above). 97 | replacedText = replacedText.replace(replacePattern2, '$1$2') 98 | 99 | # // Change email addresses to mailto:: links. 100 | replacedText = replacedText.replace(replacePattern3, '$1') 101 | 102 | # // remove 0net:// prefix in URL href 103 | replacedText = replacedText.replace(replacePattern4, '0net://$1') 104 | 105 | # // rewrite 0net:// prefix in URL href and replace //0mail with @zeroid.bit 106 | replacedText = replacedText.replace(replacePattern5, '$1@zeroid.bit') 107 | 108 | return replacedText 109 | 110 | loadMessages: -> 111 | query = """ 112 | SELECT message.*, keyvalue.value AS cert_user_id FROM message 113 | LEFT JOIN json AS data_json USING (json_id) 114 | LEFT JOIN json AS content_json ON ( 115 | data_json.directory = content_json.directory AND content_json.file_name = 'content.json' 116 | ) 117 | LEFT JOIN keyvalue ON (keyvalue.key = 'cert_user_id' AND keyvalue.json_id = content_json.json_id) 118 | ORDER BY date_added 119 | """ 120 | @cmd "dbQuery", [query], (messages) => 121 | document.getElementById("messages").innerHTML = "" # Always start with empty messages 122 | message_lines = [] 123 | 124 | # // remove later 125 | @resetDebug() 126 | 127 | for message in messages 128 | body = message.body.replace(//g, ">") # Escape html tags in body 129 | added = new Date(message.date_added) 130 | 131 | # // OUR ADDITIONS 132 | time = Time.since(message.date_added / 1000) 133 | userid = message.cert_user_id 134 | useridcolor = Text.toColor(userid) 135 | username = message.cert_user_id.replace('@zeroid.bit', '') 136 | msgseparator = ":" 137 | 138 | # // REPLACE URLS 139 | body = @replaceURLs(body) 140 | 141 | # // REPLACE IRC 142 | if body.substr(0, 3) == "/me" 143 | action = body.replace("/me","") 144 | username = username+' '+action 145 | body = '' 146 | msgseparator = '' 147 | 148 | # // STYLE OUR MESSAGES AND MENTIONS 149 | prestyle="" 150 | poststyle = '' 151 | # our messages 152 | if userid == Page.site_info.cert_user_id 153 | prestyle = '' 154 | # our mentions 155 | if Page.site_info.cert_user_id and body.indexOf(Page.site_info.cert_user_id.replace('@zeroid.bit', '')) > -1 156 | prestyle = '' 157 | body = prestyle + body + poststyle 158 | 159 | message_lines.push "
  • #{time} #{username}#{msgseparator} #{body}
  • " 160 | message_lines.reverse() 161 | document.getElementById("messages").innerHTML = message_lines.join("\n") 162 | 163 | 164 | addLine: (line) -> 165 | messages = document.getElementById("messages") 166 | messages.innerHTML = "
  • #{line}
  • "+messages.innerHTML 167 | 168 | 169 | # Wrapper websocket connection ready 170 | onOpenWebsocket: (e) => 171 | @cmd "siteInfo", {}, (site_info) => 172 | document.getElementById("bigTitle").innerHTML = site_info.content.title + ' - ' + site_info.content.description 173 | document.getElementById("peerCount").innerHTML = site_info.peers + ' Visitors' 174 | 175 | # Update currently selected username 176 | if site_info.cert_user_id 177 | document.getElementById("select_user").innerHTML = site_info.cert_user_id 178 | @site_info = site_info # Save site info data to allow access it later 179 | @loadMessages() 180 | 181 | 182 | window.Page = new ZeroChat() 183 | -------------------------------------------------------------------------------- /js/all.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* ---- data/1AvF5TpcaamRNtqvN1cnDEWzNmUtD47Npg/js/lib/ZeroFrame.coffee ---- */ 4 | 5 | 6 | (function() { 7 | var ZeroFrame, 8 | __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 9 | __slice = [].slice; 10 | 11 | ZeroFrame = (function() { 12 | function ZeroFrame(url) { 13 | this.onCloseWebsocket = __bind(this.onCloseWebsocket, this); 14 | this.onOpenWebsocket = __bind(this.onOpenWebsocket, this); 15 | this.route = __bind(this.route, this); 16 | this.onMessage = __bind(this.onMessage, this); 17 | this.url = url; 18 | this.waiting_cb = {}; 19 | this.connect(); 20 | this.next_message_id = 1; 21 | this.init(); 22 | } 23 | 24 | ZeroFrame.prototype.init = function() { 25 | return this; 26 | }; 27 | 28 | ZeroFrame.prototype.connect = function() { 29 | this.target = window.parent; 30 | window.addEventListener("message", this.onMessage, false); 31 | return this.cmd("innerReady"); 32 | }; 33 | 34 | ZeroFrame.prototype.onMessage = function(e) { 35 | var cmd, message; 36 | message = e.data; 37 | cmd = message.cmd; 38 | if (cmd === "response") { 39 | if (this.waiting_cb[message.to] != null) { 40 | return this.waiting_cb[message.to](message.result); 41 | } else { 42 | return this.log("Websocket callback not found:", message); 43 | } 44 | } else if (cmd === "wrapperReady") { 45 | return this.cmd("innerReady"); 46 | } else if (cmd === "ping") { 47 | return this.response(message.id, "pong"); 48 | } else if (cmd === "wrapperOpenedWebsocket") { 49 | return this.onOpenWebsocket(); 50 | } else if (cmd === "wrapperClosedWebsocket") { 51 | return this.onCloseWebsocket(); 52 | } else { 53 | return this.route(cmd, message); 54 | } 55 | }; 56 | 57 | ZeroFrame.prototype.route = function(cmd, message) { 58 | return this.log("Unknown command", message); 59 | }; 60 | 61 | ZeroFrame.prototype.response = function(to, result) { 62 | return this.send({ 63 | "cmd": "response", 64 | "to": to, 65 | "result": result 66 | }); 67 | }; 68 | 69 | ZeroFrame.prototype.cmd = function(cmd, params, cb) { 70 | if (params == null) { 71 | params = {}; 72 | } 73 | if (cb == null) { 74 | cb = null; 75 | } 76 | return this.send({ 77 | "cmd": cmd, 78 | "params": params 79 | }, cb); 80 | }; 81 | 82 | ZeroFrame.prototype.send = function(message, cb) { 83 | if (cb == null) { 84 | cb = null; 85 | } 86 | message.id = this.next_message_id; 87 | this.next_message_id += 1; 88 | this.target.postMessage(message, "*"); 89 | if (cb) { 90 | return this.waiting_cb[message.id] = cb; 91 | } 92 | }; 93 | 94 | ZeroFrame.prototype.log = function() { 95 | var args; 96 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 97 | return console.log.apply(console, ["[ZeroFrame]"].concat(__slice.call(args))); 98 | }; 99 | 100 | ZeroFrame.prototype.onOpenWebsocket = function() { 101 | return this.log("Websocket open"); 102 | }; 103 | 104 | ZeroFrame.prototype.onCloseWebsocket = function() { 105 | return this.log("Websocket close"); 106 | }; 107 | 108 | return ZeroFrame; 109 | 110 | })(); 111 | 112 | window.ZeroFrame = ZeroFrame; 113 | 114 | }).call(this); 115 | 116 | 117 | 118 | /* ---- data/1AvF5TpcaamRNtqvN1cnDEWzNmUtD47Npg/js/utils/Text.coffee ---- */ 119 | 120 | 121 | (function() { 122 | var Text; 123 | 124 | Text = (function() { 125 | function Text() {} 126 | 127 | Text.prototype.toColor = function(text, saturation, lightness) { 128 | var hash, i, _i, _ref; 129 | if (saturation == null) { 130 | saturation = 30; 131 | } 132 | if (lightness == null) { 133 | lightness = 40; 134 | } 135 | hash = 0; 136 | for (i = _i = 0, _ref = text.length - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; i = 0 <= _ref ? ++_i : --_i) { 137 | hash += text.charCodeAt(i) * i; 138 | hash = hash % 1777; 139 | } 140 | return "hsl(" + (hash % 360) + ("," + saturation + "%," + lightness + "%)"); 141 | }; 142 | 143 | return Text; 144 | 145 | })(); 146 | 147 | window.Text = new Text(); 148 | 149 | }).call(this); 150 | 151 | 152 | 153 | /* ---- data/1AvF5TpcaamRNtqvN1cnDEWzNmUtD47Npg/js/utils/Time.coffee ---- */ 154 | 155 | 156 | (function() { 157 | var Time; 158 | 159 | Time = (function() { 160 | function Time() {} 161 | 162 | Time.prototype.since = function(time) { 163 | var back, now, secs; 164 | now = +(new Date) / 1000; 165 | secs = now - time; 166 | if (secs < 60) { 167 | back = "Just now"; 168 | } else if (secs < 60 * 60) { 169 | back = (Math.round(secs / 60)) + " mins ago"; 170 | } else if (secs < 60 * 60 * 24) { 171 | back = (Math.round(secs / 60 / 60)) + " hrs ago"; 172 | } else if (secs < 60 * 60 * 24 * 3) { 173 | back = (Math.round(secs / 60 / 60 / 24)) + " days ago"; 174 | } else { 175 | back = "on " + this.date(time); 176 | } 177 | back = back.replace(/1 ([a-z]+)s/, "1 $1"); 178 | return back; 179 | }; 180 | 181 | Time.prototype.date = function(timestamp, format) { 182 | var display, parts; 183 | if (format == null) { 184 | format = "short"; 185 | } 186 | parts = (new Date(timestamp * 1000)).toString().split(" "); 187 | if (format === "short") { 188 | display = parts.slice(1, 4); 189 | } else { 190 | display = parts.slice(1, 5); 191 | } 192 | return display.join(" ").replace(/( [0-9]{4})/, ",$1"); 193 | }; 194 | 195 | Time.prototype.timestamp = function(date) { 196 | if (date == null) { 197 | date = ""; 198 | } 199 | if (date === "now" || date === "") { 200 | return parseInt(+(new Date) / 1000); 201 | } else { 202 | return parseInt(Date.parse(date) / 1000); 203 | } 204 | }; 205 | 206 | return Time; 207 | 208 | })(); 209 | 210 | window.Time = new Time; 211 | 212 | }).call(this); 213 | 214 | 215 | 216 | /* ---- data/1AvF5TpcaamRNtqvN1cnDEWzNmUtD47Npg/js/ZeroChat.coffee ---- */ 217 | 218 | 219 | (function() { 220 | var ZeroChat, 221 | bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, 222 | extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, 223 | hasProp = {}.hasOwnProperty; 224 | 225 | ZeroChat = (function(superClass) { 226 | extend(ZeroChat, superClass); 227 | 228 | function ZeroChat() { 229 | this.onOpenWebsocket = bind(this.onOpenWebsocket, this); 230 | this.sendMessage = bind(this.sendMessage, this); 231 | this.resetInputField = bind(this.resetInputField, this); 232 | this.selectUser = bind(this.selectUser, this); 233 | return ZeroChat.__super__.constructor.apply(this, arguments); 234 | } 235 | 236 | ZeroChat.prototype.init = function() { 237 | return this.addLine("inited!"); 238 | }; 239 | 240 | ZeroChat.prototype.selectUser = function() { 241 | Page.cmd("certSelect", [["zeroid.bit"]]); 242 | return false; 243 | }; 244 | 245 | ZeroChat.prototype.route = function(cmd, message) { 246 | if (cmd === "setSiteInfo") { 247 | if (message.params.cert_user_id) { 248 | document.getElementById("select_user").innerHTML = message.params.cert_user_id; 249 | } else { 250 | document.getElementById("select_user").innerHTML = "Select user"; 251 | } 252 | this.site_info = message.params; 253 | if (message.params.event[0] === "file_done") { 254 | return this.loadMessages(); 255 | } 256 | } 257 | }; 258 | 259 | ZeroChat.prototype.resetDebug = function() { 260 | return document.getElementById("list").innerHTML = ""; 261 | }; 262 | 263 | ZeroChat.prototype.debugMsg = function(msg) { 264 | var newElement; 265 | newElement = document.createElement('li'); 266 | newElement.innerHTML = msg; 267 | return document.getElementById("list").appendChild(newElement); 268 | }; 269 | 270 | ZeroChat.prototype.resetInputField = function() { 271 | document.getElementById("message").disabled = false; 272 | document.getElementById("message").value = ""; 273 | return document.getElementById("message").focus(); 274 | }; 275 | 276 | ZeroChat.prototype.sendMessage = function() { 277 | var inner_path; 278 | if (!Page.site_info.cert_user_id) { 279 | Page.cmd("wrapperNotification", ["info", "Please, select your account."]); 280 | return false; 281 | } 282 | document.getElementById("message").disabled = true; 283 | inner_path = "data/users/" + this.site_info.auth_address + "/data.json"; 284 | this.cmd("fileGet", { 285 | "inner_path": inner_path, 286 | "required": false 287 | }, (function(_this) { 288 | return function(data) { 289 | var json_raw, msg; 290 | if (data) { 291 | data = JSON.parse(data); 292 | } else { 293 | data = { 294 | "message": [] 295 | }; 296 | } 297 | msg = document.getElementById("message").value; 298 | if (msg === "" || msg === "/me" || msg === "/me ") { 299 | _this.debugMsg('empty message'); 300 | _this.resetInputField(); 301 | return false; 302 | } 303 | data.message.push({ 304 | "body": document.getElementById("message").value, 305 | "date_added": +(new Date) 306 | }); 307 | json_raw = unescape(encodeURIComponent(JSON.stringify(data, void 0, '\t'))); 308 | return _this.cmd("fileWrite", [inner_path, btoa(json_raw)], function(res) { 309 | if (res === "ok") { 310 | return _this.cmd("sitePublish", { 311 | "inner_path": inner_path 312 | }, function(res) { 313 | _this.resetInputField(); 314 | return _this.loadMessages(); 315 | }); 316 | } else { 317 | _this.cmd("wrapperNotification", ["error", "File write error: " + res]); 318 | return document.getElementById("message").disabled = false; 319 | } 320 | }); 321 | }; 322 | })(this)); 323 | return false; 324 | }; 325 | 326 | ZeroChat.prototype.replaceURLs = function(body) { 327 | var inputText, replacePattern0, replacePattern1, replacePattern2, replacePattern3, replacePattern4, replacedText; 328 | replacePattern0 = /(http:\/\/127.0.0.1:43110\/)/gi; 329 | replacePattern1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; 330 | replacePattern2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim; 331 | replacePattern3 = /(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)/gim; 332 | replacePattern4 = /0net:\/\/([-a-zA-Z0-9+&@#+\/%?=~_|!:,.;]*)/g; 333 | replacePattern5 = /(([a-zA-Z0-9\-\_\.])+)\/\/0mail/gim; 334 | 335 | replacedText = body.replace(replacePattern0, '0net://'); 336 | replacedText = replacedText.replace('@zeroid.bit', '//0mail'); 337 | replacedText = replacedText.replace(replacePattern1, '$1'); 338 | replacedText = replacedText.replace(replacePattern2, '$1$2'); 339 | replacedText = replacedText.replace(replacePattern3, '$1'); 340 | replacedText = replacedText.replace(replacePattern4, '0net://$1'); 341 | replacedText = replacedText.replace(replacePattern5, '$1@zeroid.bit'); 342 | 343 | return replacedText; 344 | }; 345 | 346 | ZeroChat.prototype.loadMessages = function() { 347 | var query; 348 | query = "SELECT message.*, keyvalue.value AS cert_user_id FROM message\nLEFT JOIN json AS data_json USING (json_id)\nLEFT JOIN json AS content_json ON (\n data_json.directory = content_json.directory AND content_json.file_name = 'content.json'\n)\nLEFT JOIN keyvalue ON (keyvalue.key = 'cert_user_id' AND keyvalue.json_id = content_json.json_id)\nORDER BY date_added"; 349 | return this.cmd("dbQuery", [query], (function(_this) { 350 | return function(messages) { 351 | var action, added, body, i, len, message, message_lines, msgseparator, poststyle, prestyle, time, userid, useridcolor, username; 352 | document.getElementById("messages").innerHTML = ""; 353 | message_lines = []; 354 | _this.resetDebug(); 355 | for (i = 0, len = messages.length; i < len; i++) { 356 | message = messages[i]; 357 | body = message.body.replace(//g, ">"); 358 | added = new Date(message.date_added); 359 | time = Time.since(message.date_added / 1000); 360 | userid = message.cert_user_id; 361 | useridcolor = Text.toColor(userid); 362 | username = message.cert_user_id.replace('@zeroid.bit', ''); 363 | msgseparator = ":"; 364 | body = _this.replaceURLs(body); 365 | if (body.substr(0, 3) === "/me") { 366 | action = body.replace("/me", ""); 367 | username = username + ' ' + action; 368 | body = ''; 369 | msgseparator = ''; 370 | } 371 | prestyle = ""; 372 | poststyle = ""; 373 | if (userid === Page.site_info.cert_user_id) { 374 | prestyle = ''; 375 | poststyle = ''; 376 | } 377 | if (Page.site_info.cert_user_id && body.indexOf(Page.site_info.cert_user_id.replace('@zeroid.bit', '')) > -1) { 378 | prestyle = ''; 379 | poststyle = ''; 380 | } 381 | body = prestyle + body + poststyle; 382 | message_lines.push("
  • " + time + " " + username + "" + msgseparator + " " + body + "
  • "); 383 | } 384 | message_lines.reverse(); 385 | return document.getElementById("messages").innerHTML = message_lines.join("\n"); 386 | }; 387 | })(this)); 388 | }; 389 | 390 | ZeroChat.prototype.addLine = function(line) { 391 | var messages; 392 | messages = document.getElementById("messages"); 393 | return messages.innerHTML = ("
  • " + line + "
  • ") + messages.innerHTML; 394 | }; 395 | 396 | ZeroChat.prototype.onOpenWebsocket = function(e) { 397 | this.cmd("siteInfo", {}, (function(_this) { 398 | return function(site_info) { 399 | document.getElementById("bigTitle").innerHTML = site_info.content.title + ' - ' + site_info.content.description; 400 | document.getElementById("peerCount").innerHTML = site_info.peers + ' Visitors'; 401 | 402 | if (site_info.cert_user_id) { 403 | document.getElementById("select_user").innerHTML = site_info.cert_user_id; 404 | } 405 | return _this.site_info = site_info; 406 | }; 407 | })(this)); 408 | return this.loadMessages(); 409 | }; 410 | 411 | return ZeroChat; 412 | 413 | })(ZeroFrame); 414 | 415 | window.Page = new ZeroChat(); 416 | 417 | }).call(this); 418 | -------------------------------------------------------------------------------- /js/lib/ZeroFrame.coffee: -------------------------------------------------------------------------------- 1 | class ZeroFrame 2 | constructor: (url) -> 3 | @url = url 4 | @waiting_cb = {} 5 | @connect() 6 | @next_message_id = 1 7 | @init() 8 | 9 | 10 | init: -> 11 | @ 12 | 13 | 14 | connect: -> 15 | @target = window.parent 16 | window.addEventListener("message", @onMessage, false) 17 | @cmd("innerReady") 18 | 19 | 20 | onMessage: (e) => 21 | message = e.data 22 | cmd = message.cmd 23 | if cmd == "response" 24 | if @waiting_cb[message.to]? 25 | @waiting_cb[message.to](message.result) 26 | else 27 | @log "Websocket callback not found:", message 28 | else if cmd == "wrapperReady" # Wrapper inited later 29 | @cmd("innerReady") 30 | else if cmd == "ping" 31 | @response message.id, "pong" 32 | else if cmd == "wrapperOpenedWebsocket" 33 | @onOpenWebsocket() 34 | else if cmd == "wrapperClosedWebsocket" 35 | @onCloseWebsocket() 36 | else 37 | @route cmd, message 38 | 39 | 40 | route: (cmd, message) => 41 | @log "Unknown command", message 42 | 43 | 44 | response: (to, result) -> 45 | @send {"cmd": "response", "to": to, "result": result} 46 | 47 | 48 | cmd: (cmd, params={}, cb=null) -> 49 | @send {"cmd": cmd, "params": params}, cb 50 | 51 | 52 | send: (message, cb=null) -> 53 | message.id = @next_message_id 54 | @next_message_id += 1 55 | @target.postMessage(message, "*") 56 | if cb 57 | @waiting_cb[message.id] = cb 58 | 59 | 60 | log: (args...) -> 61 | console.log "[ZeroFrame]", args... 62 | 63 | 64 | onOpenWebsocket: => 65 | @log "Websocket open" 66 | 67 | 68 | onCloseWebsocket: => 69 | @log "Websocket close" 70 | 71 | 72 | 73 | window.ZeroFrame = ZeroFrame -------------------------------------------------------------------------------- /js/utils/Text.coffee: -------------------------------------------------------------------------------- 1 | class Text 2 | toColor: (text, saturation=30, lightness=40) -> 3 | hash = 0 4 | for i in [0..text.length-1] 5 | hash += text.charCodeAt(i)*i 6 | hash = hash % 1777 7 | return "hsl(" + (hash % 360) + ",#{saturation}%,#{lightness}%)"; 8 | 9 | window.Text = new Text() 10 | -------------------------------------------------------------------------------- /js/utils/Time.coffee: -------------------------------------------------------------------------------- 1 | class Time 2 | since: (time) -> 3 | now = +(new Date)/1000 4 | secs = now - time 5 | if secs < 60 6 | back = "Just now" 7 | else if secs < 60*60 8 | back = "#{Math.round(secs/60)} mins ago" 9 | else if secs < 60*60*24 10 | back = "#{Math.round(secs/60/60)} hrs ago" 11 | else if secs < 60*60*24*3 12 | back = "#{Math.round(secs/60/60/24)} days ago" 13 | else 14 | back = "on "+@date(time) 15 | back = back.replace(/1 ([a-z]+)s/, "1 $1") # 1 days ago fix 16 | return back 17 | 18 | 19 | date: (timestamp, format="short") -> 20 | parts = (new Date(timestamp*1000)).toString().split(" ") 21 | if format == "short" 22 | display = parts.slice(1, 4) 23 | else 24 | display = parts.slice(1, 5) 25 | return display.join(" ").replace(/( [0-9]{4})/, ",$1") 26 | 27 | 28 | timestamp: (date="") -> 29 | if date == "now" or date == "" 30 | return parseInt(+(new Date)/1000) 31 | else 32 | return parseInt(Date.parse(date)/1000) 33 | 34 | 35 | window.Time = new Time 36 | --------------------------------------------------------------------------------