In part1 we created a ZeroNet site with some simple ZeroFrame API call.
We going to extend it to accept, store and query messages using a SQLite database and use ZeroID to identify user names.
The final page and source code is available at https://www.zerogate.tk/1AvF5TpcaamRNtqvN1cnDEWzNmUtD47Npg
Adding ZeroID user selector
Add a new link to index.html that will allow us to select the ZeroID identity we want to use:
<html>
<body>
<a href="#Select+user" id="select_user" onclick='return Page.selectUser()'>Select user</a>:
<input type="text" id="message"><input type="button" id="send" value="Send!"/>
<ul id="messages">
<li>Welcome to ZeroChat!</li>
</ul>
<script type="text/javascript" src="js/all.js" async></script>
</body>
</html>
To make it work add a function to js/ZeroChat.coffee that displays the certificate selection dialog to user:
selectUser: =>
Page.cmd "certSelect", [["zeroid.bit"]]
return false
As parameter you should include the accepted certificate provider names. (zeroid.bit here)
If you hit a refresh and click on "Select user", then you should see the dialog, but nothing changes when you select your zeroid certificate.
Display user's current ZeroID account
When something is changed that affects the site (new content arrived, user changed, etc.) a websocket event will be pushed to your browser.
(the format is same as you query setSiteInfo command)
To handle this event add this function:
route: (cmd, message) ->
if cmd == "setSiteInfo"
if message.params.cert_user_id
document.getElementById("select_user").innerHTML = message.params.cert_user_id
else
document.getElementById("select_user").innerHTML = "Select user"
@site_info = message.params # Save site info data to allow access it later
This code will real-time update the user's currently selected user name.
To also update the user name on page load modify the onOpenWebsocket function:
onOpenWebsocket: (e) =>
@cmd "serverInfo", {}, (server_info) =>
@addLine "serverInfo response: <pre>" + JSON.stringify(server_info,null,2) + "</pre>"
@cmd "siteInfo", {}, (site_info) =>
@addLine "siteInfo response: <pre>" + JSON.stringify(site_info,null,2) + "</pre>"
# Update currently selected username
if site_info.cert_user_id
document.getElementById("select_user").innerHTML = site_info.cert_user_id
@site_info = site_info # Save site info data to allow access it later
Setting user content permissions
To allow users to post on our site we have to define the rules of the third-party content.
Create a data/users directory and create a data/users/content.json file in it:
{
"files": {},
"ignore": ".*",
"modified": 0.0,
"signs": {},
"user_contents": {
"cert_signers": {
"zeroid.bit": [ "1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz" ]
},
"permission_rules": {
".*": {
"files_allowed": "data.json",
"max_size": 10000
},
"bitmsg/.*@zeroid.bit": { "max_size": 15000 }
},
"permissions": {
"bad@zeroid.bit": false,
"nofish@zeroid.bit": { "max_size": 100000 }
}
}
}
"ignore": ".*",
: When the site owner signing this content.json don't add any file to it, they will be signed by the users.
cert_signers
: We accept *@zeroid.bit users and they have to come with a cert that is has to signed by 1iD5ZQJMNXu43w1qLB8sfdHVKppVMduGz
address.
permission_rules
: We give 10kbytes of space to every user (15kb if registered using bitmessage)
permissions
: Per-user permissions: ban bad@zeroid.bit user and allow 100k storage to nofish@zeroid.bit user. ( it's me :) )
After we saved this file we have to modify our root content.json to also ignore files in this directory and load the file containing the rules:
...
"ignore": "data/.*",
"includes": {
"data/users/content.json": {
"signers": [],
"signers_required": 1
}
},
...
Note: You can give moderation permissions to other users by adding addresses to "signers" list.
Now we have to sign the data/users/content.json file using the following command:
zeronet.py siteSign [siteaddress] --inner_path data/users/content.json
Adding messages to our json file
When hitting the Send button we going to add the message to our data.json file, sign it, then publish it to other users.
sendMessage: =>
if not Page.site_info.cert_user_id # No account selected, display error
Page.cmd "wrapperNotification", ["info", "Please, select your account."]
return false
inner_path = "data/users/#{@site_info.auth_address}/data.json" # This is our data file
# Load our current messages
@cmd "fileGet", {"inner_path": inner_path, "required": false}, (data) =>
if data # Parse if already exits
data = JSON.parse(data)
else # Not exits yet, use default data
data = { "message": [] }
# Add the message to data
data.message.push({
"body": document.getElementById("message").value,
"date_added": (+new Date)
})
# Encode data array to utf8 json text
json_raw = unescape(encodeURIComponent(JSON.stringify(data, undefined, '\t')))
# Write file to disk
@cmd "fileWrite", [inner_path, btoa(json_raw)], (res) =>
if res == "ok"
# Publish the file to other users
@cmd "sitePublish", {"inner_path": inner_path}, (res) =>
document.getElementById("message").value = "" # Reset the message input
else
@cmd "wrapperNotification", ["error", "File write error: #{res}"]
return false
- After this is done type something to the message input and press the Send! button! You should see the message in the data/users/[your auth address]/data.json file.
Creating database
Now we can save and publish our messages to other users, let's display it in our application! The best way to do this is map all data.json files to an SQL database.
The ZeroNet automatically do this for you, all you need is a dbschema.json file in your site's directory that describe your table structure:
{
"db_name": "ZeroChat",
"db_file": "data/zerochat.db",
"version": 2,
"maps": {
"users/.+/data.json": {
"to_table": [ "message" ]
},
"users/.+/content.json": {
"to_keyvalue": [ "cert_user_id" ]
}
},
"tables": {
"message": {
"cols": [
["body", "TEXT"],
["date_added", "INTEGER"],
["json_id", "INTEGER REFERENCES json (json_id)"]
],
"indexes": ["CREATE UNIQUE INDEX message_key ON message(json_id, date_added)"],
"schema_changed": 1
}
}
}
"db_name": "ZeroChat"
: Used only for debugging
"db_file": "data/zerochat.db"
: The SQLite database file will be stored here
"version": 2
: Define the json table structure, version 2 is better suited to ZeroID based sites. More info in the reference docs.
"maps": {
: Describe the json files -> table conversion
"users/.+/data.json": { "to_table": [ "message" ] }
: Put the data from every user data.json file message node to message table.
"users/.+/content.json": { "to_keyvalue": [ "cert_user_id" ] }
: Store the user's authentication id in simple key/value structure.
"tables": {
: Describe the table and indexing structure.
["json_id", "INTEGER REFERENCES json (json_id)"]
: Every table should contain a json_id column, it defines the source file path.
"schema_changed": 1
: Increment this when you change the table structure, so the peers can drop the table and re-create it from the json files.
Tip: For the best performance always create an index for json_id column in your tables, because when new file arrives the data will be updated based on this column.
Execute zeronet.py dbRebuild [your site address]
command to generate database from current files.
The data/zerochat.db SQLite file will be created, to browse these files I recommend using SQLiteStudio (it's free and opensource)
At this point you have to restart your ZeroNet client to detect and manage your site's newly created database.
Displaying messages
As the messages are now it the SQL database we can query them using the dbQuery command:
loadMessages: ->
@cmd "dbQuery", ["SELECT * FROM message ORDER BY date_added"], (messages) =>
document.getElementById("messages").innerHTML = "" # Always start with empty messages
for message in messages
@addLine message.body
Add the @loadMessages()
line to onOpenWebsocket function, then reload the page and you should see the messages you typed in.
To make it "real time" and display new messages immediately as they come in you have to add the @loadMessages()
to the route part:
route: (cmd, message) ->
if cmd == "setSiteInfo"
if message.params.cert_user_id
document.getElementById("select_user").innerHTML = message.params.cert_user_id
else
document.getElementById("select_user").innerHTML = "Select user"
@site_info = message.params # Save site info data to allow access it later
# Reload messages if new file arrives
if message.params.event[0] == "file_done"
@loadMessages()
And also reload the data when we submit a new message:
sendMessage: =>
...
# Write file to disk
@cmd "fileWrite", [inner_path, btoa(json_raw)], (res) =>
@loadMessages()
...
...
That's it! Now the messages are updated in real-time! You can try it by opening an another browser window and enter a messages there.
Displaying ZeroID usernames
To display the sender user name we have to create a more complex SQL query, because the user names are stored in content.json
file and the messages are in data.json
.
+ we will also escape the incoming messages to disallow html codes.
loadMessages: ->
query = """
SELECT message.*, keyvalue.value AS cert_user_id FROM message
LEFT JOIN json AS data_json USING (json_id)
LEFT JOIN json AS content_json ON (
data_json.directory = content_json.directory AND content_json.file_name = 'content.json'
)
LEFT JOIN keyvalue ON (keyvalue.key = 'cert_user_id' AND keyvalue.json_id = content_json.json_id)
ORDER BY date_added
"""
@cmd "dbQuery", [query], (messages) =>
document.getElementById("messages").innerHTML = "" # Always start with empty messages
for message in messages
body = message.body.replace(/</g, "<").replace(/>/g, ">") # Escape html tags in body
@addLine "<b>#{message.cert_user_id}</b>: #{body}"
Final touches
- We can remove the site siteInfo and serverInfo debug messages:
onOpenWebsocket: (e) =>
@cmd "siteInfo", {}, (site_info) =>
# Update currently selected username
if site_info.cert_user_id
document.getElementById("select_user").innerHTML = site_info.cert_user_id
@site_info = site_info # Save site info data to allow access it later
@loadMessages()
- Send messages by hitting Enter:
<input type="text" id="message" onkeypress="if (event.keyCode == 13) Page.sendMessage()">
- And add some CSS style to make it look better
<style>
* { font-family: monospace; line-height: 1.5em; font-size: 13px; vertical-align: middle; }
body { background-color: white; }
input#message { padding: 5px; width: 50%; height: 34px; }
input#send { height: 34px; margin-left: -2px; }
ul { padding: 0px; }
li { list-style-type: none; border-top: 1px solid #eee; padding: 5px 0px; }
li:nth-child(odd) { background-color: #F9FAFD; }
li b { color: #3F51B5; }
</style>
- After you edited any file never forget to sign and publish the modifications!
zeronet.py siteSign [your site address] --publish
Congratulations! Now you have a server-less, pure P2P, SQL backed chat application! :)