├── README.md
├── apionly
├── control
│ └── control
└── data
│ └── www
│ └── cgi-bin
│ └── meshchat
├── build
├── debian-binary
├── meshchat-api_2.16_all.ipk
├── meshchat_2.16_all.ipk
└── src
├── control
├── control
├── postinst
├── preinst
└── prerm
└── data
├── etc
└── init.d
│ └── meshchatsync
├── usr
└── local
│ └── bin
│ └── meshchatsync
└── www
├── cgi-bin
├── meshchat
├── meshchatconfig.lua
└── meshchatlib.lua
└── meshchat
├── alert.mp3
├── chat.js
├── files.html
├── files.js
├── index.html
├── jquery-2.2.0.min.js
├── js.cookie.js
├── md5.js
├── messages.js
├── normalize.css
├── numeral.min.js
├── ohsnap.js
├── shared.js
├── skeleton.css
├── status.html
├── status.js
└── style.css
/README.md:
--------------------------------------------------------------------------------
1 | # meshchat
2 |
3 | MeshChat for AREDN (in Lua)
4 |
5 | Based on the Perl version https://github.com/tpaskett/meshchat
6 |
--------------------------------------------------------------------------------
/apionly/control/control:
--------------------------------------------------------------------------------
1 | Package: meshchat-api
2 | Version: 2.16
3 | Depends: luci-lib-base
4 | Provides:
5 | Source: package/meshchat-api
6 | Section: net
7 | Priority: optional
8 | Maintainer: Tim Wilkinson (KN6PLV) and Trevor Paskett (K7FPV)
9 | Architecture: all
10 | Description: P2P distributed chat for mesh networks
11 |
--------------------------------------------------------------------------------
/apionly/data/www/cgi-bin/meshchat:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 | --[[
3 |
4 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
5 | Copyright (C) 2022 Tim Wilkinson
6 | Base on code (C) Trevor Paskett (see https://github.com/tpaskett)
7 | See Contributors file for additional contributors
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation version 3 of the License.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 |
21 | Additional Terms:
22 |
23 | Additional use restrictions exist on the AREDN(TM) trademark and logo.
24 | See AREDNLicense.txt for more info.
25 |
26 | Attributions to the AREDN Project must be retained in the source code.
27 | If importing this code into a new or existing project attribution
28 | to the AREDN project must be added to the source code.
29 |
30 | You must not misrepresent the origin of the material contained within.
31 |
32 | Modified versions must be modified to attribute to the original source
33 | and be marked in reasonable ways as differentiate it from the original
34 | version
35 |
36 | --]]
37 |
38 | require('nixio')
39 | require('luci.http')
40 |
41 | function str_escape(str)
42 | return str:gsub("%(", "%%("):gsub("%)", "%%)"):gsub("%%", "%%%%"):gsub("%.", "%%."):gsub("%+", "%%+"):gsub("-", "%%-"):gsub("%*", "%%*"):gsub("%[", "%%["):gsub("%?", "%%?"):gsub("%^", "%%^"):gsub("%$", "%%$")
43 | end
44 |
45 | local query = {}
46 | if os.getenv("QUERY_STRING") ~= "" then
47 | query = luci.http.Request(nixio.getenv()):formvalue()
48 | end
49 |
50 | print("Content-type: text/plain\r")
51 | print("\r")
52 | if query.action == "meshchat_nodes" then
53 | local pattern = "http://(%S+):(%d+)/meshchat|tcp|" .. str_escape(query.zone_name) .. "%s"
54 | if nixio.fs.stat("/var/run/services_olsr") then
55 | for line in io.lines("/var/run/services_olsr")
56 | do
57 | local node, port = line:match(pattern)
58 | if node and port then
59 | print(node .. "\t" .. port)
60 | end
61 | end
62 | end
63 | if nixio.fs.stat("/var/run/arednlink/services") then
64 | for file in nixio.fs.dir("/var/run/arednlink/services")
65 | do
66 | for line in io.lines("/var/run/arednlink/services/" .. file)
67 | do
68 | local node, port = line:match(pattern)
69 | if node and port then
70 | print(node .. "\t" .. port)
71 | end
72 | end
73 | end
74 | end
75 | else
76 | print("error no action")
77 | end
78 |
--------------------------------------------------------------------------------
/build:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | rm -rf *.ipk *.deb
4 | find . -name '*~' -delete
5 |
6 | ### Build AREDN package
7 | export COPYFILE_DISABLE=1
8 | export VERSION=2.16
9 |
10 | # Main
11 | rm -rf data.tar.gz control.tar.gz
12 | cd src/data
13 | sed -i "s/^app_version.*$/app_version = \"${VERSION}\"/" www/cgi-bin/meshchatconfig.lua
14 | tar cf ../../data.tar `find . -type f | grep -v DS_Store | grep -v .pl | grep -v .pm`
15 | cd ../control
16 | sed -i "s/^Version: .*$/Version: ${VERSION}/" control
17 | tar cfz ../../control.tar.gz .
18 | cd ../..
19 | gzip data.tar
20 | COPYFILE_DISABLE=1 tar cfz meshchat_${VERSION}_all.ipk control.tar.gz data.tar.gz debian-binary
21 |
22 | # API-only
23 | rm -rf data.tar.gz control.tar.gz
24 | cd apionly/data
25 | tar cf ../../data.tar `find . -type f | grep -v DS_Store | grep -v .pl | grep -v .pm`
26 | cd ../control
27 | sed -i "s/^Version: .*$/Version: ${VERSION}/" control
28 | tar cfz ../../control.tar.gz .
29 | cd ../..
30 | gzip data.tar
31 | COPYFILE_DISABLE=1 tar cfz meshchat-api_${VERSION}_all.ipk control.tar.gz data.tar.gz debian-binary
32 |
33 | rm -rf data.tar.gz control.tar.gz *.deb
34 |
--------------------------------------------------------------------------------
/debian-binary:
--------------------------------------------------------------------------------
1 | 2.0
2 |
--------------------------------------------------------------------------------
/meshchat-api_2.16_all.ipk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kn6plv/meshchat/63e34400190da4b764253952bbffd18f14216e2e/meshchat-api_2.16_all.ipk
--------------------------------------------------------------------------------
/meshchat_2.16_all.ipk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kn6plv/meshchat/63e34400190da4b764253952bbffd18f14216e2e/meshchat_2.16_all.ipk
--------------------------------------------------------------------------------
/src/control/control:
--------------------------------------------------------------------------------
1 | Package: meshchat
2 | Version: 2.16
3 | Depends: curl, luci-lib-base, libuci-lua, libubus-lua
4 | Provides:
5 | Source: package/meshchat
6 | Section: net
7 | Priority: optional
8 | Maintainer: Tim Wilkinson (KN6PLV) and Trevor Paskett (K7FPV)
9 | Architecture: all
10 | Description: P2P distributed chat for mesh networks
11 |
--------------------------------------------------------------------------------
/src/control/postinst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | if [ "$(uci -q -c /etc/config.mesh show setup.services | grep '|8080|meshchat')" = "" ]; then
4 | RAND=$(awk 'BEGIN{srand();print int(rand()*10000) }')
5 | uci -c /etc/config.mesh add_list setup.services.service="MeshChat-$RAND [chat]|1|http|$(uname -n)|8080|meshchat"
6 | uci -c /etc/config.mesh commit setup
7 | fi
8 |
9 | # Hack for missing but unused lua file
10 | mkdir -p /usr/lib/lua/luci/template
11 | touch /usr/lib/lua/luci/template/parser.lua
12 |
13 | /usr/local/bin/node-setup &> /dev/null
14 | /usr/local/bin/restart-services.sh &> /dev/null
15 |
16 | /etc/init.d/meshchatsync enable
17 | /etc/init.d/meshchatsync start
18 |
19 | exit 0
20 |
--------------------------------------------------------------------------------
/src/control/preinst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | /etc/init.d/meshchatsync stop > /dev/null 2> /dev/null
4 |
5 | mkdir -p /www/meshchat
6 |
7 | # if there is not a meshchat_local.lua, then prepare one
8 | if [ ! -f /www/cgi-bin/meshchat_local.lua ]; then
9 | if [ -f /www/cgi-bin/meshchatconfig.lua ]; then
10 | cp /www/cgi-bin/meshchatconfig.lua /www/cgi-bin/meshchat_local.lua
11 |
12 | # remove vars that should not be in meshchat_local.lua
13 | sed -i "/^protocol_version/d; /^app_version/d" /www/cgi-bin/meshchat_local.lua
14 | else
15 | touch /www/cgi-bin/meshchat_local.lua
16 | fi
17 | fi
18 |
19 | exit 0
20 |
--------------------------------------------------------------------------------
/src/control/prerm:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | /etc/init.d/meshchatsync disable
4 | /etc/init.d/meshchatsync stop
5 |
6 | rm -rf /tmp/meshchat
7 |
8 | exit 0
9 |
--------------------------------------------------------------------------------
/src/data/etc/init.d/meshchatsync:
--------------------------------------------------------------------------------
1 | #!/bin/sh /etc/rc.common
2 |
3 | START=99
4 | APP=meshchatsync
5 | SERVICE_WRITE_PID=1
6 | SERVICE_DAEMONIZE=1
7 |
8 | start() {
9 | service_start /usr/local/bin/meshchatsync
10 | }
11 | stop() {
12 | service_stop /usr/local/bin/meshchatsync
13 | killall meshchatsync
14 | }
15 |
--------------------------------------------------------------------------------
/src/data/usr/local/bin/meshchatsync:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 | --[[
3 |
4 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
5 | Copyright (C) 2022 Tim Wilkinson
6 | Based on code (C) Trevor Paskett (see https://github.com/tpaskett)
7 | See Contributors file for additional contributors
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation version 3 of the License.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 |
21 | Additional Terms:
22 |
23 | Additional use restrictions exist on the AREDN(TM) trademark and logo.
24 | See AREDNLicense.txt for more info.
25 |
26 | Attributions to the AREDN Project must be retained in the source code.
27 | If importing this code into a new or existing project attribution
28 | to the AREDN project must be added to the source code.
29 |
30 | You must not misrepresent the origin of the material contained within.
31 |
32 | Modified versions must be modified to attribute to the original source
33 | and be marked in reasonable ways as differentiate it from the original
34 | version
35 |
36 | --]]
37 |
38 | package.path = package.path .. ";/www/cgi-bin/?.lua"
39 | require("nixio")
40 | require("meshchatconfig")
41 | require("meshchatlib")
42 |
43 | local sync_status = {}
44 | local non_mesh_chat_nodes = {}
45 |
46 | local node = node_name()
47 |
48 | if not nixio.fs.stat(meshchat_path) then
49 | nixio.fs.mkdir(meshchat_path)
50 | nixio.fs.mkdir(local_files_dir)
51 | end
52 |
53 | if not nixio.fs.stat(messages_db_file) then
54 | io.open(messages_db_file, "w"):close()
55 | nixio.fs.chmod(messages_db_file, "666")
56 | end
57 |
58 | io.open(local_users_status_file, "a"):close()
59 | io.open(remote_users_status_file, "a"):close()
60 |
61 | save_messages_db_version()
62 |
63 | nixio.fs.chmod(meshchat_path, "666")
64 |
65 | io.open(lock_file, "a"):close()
66 |
67 | function log_status()
68 | local cur_status = {}
69 |
70 | if not nixio.fs.stat(sync_status_file) then
71 | io.open(sync_status_file, "w"):close()
72 | end
73 |
74 | get_lock()
75 |
76 | for line in io.lines(sync_status_file)
77 | do
78 | local key, value = line:match("^(.*)\t(.*)$")
79 | cur_status[key] = value
80 | end
81 |
82 | local f = io.open(sync_status_file, "w")
83 | if f then
84 | for key, value in pairs(sync_status)
85 | do
86 | f:write(key .. "\t" .. value .. "\n")
87 | end
88 | for key, value in pairs(cur_status)
89 | do
90 | if not sync_status[key] then
91 | f:write(key .. "\t" .. value .. "\n")
92 | end
93 | end
94 | f:close()
95 | end
96 |
97 | release_lock()
98 | end
99 |
100 | function merge_messages()
101 | local rmsg = {}
102 | local lmsg = {}
103 |
104 | for line in io.lines(meshchat_path .. "/remote_messages")
105 | do
106 | local key = line:match("^(%S+)%s")
107 | rmsg[key] = line
108 | end
109 |
110 | get_lock()
111 |
112 | for line in io.lines(messages_db_file)
113 | do
114 | local key = line:match("^(%S+)%s")
115 | lmsg[key] = line
116 | end
117 |
118 | local f = io.open(messages_db_file, "a")
119 | if f then
120 | for rmsg_id, line in pairs(rmsg)
121 | do
122 | if not lmsg[rmsg_id] then
123 | f:write(line .. "\n")
124 | end
125 | end
126 | f:close()
127 | end
128 |
129 | sort_and_trim_db()
130 |
131 | save_messages_db_version()
132 |
133 | release_lock()
134 | end
135 |
136 | function merge_users()
137 | local rusers = {}
138 | local lusers = {}
139 |
140 | for line in io.lines(meshchat_path .. "/remote_users")
141 | do
142 | local key, value = line:match("^(%S+\t%S+\t%S+)\t(.*)$")
143 | if not line:match("error") and key then
144 | rusers[key] = value
145 | end
146 | end
147 |
148 | get_lock()
149 |
150 | for line in io.lines(remote_users_status_file)
151 | do
152 | local key, value = line:match("^(%S+\t%S+\t%S+)\t(.*)$")
153 | if not line:match("error") and key then
154 | lusers[key] = value
155 | end
156 | end
157 |
158 | local f = io.open(remote_users_status_file, "w")
159 | if f then
160 | for key, _ in pairs(rusers)
161 | do
162 | if lusers[key] and lusers[key] > rusers[key] then
163 | f:write(key .. "\t" .. lusers[key] .. "\n")
164 | else
165 | f:write(key .. "\t" .. rusers[key] .. "\n")
166 | end
167 | end
168 | for key, _ in pairs(lusers)
169 | do
170 | if not rusers[key] then
171 | f:write(key .. "\t" .. lusers[key] .. "\n")
172 | end
173 | end
174 | f:close()
175 | end
176 |
177 | release_lock()
178 | end
179 |
180 | while true
181 | do
182 | local nodes = node_list()
183 |
184 | sync_status = {}
185 |
186 | for _, node_info in ipairs(nodes)
187 | do
188 | for _ = 1,1
189 | do
190 | local remote_node = node_info.node
191 | local remote_platform = node_info.platform
192 | local remote_port = node_info.port
193 |
194 | local port = ""
195 | if remote_port ~= "" then
196 | port = ":" .. remote_port
197 | end
198 |
199 | if port == "" and remote_platform == "node" then
200 | port = ":8080"
201 | end
202 |
203 | local version = get_messages_db_version()
204 |
205 | -- Poll non mesh chat nodes at a longer interval
206 | if non_mesh_chat_nodes[remote_node] and os.time() < non_mesh_chat_nodes[remote_node] then
207 | break
208 | end
209 |
210 | nixio.fs.remove(meshchat_path .. "/remote_users")
211 |
212 | -- Get remote users file
213 | local f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " -sD - \"http://" .. remote_node .. port .. "/cgi-bin/meshchat?action=users_raw&platform=" .. platform .. "&node=" .. node .. "\" -o " .. meshchat_path .. "/remote_users 2>&1")
214 | local output = f:read("*a")
215 | f:close()
216 |
217 | -- Check if meshchat is installed
218 | if output:match("404 Not Found") then
219 | non_mesh_chat_nodes[remote_node] = os.time() + non_meshchat_poll_interval
220 | break
221 | end
222 |
223 | local md5 = output:match("Content%-MD5:%s([0-9a-f]+)\r\n")
224 | if md5 then
225 | local f_md5 = file_md5(meshchat_path .. "/remote_users")
226 | if md5 == f_md5 then
227 | local cur_size = nixio.fs.stat(meshchat_path .. "/remote_users").size
228 | if cur_size > 0 then
229 | merge_users()
230 | end
231 | end
232 | end
233 |
234 | -- Get remote files file
235 | nixio.fs.remove(meshchat_path .. "/remote_files")
236 | f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " -sD - \"http://" .. remote_node .. port .. "/cgi-bin/meshchat?action=local_files_raw\" -o " .. meshchat_path .. "/remote_files 2>&1")
237 | output = f:read("*a")
238 | f:close()
239 |
240 | md5 = output:match("Content%-MD5:%s([0-9a-f]+)\r\n")
241 | if md5 then
242 | local f_md5 = file_md5(meshchat_path .. "/remote_files")
243 | nixio.fs.remove(meshchat_path .. "/remote_files." .. remote_node)
244 | if md5 == f_md5 then
245 | local cur_size = nixio.fs.stat(meshchat_path .. "/remote_files").size
246 | if cur_size > 0 then
247 | nixio.fs.rename(meshchat_path .. "/remote_files", meshchat_path .. "/remote_files." .. remote_node)
248 | end
249 | end
250 | end
251 |
252 | -- Get remote messages
253 | nixio.fs.remove(meshchat_path .. "/remote_messages")
254 |
255 | f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " \"http://" .. remote_node .. port .. "/cgi-bin/meshchat?action=messages_version\" -o - 2> /dev/null")
256 | local remote_version = f:read("*a")
257 | f:close()
258 |
259 | -- Check the version of the remote db against ours. Only download the db if the remote has a different copy
260 |
261 | if remote_version ~= "" and version == remote_version then
262 | sync_status[remote_node] = os.time()
263 | break
264 | end
265 |
266 | f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " -sD - \"http://" .. remote_node .. port .. "/cgi-bin/meshchat?action=messages_raw\" -o " .. meshchat_path .. "/remote_messages 2>&1")
267 | local output = f:read("*a")
268 | f:close()
269 |
270 | if nixio.fs.stat(meshchat_path .. "/remote_messages") then
271 | local md5 = output:match("Content%-MD5:%s([0-9a-f]+)\r\n")
272 | if md5 then
273 | local f_md5 = file_md5(meshchat_path .. "/remote_messages")
274 | if md5 == f_md5 then
275 | local cur_size = nixio.fs.stat(meshchat_path .. "/remote_messages").size
276 | if cur_size > 0 then
277 | sync_status[remote_node] = os.time()
278 | merge_messages()
279 | end
280 | end
281 | end
282 | end
283 | end
284 | end
285 |
286 | log_status()
287 |
288 | nixio.fs.remove(meshchat_path .. "/remote_messages")
289 | nixio.fs.remove(meshchat_path .. "/remote_users")
290 | nixio.fs.remove(meshchat_path .. "/remote_files")
291 |
292 | nixio.nanosleep(poll_interval, 0)
293 | end
294 |
--------------------------------------------------------------------------------
/src/data/www/cgi-bin/meshchat:
--------------------------------------------------------------------------------
1 | #!/usr/bin/lua
2 | --[[
3 |
4 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
5 | Copyright (C) 2022 Tim Wilkinson
6 | Base on code (C) Trevor Paskett (see https://github.com/tpaskett)
7 | See Contributors file for additional contributors
8 |
9 | This program is free software: you can redistribute it and/or modify
10 | it under the terms of the GNU General Public License as published by
11 | the Free Software Foundation version 3 of the License.
12 |
13 | This program is distributed in the hope that it will be useful,
14 | but WITHOUT ANY WARRANTY; without even the implied warranty of
15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 | GNU General Public License for more details.
17 |
18 | You should have received a copy of the GNU General Public License
19 | along with this program. If not, see .
20 |
21 | Additional Terms:
22 |
23 | Additional use restrictions exist on the AREDN(TM) trademark and logo.
24 | See AREDNLicense.txt for more info.
25 |
26 | Attributions to the AREDN Project must be retained in the source code.
27 | If importing this code into a new or existing project attribution
28 | to the AREDN project must be added to the source code.
29 |
30 | You must not misrepresent the origin of the material contained within.
31 |
32 | Modified versions must be modified to attribute to the original source
33 | and be marked in reasonable ways as differentiate it from the original
34 | version
35 |
36 | --]]
37 |
38 | package.path = package.path .. ";/www/cgi-bin/?.lua"
39 |
40 | require('luci.http')
41 | local json = require("luci.jsonc")
42 | require("nixio")
43 | require("meshchatconfig")
44 | require("meshchatlib")
45 |
46 | ---
47 | -- @module meshchat
48 |
49 | local query = {}
50 | local uploadfilename
51 | if os.getenv("QUERY_STRING") ~= "" or os.getenv("REQUEST_METHOD") == "POST" then
52 | local request = luci.http.Request(nixio.getenv(),
53 | function()
54 | local v = io.read(1024)
55 | if not v then
56 | io.close()
57 | end
58 | return v
59 | end
60 | )
61 | local fp
62 | request:setfilehandler(
63 | function(meta, chunk, eof)
64 | if not fp then
65 | if meta and meta.file then
66 | uploadfilename = meta.file
67 | end
68 | nixio.fs.mkdir(tmp_upload_dir)
69 | fp = io.open(tmp_upload_dir .. "/file", "w")
70 | end
71 | if chunk then
72 | fp:write(chunk)
73 | end
74 | if eof then
75 | fp:close()
76 | end
77 | end
78 | )
79 | query = request:formvalue()
80 | end
81 |
82 | --- Return an error page to a browser.
83 | -- @tparam string msg Error message to be displayed
84 | --
85 | function error(msg)
86 | print("Content-type: text/plain\r")
87 | print("\r")
88 | print(msg)
89 | end
90 |
91 | --- @section API
92 |
93 | --- Returns a JSON document with basic node configuration.
94 | --
95 | -- ## API Parameters
96 | -- | Parameter | Required | Description |
97 | -- |-----------|----------|------------------------------------------|
98 | -- | action | yes | Must be set to `config` |
99 | --
100 | -- ## API Response
101 | --
102 | -- @example
103 | -- {
104 | -- "version": "meshchat_version",
105 | -- "node": "node_name",
106 | -- "zone": "meshchat_zone_name"
107 | -- }
108 | --
109 | function config()
110 | print("Content-type: application/json\r")
111 | print("\r")
112 |
113 | local settings = {
114 | version = app_version,
115 | protocol_verison = protocol_version,
116 | node = node_name(),
117 | zone = zone_name(),
118 | default_channel = default_channel,
119 | debug = debug,
120 | }
121 |
122 | print(json.stringify(settings))
123 | end
124 |
125 | --- Send a message to the MeshChat instance.
126 | --
127 | -- ## API Parameters
128 | -- | Parameter | Required | Description |
129 | -- |-----------|----------|------------------------------------------|
130 | -- | action | yes | Must be set to `send_message` |
131 | -- | message | yes | Message body |
132 | -- | call_sign | yes | Call sign of the sender |
133 | -- | channel | no | Channel name to post message |
134 | -- | epoch | no | Timestamp specified as unixtime |
135 | --
136 | -- @note message
137 | -- Needs to have newslines and double quotes escaped.
138 | --
139 | -- @note channel
140 | -- If not specified or set to empty string will post message to
141 | -- `Everything` channel
142 | --
143 | -- @note epoch
144 | -- If not specified, the current time on the MeshChat server will
145 | -- be used.
146 | --
147 | -- ## API Response
148 | --
149 | -- On a successful entry of the message into the database a success JSON
150 | -- document will be returned.
151 | --
152 | -- @example
153 | -- {
154 | -- "status": 200,
155 | -- "response": "OK"
156 | -- }
157 | --
158 | function send_message()
159 | print("Content-type: application/json\r")
160 | print("\r")
161 |
162 | local message = query.message:gsub("\n", "\\n"):gsub('"', '\\"'):gsub("\t", " ")
163 | local id = query.id or hash();
164 | local epoch = os.time()
165 | if tonumber(query.epoch) > epoch then
166 | epoch = query.epoch
167 | end
168 |
169 | get_lock()
170 |
171 | local f = io.open(messages_db_file, "a")
172 | if not f then
173 | release_lock()
174 | -- TODO return a proper error code on failure
175 | die("Cannot send message")
176 | end
177 | f:write(id .. "\t" .. epoch .. "\t" .. message .. "\t" .. query.call_sign .. "\t" .. node_name() .. "\t" .. platform .. "\t" .. query.channel .. "\n")
178 | f:close()
179 |
180 | sort_and_trim_db()
181 | save_messages_db_version()
182 |
183 | release_lock()
184 |
185 | print([[{"status":200, "response":"OK"}]])
186 | end
187 |
188 | --- Return a list of message stored on the MeshChat instance.
189 | --
190 | -- ## API Parameters
191 | -- | Parameter | Required | Description |
192 | -- |-----------|----------|------------------------------------------|
193 | -- | action | yes | Must be set to `messages` |
194 | -- | call_sign | no | Call sign of the requester |
195 | -- | epoch | no | Timestamp specified as unixtime |
196 | -- | id | no | Generated MeshChat ID |
197 | --
198 | -- ## API Response
199 | --
200 | -- @example
201 | -- {
202 | -- "id": "id_str",
203 | -- "epoch": epoch_time,
204 | -- "message": "message_text",
205 | -- "call_sign": "sending_call_sign",
206 | -- "node": "originating_node",
207 | -- "platform": "originating_node_platform",
208 | -- "channel": "channel"
209 | -- }
210 | --
211 | function messages()
212 |
213 | print("Content-type: application/json\r")
214 | local output = io.stdout
215 | local encoding = os.getenv("HTTP_ACCEPT_ENCODING")
216 | if encoding and encoding:match("gzip") then
217 | print "Content-Encoding: gzip\r"
218 | output = io.popen("gzip", "w")
219 | end
220 | print("\r")
221 | io.flush()
222 |
223 | get_lock()
224 |
225 | local node = node_name()
226 |
227 | -- read in message DB and parse the contents
228 | local messages = {}
229 | for line in io.lines(messages_db_file)
230 | do
231 | local id, epoch, message, call_sign, node, platform, channel = line:match("^(%S+)\t(%S+)\t(.+)\t([^\t]+)\t(%S*)\t(%S+)\t(%S*)$")
232 | if epoch and #epoch > 0 then
233 | messages[#messages + 1] = {
234 | id = id,
235 | epoch = tonumber(epoch),
236 | message = message:gsub("\\n", "\n"):gsub('\\"', '"'),
237 | call_sign = call_sign,
238 | node = node,
239 | platform = platform,
240 | channel = channel
241 | }
242 | end
243 | end
244 |
245 | if tonumber(query.epoch) and query.call_sign then
246 | local users = {}
247 |
248 | -- read the users status file
249 | if nixio.fs.stat(local_users_status_file) then
250 | for line in io.lines(local_users_status_file)
251 | do
252 | local call_sign = line:match("^([^\t]+)\t")
253 | if call_sign then
254 | users[call_sign] = line
255 | end
256 | end
257 | end
258 |
259 | -- set the timestamp
260 | local epoch = os.time()
261 | if tonumber(query.epoch) > epoch then
262 | epoch = query.epoch
263 | end
264 |
265 | -- rewrite user status file updating the timestamp for requesting call sign
266 | -- query.id is the meshchat_id
267 | local f = io.open(local_users_status_file, "w")
268 | if f then
269 | local found_user = false
270 | for call_sign, line in pairs(users)
271 | do
272 | if call_sign == query.call_sign then
273 | f:write(call_sign .. "\t" .. query.id .. "\t" .. node .. "\t" .. epoch .. "\t" .. platform .. "\n")
274 | found_user = true
275 | else
276 | f:write(line .. "\n")
277 | end
278 | end
279 | if not found_user then
280 | f:write(query.call_sign .. "\t" .. query.id .. "\t" .. node .. "\t" .. epoch .. "\t" .. platform .. "\n")
281 | end
282 | f:close()
283 | end
284 | end
285 |
286 | release_lock()
287 |
288 | -- order messages according to time
289 | table.sort(messages, function(a, b) return a.epoch > b.epoch end)
290 |
291 | output:write(json.stringify(messages))
292 | output:flush()
293 |
294 | end
295 |
296 | --- Return a JSON document describing the sync status.
297 | --
298 | -- ## API Parameters
299 | -- | Parameter | Required | Description |
300 | -- |-----------|----------|------------------------------------------|
301 | -- | action | yes | Must be set to `sync_status` |
302 | --
303 | -- ## API Response
304 | --
305 | -- @example
306 | -- {
307 | -- "node": "node_name",
308 | -- "epoch": sync_time
309 | -- }
310 | --
311 | function sync_status()
312 | print("Content-type: application/json\r")
313 | print("\r")
314 |
315 | get_lock()
316 |
317 | local status = {}
318 | if nixio.fs.stat(sync_status_file) then
319 | for line in io.lines(sync_status_file)
320 | do
321 | local node, epoch = line:match("^(.*)\t(.*)$")
322 | status[#status + 1] = {
323 | node = node,
324 | epoch = tonumber(epoch)
325 | }
326 | end
327 | end
328 |
329 | release_lock()
330 |
331 | table.sort(status, function(a, b) return a.epoch > b.epoch end)
332 |
333 | print(json.stringify(status))
334 | end
335 |
336 | --- Return a list of messages as text.
337 | function messages_raw()
338 | get_lock()
339 |
340 | local md5 = file_md5(messages_db_file)
341 | local lines = {}
342 | for line in io.lines(messages_db_file)
343 | do
344 | lines[#lines + 1] = line
345 | end
346 |
347 | release_lock()
348 |
349 | print("Content-MD5: " .. md5 .. "\r")
350 | print("Content-type: text/plain\r")
351 | print("\r")
352 |
353 | for _, line in ipairs(lines)
354 | do
355 | print(line)
356 | end
357 | end
358 |
359 | --- Return the current MD5 has of the messages database.
360 | function messages_md5()
361 | get_lock()
362 |
363 | local md5 = file_md5(messages_db_file)
364 |
365 | release_lock()
366 |
367 | print("Content-type: text/plain\r")
368 | print("\r")
369 | print(md5)
370 | end
371 |
372 | --- Package the raw messages as the messages.txt file.
373 | function messages_download()
374 | get_lock()
375 |
376 | local md5 = file_md5(messages_db_file)
377 | local lines = {}
378 | for line in io.lines(messages_db_file)
379 | do
380 | lines[#lines + 1] = line
381 | end
382 |
383 | release_lock()
384 |
385 | print("Content-MD5: " .. md5 .. "\r")
386 | print("Content-Disposition: attachment; filename=messages.txt;\r")
387 | print("Content-type: text/plain\r")
388 | print("\r")
389 |
390 | for _, line in ipairs(lines)
391 | do
392 | print(line)
393 | end
394 | end
395 |
396 | --- Return the list of users as raw text.
397 | function users_raw()
398 | get_lock()
399 |
400 | local md5 = file_md5(local_users_status_file)
401 | local lines = {}
402 | for line in io.lines(local_users_status_file)
403 | do
404 | lines[#lines + 1] = line
405 | end
406 |
407 | release_lock()
408 |
409 | print("Content-MD5: " .. md5 .. "\r")
410 | print("Content-type: text/plain\r")
411 | print("\r")
412 |
413 | for _, line in ipairs(lines)
414 | do
415 | print(line)
416 | end
417 | end
418 |
419 | --- Return a JSON document describing the logged in users.
420 | --
421 | -- ## API Parameters
422 | -- | Parameter | Required | Description |
423 | -- |-----------|----------|------------------------------------------|
424 | -- | action | yes | Must be set to `users` |
425 | --
426 | -- ## API Response
427 | --
428 | -- @example
429 | -- {
430 | -- "id": "id_str",
431 | -- "epoch": epoch_time,
432 | -- "call_sign": "sender_call_sign',
433 | -- "node": "originating_node",
434 | -- "platform": "originating_platform",
435 | -- }
436 | --
437 | function users()
438 | print("Content-type: application/json\r")
439 | print("\r")
440 |
441 | get_lock()
442 |
443 | local users = {}
444 | for line in io.lines(local_users_status_file)
445 | do
446 | local call_sign, id, node, epoch, platform = line:match("^(.*)\t(.*)\t(.*)\t(.*)\t(.*)$")
447 | if epoch and #epoch > 0 then
448 | users[#users + 1] = {
449 | epoch = tonumber(epoch),
450 | id = id,
451 | call_sign = call_sign,
452 | node = node,
453 | platform = platform
454 | }
455 | end
456 | end
457 | for line in io.lines(remote_users_status_file)
458 | do
459 | local call_sign, id, node, epoch, platform = line:match("^(.*)\t(.*)\t(.*)\t(.*)\t(.*)$")
460 | if epoch and #epoch > 0 then
461 | users[#users + 1] = {
462 | epoch = tonumber(epoch),
463 | id = id,
464 | call_sign = call_sign,
465 | node = node,
466 | platform = platform
467 | }
468 | end
469 | end
470 |
471 | release_lock()
472 |
473 | table.sort(users, function(a, b) return a.epoch > b.epoch end)
474 |
475 | print(json.stringify(users))
476 | end
477 |
478 | --- Return a list of files as plain text.
479 | function local_files_raw()
480 | get_lock()
481 |
482 | local tmp_file = meshchat_path .. "/meshchat_files_local." .. nixio.getpid()
483 | local f = io.open(tmp_file, "w")
484 | if not f then
485 | die("Cannot list local files")
486 | end
487 | local name = node_name() .. ":" .. os.getenv("SERVER_PORT")
488 | for file in nixio.fs.dir(local_files_dir)
489 | do
490 | local stat = nixio.fs.stat(local_files_dir .. "/" .. file)
491 | f:write(file .. "\t" .. name .. "\t" .. stat.size .. "\t" .. stat.mtime .. platform .. "\n")
492 | end
493 | f:close()
494 |
495 | local md5 = file_md5(tmp_file)
496 |
497 | release_lock()
498 |
499 | print("Content-MD5: " .. md5 .. "\r")
500 | print("Content-type: text/plain\r")
501 | print("\r")
502 |
503 | for line in io.lines(tmp_file)
504 | do
505 | print(line)
506 | end
507 |
508 | nixio.fs.remove(tmp_file)
509 | end
510 |
511 | --- Return a specified file as a download.
512 | --
513 | -- ## API Parameters
514 | -- | Parameter | Required | Description |
515 | -- |-----------|----------|------------------------------------------|
516 | -- | action | yes | Must be set to `file_download` |
517 | -- | file | yes | Name of file to downlaod |
518 | --
519 | -- ## API Response
520 | --
521 | -- Returns a page as an octet-stream that is tagged as an attachment
522 | -- to cause the browser to receive the file as a download.
523 | --
524 | function file_download()
525 | local file = query.file
526 | local file_path = local_files_dir .. "/" .. file
527 |
528 | if file == "" or not nixio.fs.stat(file_path) then
529 | error("no file")
530 | return
531 | end
532 |
533 | get_lock()
534 |
535 | local md5 = file_md5(file_path)
536 | local f = io.open(file_path, "rb")
537 |
538 | release_lock()
539 |
540 | print("Content-MD5: " .. md5 .. "\r")
541 | print("Content-Disposition: attachment; filename=\"" .. file .. "\";\r")
542 | print("Content-type: application/octet-stream\r")
543 | print("\r")
544 |
545 | if f then
546 | io.write(f:read("*a"))
547 | f:close()
548 | end
549 | end
550 |
551 | --- Return a JSON document describing the list of files.
552 | --
553 | -- ## API Parameters
554 | -- | Parameter | Required | Description |
555 | -- |-----------|----------|------------------------------------------|
556 | -- | action | yes | Must be set to `files` |
557 | --
558 | -- ## API Response
559 | --
560 | -- @example
561 | -- {
562 | -- "file": "filename",
563 | -- "epoch": modification_time,
564 | -- "size": file_size_in_bytes,
565 | -- "node": "originating_node",
566 | -- "platform": "originating_platform"
567 | -- }
568 | --
569 | function files()
570 | print("Content-type: application/json\r")
571 | print("\r")
572 |
573 | get_lock()
574 |
575 | local files = {}
576 | local node = node_name() .. ":" .. os.getenv("SERVER_PORT")
577 | for file in nixio.fs.dir(local_files_dir)
578 | do
579 | local stat = nixio.fs.stat(local_files_dir .. "/" .. file)
580 | files[#files + 1] = {
581 | file = file,
582 | epoch = stat.mtime,
583 | size = stat.size,
584 | node = node,
585 | platform = platform
586 | }
587 | files[#files]["local"] = 1
588 | end
589 | for file in nixio.fs.dir(meshchat_path)
590 | do
591 | if file:match("^remote_files%.") then
592 | for line in io.lines(meshchat_path .. "/" .. file)
593 | do
594 | local name, node, size, epoch, platform = line:match("^(.*)\t(.*)\t(.*)\t(.*)\t(.*)$")
595 | if epoch and #epoch > 0 then
596 | files[#files + 1] = {
597 | file = name,
598 | epoch = tonumber(epoch),
599 | size = size,
600 | node = node,
601 | platform = platform
602 | }
603 | files[#files]["local"] = 0
604 | end
605 | end
606 | end
607 | end
608 |
609 | local stats = file_storage_stats()
610 |
611 | release_lock()
612 |
613 | table.sort(files, function(a, b) return a.epoch > b.epoch end)
614 |
615 | print(json.stringify({
616 | stats = stats,
617 | files = files
618 | }))
619 | end
620 |
621 | --- Delete the specified file.
622 | function delete_file()
623 | nixio.fs.remove(local_files_dir .. "/" .. query.file)
624 | print("Content-type: application/json\r")
625 | print("\r")
626 | print([[{"status":200, "response":"OK"}]])
627 | end
628 |
629 | --- Return the current version string for the messages database.
630 | function messages_version()
631 | print("Content-type: text/plain\r")
632 | print("\r")
633 | print(get_messages_db_version())
634 | end
635 |
636 | --- Return a JSON document of the messages database.
637 | function messages_version_ui()
638 | print("Content-type: application/json\r")
639 | print("\r")
640 |
641 | print(string.format([[{"messages_version":%s}]], get_messages_db_version()))
642 |
643 | get_lock()
644 |
645 | local users = {}
646 | for line in io.lines(local_users_status_file)
647 | do
648 | local call_sign = line:match("^([^\t]+)\t")
649 | if call_sign then
650 | users[call_sign] = line
651 | end
652 | end
653 |
654 | local node = node_name()
655 | local epoch = os.time()
656 | if tonumber(query.epoch) > epoch then
657 | epoch = query.epoch
658 | end
659 |
660 | -- TODO refactor here and messages function into a single code block
661 | local f = io.open(local_users_status_file, "w")
662 | if f then
663 | local found_user = false
664 | for call_sign, line in pairs(users)
665 | do
666 | if call_sign == query.call_sign then
667 | f:write(call_sign .. "\t" .. query.id .. "\t" .. node .. "\t" .. epoch .. "\t" .. platform .. "\n")
668 | found_user = true
669 | else
670 | f:write(line .. "\n")
671 | end
672 | end
673 | if not found_user then
674 | f:write(query.call_sign .. "\t" .. query.id .. "\t" .. node .. "\t" .. epoch .. "\t" .. platform .. "\n")
675 | end
676 | f:close()
677 | end
678 |
679 | release_lock()
680 | end
681 |
682 | --- Return a JSON document describing all the hosts.
683 | --
684 | -- ## API Parameters
685 | -- | Parameter | Required | Description |
686 | -- |-----------|----------|------------------------------------------|
687 | -- | action | yes | Must be set to `hosts` |
688 | --
689 | -- ## API Response
690 | --
691 | -- @example
692 | -- {
693 | -- "ip": "ip_address",
694 | -- "hostname": "hostname",
695 | -- "node": "node_name"
696 | -- }
697 | --
698 | function hosts()
699 | print("Content-type: application/json\r")
700 | print("\r")
701 |
702 | local node = node_name()
703 | local hosts = {}
704 | for line in io.lines("/tmp/dhcp.leases")
705 | do
706 | local epoch, mac1, ip, hostname, mac2 = line:match("^(%S+)%s(%S+)%s(%S+)%s(%S+)%s(%S+)$")
707 | hosts[#hosts + 1] = {
708 | ip = ip,
709 | hostname = hostname,
710 | node = node
711 | }
712 | end
713 |
714 | for line in io.lines("/etc/config.mesh/_setup.dhcp.dmz")
715 | do
716 | local mac, num, hostname = line:match("^(%S+)%s(%S+)%s(%S+)$")
717 | local ip = gethostbyname(hostname)
718 | hosts[#hosts + 1] = {
719 | ip = ip,
720 | hostname = hostname,
721 | node = node
722 | }
723 | end
724 |
725 | for _, remote_node in ipairs(node_list())
726 | do
727 | local f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " http://" .. remote_node .. ":8080/cgi-bin/meshchat?action=hosts_raw 2> /dev/null")
728 | if f then
729 | for line in f:lines()
730 | do
731 | if line ~= "" and not line:match("error") then
732 | local ip, hostname = line:match("^(.+)\t(.+)$")
733 | hosts[#hosts + 1] = {
734 | ip = ip,
735 | hostname = hostname,
736 | node = remote_node
737 | }
738 | end
739 | end
740 | f:close()
741 | end
742 | end
743 |
744 | table.sort(hosts, function(a, b) return a.hostname < b.hostname end)
745 |
746 | print(json.stringify(hosts))
747 | end
748 |
749 | --- Return a list of hosts as plain text.
750 | function hosts_raw()
751 | print("Content-type: application/json\r")
752 | print("\r")
753 |
754 | local hosts = {}
755 | for line in io.lines("/tmp/dhcp.leases")
756 | do
757 | local epoch, mac1, ip, hostname, mac2 = line:match("^(%S+)%s(%S+)%s(%S+)%s(%S+)%s(%S+)$")
758 | hosts[#hosts + 1] = {
759 | ip = ip,
760 | hostname = hostname
761 | }
762 | end
763 |
764 | for line in io.lines("/etc/config.mesh/_setup.dhcp.dmz")
765 | do
766 | local mac, num, hostname = line:match("^(%S+)%s(%S+)%s(%S+)$")
767 | local ip = gethostbyname(hostname)
768 | hosts[#hosts + 1] = {
769 | ip = ip,
770 | hostname = hostname
771 | }
772 | end
773 |
774 | for _, host in ipairs(hosts)
775 | do
776 | print(host.ip .. "\t" .. host.hostname)
777 | end
778 | end
779 |
780 | --- Store a file into the file directory.
781 | function upload_file()
782 | local new_file_size = nixio.fs.stat(tmp_upload_dir .. "/file").size
783 |
784 | get_lock()
785 |
786 | local stats = file_storage_stats()
787 |
788 | release_lock()
789 |
790 | print("Content-type: application/json\r")
791 | print("\r")
792 |
793 | if new_file_size > stats.files_free then
794 | nixio.fs.remove(tmp_upload_dir .. "/file")
795 | print([[{"status":500, "response":"Not enough storage, delete some files"}]])
796 | else
797 | local fi = io.open(tmp_upload_dir .. "/file", "r")
798 | local fo = io.open(local_files_dir .. "/" .. uploadfilename, "w")
799 | fo:write(fi:read("*a"))
800 | fi:close()
801 | fo:close()
802 | nixio.fs.remove(tmp_upload_dir .. "/file")
803 | print([[{"status":200, "response":"OK"}]])
804 | end
805 | end
806 |
807 | --- Return a list of nodes running MeshChat as text.
808 | --
809 | -- ## API Parameters
810 | -- | Parameter | Required | Description |
811 | -- |-----------|----------|------------------------------------------|
812 | -- | action | yes | Must be set to `meshchat_nodes` |
813 | -- | zone_name | yes | MeshChat zone name |
814 | --
815 | -- ## API Response
816 | --
817 | -- The list of nodes and ports seperated by a tab.
818 | --
819 | -- @example
820 | -- node1 8080
821 | -- node2 8080
822 | --
823 | function meshchat_nodes()
824 | print("Content-type: text/plain\r")
825 | print("\r")
826 |
827 | local pattern = "http://(%S+):(%d+)/meshchat|tcp|" .. str_escape(query.zone_name) .. "%s"
828 | if nixio.fs.stat("/var/run/services_olsr") then
829 | for line in io.lines("/var/run/services_olsr")
830 | do
831 | local node, port = line:match(pattern)
832 | if node and port then
833 | print(node .. "\t" .. port)
834 | end
835 | end
836 | end
837 | if nixio.fs.stat("/var/run/arednlink/services") then
838 | for file in nixio.fs.dir("/var/run/arednlink/services")
839 | do
840 | for line in io.lines("/var/run/arednlink/services/" .. file)
841 | do
842 | local node, port = line:match(pattern)
843 | if node and port then
844 | print(node .. "\t" .. port)
845 | end
846 | end
847 | end
848 | end
849 | end
850 |
851 | --- Return a JSON document of the action log.
852 | --
853 | -- Currently this call returns an empty list. In the future it will
854 | -- return a list of action log events.
855 | --
856 | function action_log()
857 | print("Content-type: application/json\r")
858 | print("\r")
859 | print("[]")
860 | end
861 |
862 | -- Command dispatch --
863 |
864 | if query.action == "messages" then
865 | messages()
866 | elseif query.action == "config" then
867 | config()
868 | elseif query.action == "send_message" then
869 | send_message()
870 | elseif query.action == "sync_status" then
871 | sync_status()
872 | elseif query.action == "messages_raw" then
873 | messages_raw()
874 | elseif query.action == "messages_md5" then
875 | messages_md5()
876 | elseif query.action == "messages_download" then
877 | messages_download()
878 | elseif query.action == "users_raw" then
879 | users_raw()
880 | elseif query.action == "users" then
881 | users()
882 | elseif query.action == "local_files_raw" then
883 | local_files_raw()
884 | elseif query.action == "file_download" then
885 | file_download()
886 | elseif query.action == "files" then
887 | files()
888 | elseif query.action == "delete_file" then
889 | delete_file()
890 | elseif query.action == "messages_version" then
891 | messages_version()
892 | elseif query.action == "messages_version_ui" then
893 | messages_version_ui()
894 | elseif query.action == "hosts" then
895 | hosts()
896 | elseif query.action == "hosts_raw" then
897 | hosts_raw()
898 | elseif query.action == "upload_file" then
899 | upload_file()
900 | elseif query.action == "meshchat_nodes" then
901 | meshchat_nodes()
902 | elseif query.action == "action_log" then
903 | action_log()
904 | else
905 | error("error no action")
906 | end
907 |
--------------------------------------------------------------------------------
/src/data/www/cgi-bin/meshchatconfig.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | STOP STOP STOP DO NOT EDIT THIS FILE
3 |
4 | This file is used to set default values for MeshChat and should NOT
5 | be edited. Edits made to this file WILL BE LOST when upgrading or
6 | downgrading MeshChat. All the values below can be overridden by
7 | adding them to the meshchat_local.lua file located in the same
8 | directory as this file.
9 |
10 | EDIT THIS FILE AT YOUR PERIL. YOU HAVE BEEN WARNED.
11 |
12 |
13 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
14 | Copyright (C) 2022 Tim Wilkinson
15 | Base on code (C) Trevor Paskett (see https://github.com/tpaskett)
16 | See Contributors file for additional contributors
17 |
18 | This program is free software: you can redistribute it and/or modify
19 | it under the terms of the GNU General Public License as published by
20 | the Free Software Foundation version 3 of the License.
21 |
22 | This program is distributed in the hope that it will be useful,
23 | but WITHOUT ANY WARRANTY; without even the implied warranty of
24 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 | GNU General Public License for more details.
26 |
27 | You should have received a copy of the GNU General Public License
28 | along with this program. If not, see .
29 |
30 | Additional Terms:
31 |
32 | Additional use restrictions exist on the AREDN(TM) trademark and logo.
33 | See AREDNLicense.txt for more info.
34 |
35 | Attributions to the AREDN Project must be retained in the source code.
36 | If importing this code into a new or existing project attribution
37 | to the AREDN project must be added to the source code.
38 |
39 | You must not misrepresent the origin of the material contained within.
40 |
41 | Modified versions must be modified to attribute to the original source
42 | and be marked in reasonable ways as differentiate it from the original
43 | version
44 |
45 | --]]
46 |
47 | ---
48 | -- @module meshchatconfig
49 | -- @section MeshChat Configuration
50 |
51 | --- Base directory to store all MeshChat generated files
52 | -- @type string
53 | meshchat_path = "/tmp/meshchat"
54 | --- Maximum number of messages in the database
55 | -- @type int
56 | max_messages_db_size = 500
57 | --- Maximum amount of filesystem space for storing files
58 | -- @type int
59 | max_file_storage = 512 * 1024
60 | lock_file = meshchat_path .. "/lock"
61 | messages_db_file = meshchat_path .. "/messages"
62 | messages_db_file_orig = meshchat_path .. "/messages"
63 | sync_status_file = meshchat_path .. "/sync_status"
64 | local_users_status_file = meshchat_path .. "/users_local"
65 | remote_users_status_file = meshchat_path .. "/users_remote"
66 | remote_files_file = meshchat_path .. "/files_remote"
67 | messages_version_file = meshchat_path .. "/messages_version"
68 | local_files_dir = meshchat_path .. "/files"
69 | tmp_upload_dir = "/tmp/web/upload"
70 | --- How often to check for new messages
71 | -- @type int
72 | poll_interval = 10
73 | non_meshchat_poll_interval = 600
74 | valid_future_message_time = 30 * 24 * 60 * 60
75 | connect_timeout = 5
76 | speed_time = 10
77 | speed_limit = 1000
78 | --- Type of node that MeshChat is installed on ("node" or "pi")
79 | -- @type string
80 | platform = "node"
81 | --- Turn debug message on
82 | -- @type bool
83 | debug = 0
84 | extra_nodes = {}
85 | --- MeshChat protocol version
86 | -- @type string
87 | protocol_version = "1.02"
88 | app_version = "2.16"
89 | default_channel = ""
90 |
91 | require("meshchat_local")
92 |
--------------------------------------------------------------------------------
/src/data/www/cgi-bin/meshchatlib.lua:
--------------------------------------------------------------------------------
1 | --[[
2 |
3 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks
4 | Copyright (C) 2022 Tim Wilkinson
5 | Base on code (C) Trevor Paskett (see https://github.com/tpaskett)
6 | See Contributors file for additional contributors
7 |
8 | This program is free software: you can redistribute it and/or modify
9 | it under the terms of the GNU General Public License as published by
10 | the Free Software Foundation version 3 of the License.
11 |
12 | This program is distributed in the hope that it will be useful,
13 | but WITHOUT ANY WARRANTY; without even the implied warranty of
14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 | GNU General Public License for more details.
16 |
17 | You should have received a copy of the GNU General Public License
18 | along with this program. If not, see .
19 |
20 | Additional Terms:
21 |
22 | Additional use restrictions exist on the AREDN(TM) trademark and logo.
23 | See AREDNLicense.txt for more info.
24 |
25 | Attributions to the AREDN Project must be retained in the source code.
26 | If importing this code into a new or existing project attribution
27 | to the AREDN project must be added to the source code.
28 |
29 | You must not misrepresent the origin of the material contained within.
30 |
31 | Modified versions must be modified to attribute to the original source
32 | and be marked in reasonable ways as differentiate it from the original
33 | version
34 |
35 | --]]
36 |
37 | require("nixio")
38 | require("uci")
39 |
40 | --- @module meshchatlib
41 |
42 | --- Exit the program with an error message.
43 | --
44 | -- @tparam string msg Message to display
45 | --
46 | function die(msg)
47 | os.exit(-1)
48 | end
49 |
50 | --- Execute a command and capture the output.
51 | --
52 | -- @tparam string cmd Command line to execute
53 | -- @treturn string stdout of the command
54 | --
55 | function capture(cmd)
56 | local f = io.popen(cmd)
57 | if not f then
58 | return ""
59 | end
60 | local output = f:read("*a")
61 | f:close()
62 | return output
63 | end
64 |
65 | ---
66 | -- Retrieve the current node name.
67 | --
68 | -- This function will interogate the UCI settings to retrieve the current
69 | -- node name stored in the `hsmmmesh` settings.
70 | --
71 | -- @treturn string Name of current node
72 | --
73 | function node_name()
74 | return uci.cursor("/etc/local/uci"):get("hsmmmesh", "settings", "node") or ""
75 | end
76 |
77 | ---
78 | -- Retrieve the current MeshChat zone name that the node is operating under.
79 | --
80 | -- @treturn string Name of MeshChat zone
81 | --
82 | function zone_name()
83 | local services = uci.cursor("/etc/config.mesh"):get("setup", "services", "service") or {}
84 | for _, service in ipairs(services)
85 | do
86 | local zone = service:match("^(.*)%s+%[.+%]|.*|.*|.*|.*|meshchat$") or service:match("^(.*)|.*|.*|.*|.*|meshchat$")
87 | if zone then
88 | return zone
89 | end
90 | end
91 | return "MeshChat"
92 | end
93 |
94 | messages_db_file = messages_db_file_orig .. "." .. zone_name()
95 |
96 | local lock_fd
97 | function get_lock()
98 | if not lock_fd then
99 | lock_fd = nixio.open(lock_file, "w", "666")
100 | end
101 | lock_fd:lock("lock")
102 | end
103 |
104 | function release_lock()
105 | lock_fd:lock("ulock")
106 | end
107 |
108 | --- Generate the MD5 sum of a file.
109 | --
110 | -- This under the covers relies on `md5sum` and executes `md5sum` against
111 | -- the specified file.
112 | --
113 | -- @note
114 | -- There is no checking to determine if `md5sum` is installed or
115 | -- executable. In the future, this may change.
116 | --
117 | -- @tparam string file Path to file
118 | -- @treturn string Result of `md5sum` of the file
119 | --
120 | function file_md5(file)
121 | if not nixio.fs.stat(file) then
122 | return ""
123 | end
124 | local output = capture("md5sum " .. file:gsub(" ", "\\ ")):match("^(%S+)%s")
125 | return output and output or ""
126 | end
127 |
128 | function get_messages_db_version()
129 | for line in io.lines(messages_version_file)
130 | do
131 | line = line:gsub("\n$", "")
132 | return line
133 | end
134 | end
135 |
136 | function save_messages_db_version()
137 | local f = io.open(messages_version_file, "w")
138 | f:write(get_messages_version_file() .. "\n")
139 | f:close()
140 | nixio.fs.chmod(messages_version_file, "666")
141 | end
142 |
143 | function get_messages_version_file()
144 | local sum = 0
145 | for line in io.lines(messages_db_file)
146 | do
147 | local key = line:match("^([0-9a-f]+)")
148 | if key then
149 | sum = sum + tonumber(key, 16)
150 | end
151 | end
152 | return sum
153 | end
154 |
155 | --- Generate a unique hash.
156 | --
157 | -- Combine the current time (epoch time) and a randomly generated number
158 | -- between 0 - 99999 and run through `md5sum` to generate a random hash.
159 | --
160 | -- @note
161 | -- There is no checking to determine if `md5sum` is installed or
162 | -- executable. In the future, this may change.
163 | --
164 | -- @treturn string Generated hash value
165 | --
166 | function hash()
167 | return capture("echo " .. os.time() .. math.random(99999) .. " | md5sum"):sub(1, 8)
168 | end
169 |
170 | function sort_and_trim_db()
171 | local valid_time = os.time() + valid_future_message_time
172 | local unused_count = max_messages_db_size
173 | local messages = {}
174 | for line in io.lines(messages_db_file)
175 | do
176 | local id, epoch = line:match("^(%x+)\t(%S+)\t")
177 | -- ignore messages that are too far in the future (assume they're errors)
178 | epoch = tonumber(epoch)
179 | if epoch and epoch < valid_time then
180 | messages[#messages + 1] = {
181 | epoch = epoch,
182 | id = tonumber(id, 16),
183 | line = line
184 | }
185 | end
186 | unused_count = unused_count - 1
187 | end
188 |
189 | table.sort(messages, function(a, b)
190 | if a.epoch == b.epoch then
191 | return a.id < b.id
192 | else
193 | return a.epoch < b.epoch
194 | end
195 | end)
196 |
197 | local f = io.open(messages_db_file, "w")
198 | for _, line in ipairs(messages)
199 | do
200 | unused_count = unused_count + 1
201 | if unused_count > 0 then
202 | f:write(line.line .. "\n")
203 | end
204 | end
205 | f:close()
206 | end
207 |
208 | function file_storage_stats()
209 | local lines = capture("df -k " .. local_files_dir)
210 | local blocks, used, available, perc = lines:match("(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%%")
211 | used = tonumber(used) * 1024
212 | available = tonumber(available) * 1024
213 | local total = used + available
214 |
215 | local local_files_bytes = 0
216 | for file in nixio.fs.dir(local_files_dir)
217 | do
218 | local_files_bytes = local_files_bytes + nixio.fs.stat(local_files_dir .. "/" .. file).size
219 | end
220 |
221 | if max_file_storage - local_files_bytes < 0 then
222 | local_files_bytes = max_file_storage
223 | end
224 |
225 | return {
226 | total = total,
227 | used = used,
228 | files = local_files_bytes,
229 | files_free = max_file_storage - local_files_bytes,
230 | allowed = max_file_storage
231 | }
232 | end
233 |
234 | function gethostbyname(hostname)
235 | return capture("nslookup " .. hostname):match("Address 1:%s*([%d%.]+)")
236 | end
237 |
238 | function node_list()
239 | local local_node = node_name():lower()
240 | local zone = zone_name()
241 |
242 | local nodes = {}
243 | local pattern = "http://(%S+):(%d+)/meshchat|tcp|" .. str_escape(zone) .. "%s"
244 |
245 | if nixio.fs.stat("/var/run/services_olsr") then
246 | for line in io.lines("/var/run/services_olsr")
247 | do
248 | local node, port = line:match(pattern)
249 | if node and port then
250 | node = node:lower()
251 | if node ~= local_node then
252 | nodes[#nodes + 1] = {
253 | platform = (port == "8080" and "node" or "pi"),
254 | node = node,
255 | port = port
256 | }
257 | end
258 | end
259 | end
260 | end
261 | if nixio.fs.stat("/var/run/arednlink/services") then
262 | for file in nixio.fs.dir("/var/run/arednlink/services")
263 | do
264 | for line in io.lines("/var/run/arednlink/services/" .. file)
265 | do
266 | local node, port = line:match(pattern)
267 | if node and port then
268 | node = node:lower()
269 | if node ~= local_node then
270 | nodes[#nodes + 1] = {
271 | platform = (port == "8080" and "node" or "pi"),
272 | node = node,
273 | port = port
274 | }
275 | end
276 | end
277 | end
278 | end
279 | end
280 |
281 | for _, extra in ipairs(extra_nodes)
282 | do
283 | nodes[#node + 1] = extra
284 | end
285 |
286 | return nodes
287 | end
288 |
289 | ---
290 | -- Escape percent signs.
291 | --
292 | -- @tparam string str String to encode
293 | -- @treturn string Encoded string
294 | --
295 | function str_escape(str)
296 | return str:gsub("%(", "%%("):gsub("%)", "%%)"):gsub("%%", "%%%%"):gsub("%.", "%%."):gsub("%+", "%%+"):gsub("-", "%%-"):gsub("%*", "%%*"):gsub("%[", "%%["):gsub("%?", "%%?"):gsub("%^", "%%^"):gsub("%$", "%%$")
297 | end
298 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/alert.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kn6plv/meshchat/63e34400190da4b764253952bbffd18f14216e2e/src/data/www/meshchat/alert.mp3
--------------------------------------------------------------------------------
/src/data/www/meshchat/chat.js:
--------------------------------------------------------------------------------
1 | var meshchat_id;
2 | var last_messages_update = epoch();
3 | var call_sign = 'NOCALL';
4 | var enable_video = 0;
5 |
6 | var messages = new Messages();
7 | let alert = new Audio('alert.mp3');
8 |
9 | let context = {
10 | config_loaded: false,
11 | debug: true, // let startup funcs show debug
12 | }
13 |
14 | $(function() {
15 | meshchat_init();
16 | });
17 |
18 | function monitor_last_update() {
19 | var secs = epoch() - last_messages_update;
20 | $('#last-update').html('Updated: ' + secs + ' seconds ago');
21 | }
22 |
23 | function update_messages(reason=Messages.MSG_UPDATE) {
24 | if (reason != Messages.MSG_UPDATE) return;
25 | try {
26 | var caller = (new Error()).stack.split("\n")[3].split("/")[0];
27 | }
28 | catch (TypeError) {
29 | var caller = "unknown_caller";
30 | }
31 | console.debug(caller + "->update_messages(reason=MSG_UPDATE)");
32 |
33 | // update the message table
34 | let html = messages.render($('#channels').val(), $('#search').val());
35 | if (html) $('#message-table').html(html);
36 | last_messages_update = epoch();
37 | }
38 |
39 | function new_messages(reason) {
40 | if (reason != Messages.NEW_MSG) return;
41 | try {
42 | var caller = (new Error()).stack.split("\n")[3].split("/")[0];
43 | }
44 | catch (TypeError) {
45 | var caller = "unknown_caller";
46 | }
47 | console.debug(caller + "->new_messages(reason=NEW_MSG)");
48 | alert.play();
49 | }
50 |
51 | function update_channels(reason) {
52 | if (reason != Messages.CHAN_UPDATE) return;
53 | try {
54 | var caller = (new Error()).stack.split("\n")[3].split("/")[0];
55 | }
56 | catch (TypeError) {
57 | var caller = "unknown_caller";
58 | }
59 | console.debug(caller + "->update_channels(reason=CHAN_UPDATE)");
60 |
61 | let msg_refresh = false;
62 | let channels = messages.channels().sort();
63 | let channel_filter = $('#channels').val();
64 | let cur_send_channel = $('#send-channel').val();
65 | // null signals a new channel was just created
66 | if (cur_send_channel == null) {
67 | channel_filter = messages.current_channel();
68 | cur_send_channel = messages.current_channel();
69 | msg_refresh = true;
70 | }
71 |
72 | // clear channel selection boxes
73 | $('#send-channel').find('option').remove().end();
74 | $('#channels').find('option').remove().end();
75 |
76 | function add_option(select, title, value) {
77 | select.append("");
78 | }
79 |
80 | // Add static channels to channel selection boxes
81 | add_option($('#send-channel'), "Everything", "");
82 | add_option($('#send-channel'), "Add New Channel", "Add New Channel");
83 | add_option($('#channels'), "Everything", "");
84 |
85 | for (var chan of channels) {
86 | if (chan != "") {
87 | add_option($('#send-channel'), chan, chan);
88 | add_option($('#channels'), chan, chan);
89 | }
90 | }
91 |
92 | $("#channels").val(channel_filter);
93 | $("#send-channel").val(cur_send_channel);
94 | if (msg_refresh) update_messages();
95 | }
96 |
97 | function start_chat() {
98 | debug("start_chat()");
99 |
100 | // wait until the configuration is fully loaded
101 | $.getJSON('/cgi-bin/meshchat?action=config',
102 | (data) => {
103 | config = data;
104 | document.title = 'Mesh Chat v' + data.version;
105 | $('#version').html('Mesh Chat v' + data.version + '');
106 | $('#node').html('Node: ' + data.node);
107 | $('#zone').html('Zone: ' + data.zone);
108 | $('#callsign').html('Call Sign: ' + Cookies.get('meshchat_call_sign'));
109 | $('#copyright').html('Mesh Chat v' + data.version + ' Copyright © ' + new Date().getFullYear() + ' Trevor Paskett - K7FPV (Lua by KN6PLV)');
110 |
111 | if ("default_channel" in data) {
112 | default_channel = data.default_channel;
113 | $('#send-channel').val(data.default_channel);
114 | $('#channels').val(data.default_channel);
115 | messages.set_channel(data.default_channel);
116 | update_messages();
117 | }
118 |
119 | if ("debug" in data) {
120 | context.debug = data.debug == 1 ? true : false;
121 | }
122 |
123 | // signal that the config has finished loading
124 | context.config_loaded = true;
125 | }
126 | ).fail(
127 | (error) => {
128 | // TODO error message on UI describing failure
129 | error("Failed to load configuration from config API: " + error);
130 | }
131 | );
132 |
133 | //$('#logout').html('Logout ' + call_sign);
134 | messages.subscribe(update_messages);
135 | messages.subscribe(new_messages);
136 | messages.subscribe(update_channels);
137 | messages.check();
138 | load_users();
139 | monitor_last_update();
140 |
141 | // start event loops to update MeshChat client
142 | setInterval(() => { messages.check() }, 15000);
143 | setInterval(() => { load_users() }, 15000);
144 | setInterval(() => { monitor_last_update() }, 2500);
145 | }
146 |
147 | function meshchat_init() {
148 | debug("meshchat_init()");
149 |
150 | $('#message').val('');
151 | meshchat_id = Cookies.get('meshchat_id');
152 | if (meshchat_id == undefined) {
153 | // TODO set default expiration of cookie
154 | Cookies.set('meshchat_id', make_id());
155 | meshchat_id = Cookies.get('meshchat_id');
156 | }
157 |
158 | $('#submit-message').on('click', function(e) {
159 | e.preventDefault();
160 | if ($('#message').val().length == 0) return;
161 |
162 | ohSnapX();
163 |
164 | // disable message sending box
165 | $(this).prop("disabled", true);
166 | $('#message').prop("disabled", true);
167 | $(this).html('
');
168 |
169 | let channel = $('#send-channel').val();
170 |
171 | if ($('#new-channel').val() != '') {
172 | channel = $('#new-channel').val();
173 | $('#send-channel').val('Everything');
174 | }
175 |
176 | messages.send($('#message').val(), channel, call_sign).then(
177 | // sent
178 | (sent) => {
179 | $('#message').val('');
180 | ohSnap('Message sent', 'green');
181 | update_messages(Messages.NEW_MSG);
182 |
183 | // clear out new channel box in case it was used and
184 | // reset to normal selection box
185 | $('#new-channel').val('');
186 | $('#new-channel').hide();
187 | $('#send-channel').show();
188 | },
189 | // error
190 | (err_msg) => {
191 | ohSnap(err_msg, 'red', {time: '30000'});
192 | }
193 | ).finally(() => {
194 | // change the channel selector to the channel the message was
195 | // just sent to
196 | $('#channels').val(channel);
197 | messages.set_channel(channel);
198 | update_messages();
199 |
200 | // re-enable message sending box
201 | $(this).prop("disabled", false);
202 | $('#message').prop("disabled", false);
203 | $(this).html('Send');
204 | });
205 | });
206 |
207 | $('#submit-call-sign').on('click', function(e) {
208 | e.preventDefault();
209 | if ($('#call-sign').val().length == 0) return;
210 | call_sign = $('#call-sign').val().toUpperCase();
211 | // TODO set default expiration of cookie
212 | Cookies.set('meshchat_call_sign', call_sign);
213 | $('#call-sign-container').addClass('hidden');
214 | $('#chat-container').removeClass('hidden');
215 | $('#callsign').html('Call Sign: ' + call_sign);
216 | start_chat();
217 | });
218 |
219 | $('#channels').on('change', function() {
220 | $('#send-channel').val(this.value);
221 | messages.set_channel(this.value);
222 | update_messages();
223 | });
224 |
225 | $('#search').keyup(function() {
226 | //console.log(this.value);
227 | update_messages();
228 | });
229 |
230 | $('#message-expand').on('click', function(e) {
231 | $('#message-panel').toggleClass('message-panel-collapse');
232 | $('#message-panel-body').toggleClass('message-panel-body-collapse');
233 | $('#users-panel').toggleClass('users-panel-collapse');
234 | $('#users-panel-body').toggleClass('users-panel-body-collapse');
235 | });
236 |
237 | // allow user to enter new channel
238 | $('#send-channel').on('change', function() {
239 | if (this.value == "Add New Channel") {
240 | $('#new-channel').show().focus();
241 | $(this).hide();
242 | }
243 | });
244 |
245 | // process a CTRL to send a message
246 | $('#message').keydown(function (e) {
247 | if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) {
248 | $("#submit-message").trigger( "click" );
249 | }
250 | });
251 |
252 | // login with a cookie
253 | var cookie_call_sign = Cookies.get('meshchat_call_sign');
254 | if (cookie_call_sign == undefined) {
255 | $('#call-sign-container').removeClass('hidden');
256 | } else {
257 | $('#call-sign-container').addClass('hidden');
258 | $('#chat-container').removeClass('hidden');
259 | call_sign = cookie_call_sign;
260 | start_chat();
261 | }
262 | }
263 |
264 | let users_updating = false;
265 | function load_users() {
266 | debug("load_users()");
267 |
268 | if (users_updating == true) return;
269 | console.debug("load_users()");
270 |
271 | // lock to prevent simultaneous updates
272 | users_updating = true;
273 |
274 | $.getJSON('/cgi-bin/meshchat?action=users&call_sign=' + call_sign + '&id=' + meshchat_id,
275 | (data) => {
276 | if (data == null || data == 0) return;
277 |
278 | let html = '';
279 | let count = 0;
280 |
281 | for (var entry of data) {
282 | var date = new Date(0);
283 | date.setUTCSeconds(entry.epoch);
284 |
285 | // user heartbeat timeout > 4 mins
286 | if ((epoch() - entry.epoch) > 240) continue;
287 |
288 | // user heartbeat > 2 mins, expiring
289 | if ((epoch() - entry.epoch) > 120) {
290 | html += '';
291 | } else {
292 | html += '
';
293 | }
294 |
295 | if (enable_video == 0) {
296 | html += '' + entry.call_sign + ' | ';
297 | } else {
298 | html += '' + entry.call_sign + ' | ';
299 | }
300 |
301 | if (entry.platform == 'node') {
302 | html += '' + entry.node + ' | ';
303 | } else {
304 | html += '' + entry.node + ' | ';
305 | }
306 |
307 | html += '' + format_date(date) + ' | ';
308 | html += '
';
309 |
310 | count++;
311 | }
312 | $('#users-table').html(html);
313 | $('#users-count').html(count);
314 | }).always(() => {
315 | // allow updates again
316 | users_updating = false;
317 | });
318 | }
319 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/files.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Mesh Chat
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
27 |
28 |
29 |
30 | Mesh Chat
31 |
32 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 | Updated:
46 |
47 | 0 secs ago
48 |
49 |
50 |
51 |
66 |
67 |
68 |
71 |
72 |
73 |
74 |
75 |
76 |
79 |
80 |
81 |
82 |
83 |
86 |
87 |
88 |
89 |
92 |
93 |
94 |
95 |
96 | File |
97 | Size |
98 | Node |
99 | Time |
100 | |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/files.js:
--------------------------------------------------------------------------------
1 | var last_update = epoch();
2 | var free_space = 0;
3 |
4 | function monitor_last_update() {
5 | var secs = epoch() - last_update;
6 | $('#last-update').html('Updated: ' + secs + ' seconds ago');
7 | }
8 |
9 | $(function() {
10 | load_files();
11 | setInterval(function() {
12 | load_files()
13 | }, 30000);
14 | setInterval(function() { monitor_last_update() }, 2500);
15 | var file = null;
16 | $('#upload-file').on("change", function(event) {
17 | file = event.target.files[0];
18 | console.log(event.target.files[0].size);
19 | if (event.target.files[0].size > free_space) {
20 | ohSnap('Not enough free space for your file, delete some files first and try again', 'red');
21 | $('#upload-file').val('');
22 | event.preventDefault();
23 | }
24 | });
25 | $('#download-messages').on('click', function(e) {
26 | e.preventDefault();
27 | location.href = '/cgi-bin/meshchat?action=messages_download';
28 | });
29 | $("#upload-button").on("click", function(event) {
30 | event.preventDefault();
31 | //$('#upload-form').submit();
32 | var file_data = new FormData();
33 | if (file == null) return;
34 | file_data.append('uploadfile', file);
35 | $.ajax({
36 | url: '/cgi-bin/meshchat?action=upload_file',
37 | type: "POST",
38 | data: file_data,
39 | dataType: "json",
40 | context: this,
41 | cache: false,
42 | processData: false,
43 | contentType: false,
44 | beforeSend: function() {
45 | $('progress').removeClass('hidden');
46 | },
47 | xhr: function() {
48 | var myXhr = $.ajaxSettings.xhr();
49 | if (myXhr.upload) {
50 | myXhr.upload.addEventListener('progress', upload_progress, false);
51 | }
52 | return myXhr;
53 | },
54 | success: function(data) {
55 | if (data.status == 200) {
56 | ohSnap('File uploaded', 'green');
57 | } else {
58 | ohSnap(data.response, 'red');
59 | }
60 | $('#upload-file').val('');
61 | load_files();
62 | },
63 | error: function(data, textStatus, errorThrown) {
64 | ohSnap('File upload error');
65 | },
66 | complete: function(jqXHR, textStatus) {
67 | $('progress').addClass('hidden');
68 | }
69 | });
70 | });
71 | });
72 |
73 | function upload_progress(event) {
74 | if (event.lengthComputable) {
75 | $('progress').attr({
76 | value: event.loaded,
77 | max: event.total
78 | });
79 | }
80 | }
81 |
82 | function fileNameCompare(a, b) {
83 | if (a.file < b.file)
84 | return -1;
85 | if (a.file > b.file)
86 | return 1;
87 | return 0;
88 | }
89 |
90 | function load_files() {
91 | $.getJSON('/cgi-bin/meshchat?action=files', function(data) {
92 | var html = '';
93 |
94 | data.files.sort(fileNameCompare);
95 |
96 | for (var i = 0; i < data.files.length; i++) {
97 | var date = new Date(0);
98 | date.setUTCSeconds(data.files[i].epoch);
99 | html += '';
100 | var port = '';
101 |
102 | //console.log(data);
103 |
104 | if (data.files[i].node.match(':')) {
105 | var parts = data.files[i].node.split(':');
106 | data.files[i].node = parts[0];
107 | port = ':' + parts[1];
108 | } else {
109 | if (data.files[i].platform == 'node') {
110 | port = ':8080'
111 | }
112 | }
113 | html += '' + data.files[i].file + ' | ';
114 | html += '' + numeral(data.files[i].size).format('0.0 b') + ' | ';
115 | html += '' + data.files[i].node + ' | ';
116 | html += '' + format_date(date) + ' | ';
117 | if (data.files[i].local == 1) {
118 | html += ' | ';
119 | } else {
120 | html += ' | ';
121 | }
122 | html += '
';
123 | }
124 | $('#files-table').html(html);
125 | $('#files-count').html(data.files.length + ' Files');
126 | $('#total-bytes').html('Total Storage: ' + numeral(data.stats.allowed).format('0.0 b'));
127 | $('#free-bytes').html('Free Storage: ' + numeral(data.stats.files_free).format('0.0 b'));
128 | free_space = data.stats.files_free;
129 | $(".delete-button").on("click", function(event) {
130 | event.preventDefault();
131 | $.ajax({
132 | url: '/cgi-bin/meshchat?action=delete_file&file=' + encodeURIComponent($(this).attr('file-name')),
133 | type: "GET",
134 | success: function(data) {
135 | ohSnap('File deleted', 'green');
136 | load_files();
137 | },
138 | error: function(data, textStatus, errorThrown) {
139 | ohSnap('File delete error: ' + data, 'red');
140 | }
141 | });
142 | });
143 |
144 | last_update = epoch();
145 | });
146 | }
147 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Mesh Chat
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
47 |
48 |
49 |
50 |
76 |
77 |
78 |
79 | Mesh Chat
80 |
81 |
82 |
83 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | Updated:
96 |
97 | 0 secs ago
98 |
99 |
100 |
101 |
102 |
108 |
109 |
110 |
111 |
114 |
115 |
134 |
135 |
136 |
137 |
138 |
139 |
142 |
143 |
144 |
145 |
146 |
147 | Call Sign
148 | |
149 |
150 | Node
151 | |
152 |
153 | Last Seen
154 | |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
184 |
185 |
186 |
205 |
214 |
215 | Loading messages.... |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
232 |
233 |
235 |
237 |
239 |
241 |
243 |
245 |
247 |
248 |
249 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/js.cookie.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * JavaScript Cookie v2.0.4
3 | * https://github.com/js-cookie/js-cookie
4 | *
5 | * Copyright 2006, 2015 Klaus Hartl & Fagner Brack
6 | * Released under the MIT license
7 | */
8 | (function (factory) {
9 | if (typeof define === 'function' && define.amd) {
10 | define(factory);
11 | } else if (typeof exports === 'object') {
12 | module.exports = factory();
13 | } else {
14 | var _OldCookies = window.Cookies;
15 | var api = window.Cookies = factory();
16 | api.noConflict = function () {
17 | window.Cookies = _OldCookies;
18 | return api;
19 | };
20 | }
21 | }(function () {
22 | function extend () {
23 | var i = 0;
24 | var result = {};
25 | for (; i < arguments.length; i++) {
26 | var attributes = arguments[ i ];
27 | for (var key in attributes) {
28 | result[key] = attributes[key];
29 | }
30 | }
31 | return result;
32 | }
33 |
34 | function init (converter) {
35 | function api (key, value, attributes) {
36 | var result;
37 |
38 | // Write
39 |
40 | if (arguments.length > 1) {
41 | attributes = extend({
42 | path: '/'
43 | }, api.defaults, attributes);
44 |
45 | if (typeof attributes.expires === 'number') {
46 | var expires = new Date();
47 | expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
48 | attributes.expires = expires;
49 | }
50 |
51 | try {
52 | result = JSON.stringify(value);
53 | if (/^[\{\[]/.test(result)) {
54 | value = result;
55 | }
56 | } catch (e) {}
57 |
58 | if (!converter.write) {
59 | value = encodeURIComponent(String(value))
60 | .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
61 | } else {
62 | value = converter.write(value, key);
63 | }
64 |
65 | key = encodeURIComponent(String(key));
66 | key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
67 | key = key.replace(/[\(\)]/g, escape);
68 |
69 | return (document.cookie = [
70 | key, '=', value,
71 | attributes.expires && '; expires=' + attributes.expires.toUTCString(), // use expires attribute, max-age is not supported by IE
72 | attributes.path && '; path=' + attributes.path,
73 | attributes.domain && '; domain=' + attributes.domain,
74 | attributes.secure ? '; secure' : ''
75 | ].join(''));
76 | }
77 |
78 | // Read
79 |
80 | if (!key) {
81 | result = {};
82 | }
83 |
84 | // To prevent the for loop in the first place assign an empty array
85 | // in case there are no cookies at all. Also prevents odd result when
86 | // calling "get()"
87 | var cookies = document.cookie ? document.cookie.split('; ') : [];
88 | var rdecode = /(%[0-9A-Z]{2})+/g;
89 | var i = 0;
90 |
91 | for (; i < cookies.length; i++) {
92 | var parts = cookies[i].split('=');
93 | var name = parts[0].replace(rdecode, decodeURIComponent);
94 | var cookie = parts.slice(1).join('=');
95 |
96 | if (cookie.charAt(0) === '"') {
97 | cookie = cookie.slice(1, -1);
98 | }
99 |
100 | try {
101 | cookie = converter.read ?
102 | converter.read(cookie, name) : converter(cookie, name) ||
103 | cookie.replace(rdecode, decodeURIComponent);
104 |
105 | if (this.json) {
106 | try {
107 | cookie = JSON.parse(cookie);
108 | } catch (e) {}
109 | }
110 |
111 | if (key === name) {
112 | result = cookie;
113 | break;
114 | }
115 |
116 | if (!key) {
117 | result[name] = cookie;
118 | }
119 | } catch (e) {}
120 | }
121 |
122 | return result;
123 | }
124 |
125 | api.get = api.set = api;
126 | api.getJSON = function () {
127 | return api.apply({
128 | json: true
129 | }, [].slice.call(arguments));
130 | };
131 | api.defaults = {};
132 |
133 | api.remove = function (key, attributes) {
134 | api(key, '', extend(attributes, {
135 | expires: -1
136 | }));
137 | };
138 |
139 | api.withConverter = init;
140 |
141 | return api;
142 | }
143 |
144 | return init(function () {});
145 | }));
146 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/md5.js:
--------------------------------------------------------------------------------
1 | /*
2 | MD5 code copyright (c) by Joseph Myers
3 | http://www.myersdaily.org/joseph/javascript/md5-text.html
4 | */
5 | function md5cycle(x, k) {
6 | var a = x[0], b = x[1], c = x[2], d = x[3];
7 |
8 | a = ff(a, b, c, d, k[0], 7, -680876936);
9 | d = ff(d, a, b, c, k[1], 12, -389564586);
10 | c = ff(c, d, a, b, k[2], 17, 606105819);
11 | b = ff(b, c, d, a, k[3], 22, -1044525330);
12 | a = ff(a, b, c, d, k[4], 7, -176418897);
13 | d = ff(d, a, b, c, k[5], 12, 1200080426);
14 | c = ff(c, d, a, b, k[6], 17, -1473231341);
15 | b = ff(b, c, d, a, k[7], 22, -45705983);
16 | a = ff(a, b, c, d, k[8], 7, 1770035416);
17 | d = ff(d, a, b, c, k[9], 12, -1958414417);
18 | c = ff(c, d, a, b, k[10], 17, -42063);
19 | b = ff(b, c, d, a, k[11], 22, -1990404162);
20 | a = ff(a, b, c, d, k[12], 7, 1804603682);
21 | d = ff(d, a, b, c, k[13], 12, -40341101);
22 | c = ff(c, d, a, b, k[14], 17, -1502002290);
23 | b = ff(b, c, d, a, k[15], 22, 1236535329);
24 |
25 | a = gg(a, b, c, d, k[1], 5, -165796510);
26 | d = gg(d, a, b, c, k[6], 9, -1069501632);
27 | c = gg(c, d, a, b, k[11], 14, 643717713);
28 | b = gg(b, c, d, a, k[0], 20, -373897302);
29 | a = gg(a, b, c, d, k[5], 5, -701558691);
30 | d = gg(d, a, b, c, k[10], 9, 38016083);
31 | c = gg(c, d, a, b, k[15], 14, -660478335);
32 | b = gg(b, c, d, a, k[4], 20, -405537848);
33 | a = gg(a, b, c, d, k[9], 5, 568446438);
34 | d = gg(d, a, b, c, k[14], 9, -1019803690);
35 | c = gg(c, d, a, b, k[3], 14, -187363961);
36 | b = gg(b, c, d, a, k[8], 20, 1163531501);
37 | a = gg(a, b, c, d, k[13], 5, -1444681467);
38 | d = gg(d, a, b, c, k[2], 9, -51403784);
39 | c = gg(c, d, a, b, k[7], 14, 1735328473);
40 | b = gg(b, c, d, a, k[12], 20, -1926607734);
41 |
42 | a = hh(a, b, c, d, k[5], 4, -378558);
43 | d = hh(d, a, b, c, k[8], 11, -2022574463);
44 | c = hh(c, d, a, b, k[11], 16, 1839030562);
45 | b = hh(b, c, d, a, k[14], 23, -35309556);
46 | a = hh(a, b, c, d, k[1], 4, -1530992060);
47 | d = hh(d, a, b, c, k[4], 11, 1272893353);
48 | c = hh(c, d, a, b, k[7], 16, -155497632);
49 | b = hh(b, c, d, a, k[10], 23, -1094730640);
50 | a = hh(a, b, c, d, k[13], 4, 681279174);
51 | d = hh(d, a, b, c, k[0], 11, -358537222);
52 | c = hh(c, d, a, b, k[3], 16, -722521979);
53 | b = hh(b, c, d, a, k[6], 23, 76029189);
54 | a = hh(a, b, c, d, k[9], 4, -640364487);
55 | d = hh(d, a, b, c, k[12], 11, -421815835);
56 | c = hh(c, d, a, b, k[15], 16, 530742520);
57 | b = hh(b, c, d, a, k[2], 23, -995338651);
58 |
59 | a = ii(a, b, c, d, k[0], 6, -198630844);
60 | d = ii(d, a, b, c, k[7], 10, 1126891415);
61 | c = ii(c, d, a, b, k[14], 15, -1416354905);
62 | b = ii(b, c, d, a, k[5], 21, -57434055);
63 | a = ii(a, b, c, d, k[12], 6, 1700485571);
64 | d = ii(d, a, b, c, k[3], 10, -1894986606);
65 | c = ii(c, d, a, b, k[10], 15, -1051523);
66 | b = ii(b, c, d, a, k[1], 21, -2054922799);
67 | a = ii(a, b, c, d, k[8], 6, 1873313359);
68 | d = ii(d, a, b, c, k[15], 10, -30611744);
69 | c = ii(c, d, a, b, k[6], 15, -1560198380);
70 | b = ii(b, c, d, a, k[13], 21, 1309151649);
71 | a = ii(a, b, c, d, k[4], 6, -145523070);
72 | d = ii(d, a, b, c, k[11], 10, -1120210379);
73 | c = ii(c, d, a, b, k[2], 15, 718787259);
74 | b = ii(b, c, d, a, k[9], 21, -343485551);
75 |
76 | x[0] = add32(a, x[0]);
77 | x[1] = add32(b, x[1]);
78 | x[2] = add32(c, x[2]);
79 | x[3] = add32(d, x[3]);
80 | }
81 |
82 | function cmn(q, a, b, x, s, t) {
83 | a = add32(add32(a, q), add32(x, t));
84 | return add32((a << s) | (a >>> (32 - s)), b);
85 | }
86 |
87 | function ff(a, b, c, d, x, s, t) {
88 | return cmn((b & c) | ((~b) & d), a, b, x, s, t);
89 | }
90 |
91 | function gg(a, b, c, d, x, s, t) {
92 | return cmn((b & d) | (c & (~d)), a, b, x, s, t);
93 | }
94 |
95 | function hh(a, b, c, d, x, s, t) {
96 | return cmn(b ^ c ^ d, a, b, x, s, t);
97 | }
98 |
99 | function ii(a, b, c, d, x, s, t) {
100 | return cmn(c ^ (b | (~d)), a, b, x, s, t);
101 | }
102 |
103 | function md51(s) {
104 | txt = '';
105 | var n = s.length,
106 | state = [1732584193, -271733879, -1732584194, 271733878], i;
107 |
108 | for (i=64; i<=s.length; i+=64) {
109 | md5cycle(state, md5blk(s.substring(i-64, i)));
110 | }
111 |
112 | s = s.substring(i-64);
113 | var tail = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0];
114 | for (i=0; i>2] |= s.charCodeAt(i) << ((i%4) << 3);
116 | tail[i>>2] |= 0x80 << ((i%4) << 3);
117 |
118 | if (i > 55) {
119 | md5cycle(state, tail);
120 | for (i=0; i<16; i++) tail[i] = 0;
121 | }
122 |
123 | tail[14] = n*8;
124 | md5cycle(state, tail);
125 | return state;
126 | }
127 |
128 | /* there needs to be support for Unicode here,
129 | * unless we pretend that we can redefine the MD-5
130 | * algorithm for multi-byte characters (perhaps
131 | * by adding every four 16-bit characters and
132 | * shortening the sum to 32 bits). Otherwise
133 | * I suggest performing MD-5 as if every character
134 | * was two bytes--e.g., 0040 0025 = @%--but then
135 | * how will an ordinary MD-5 sum be matched?
136 | * There is no way to standardize text to something
137 | * like UTF-8 before transformation; speed cost is
138 | * utterly prohibitive. The JavaScript standard
139 | * itself needs to look at this: it should start
140 | * providing access to strings as preformed UTF-8
141 | * 8-bit unsigned value arrays.
142 | */
143 | function md5blk(s) { /* I figured global was faster. */
144 | var md5blks = [], i; /* Andy King said do it this way. */
145 |
146 | for (i=0; i<64; i+=4) {
147 | md5blks[i>>2] = s.charCodeAt(i)
148 | + (s.charCodeAt(i+1) << 8)
149 | + (s.charCodeAt(i+2) << 16)
150 | + (s.charCodeAt(i+3) << 24);
151 | }
152 | return md5blks;
153 | }
154 |
155 | var hex_chr = '0123456789abcdef'.split('');
156 |
157 | function rhex(n) {
158 | var s='', j=0;
159 |
160 | for(; j<4; j++)
161 | s += hex_chr[(n >> (j * 8 + 4)) & 0x0F]
162 | + hex_chr[(n >> (j * 8)) & 0x0F];
163 | return s;
164 | }
165 |
166 | function hex(x) {
167 | for (var i=0; i> 16) + (y >> 16) + (lsw >> 16);
190 | return (msw << 16) | (lsw & 0xFFFF);
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/messages.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | // Messages is a singleton that keeps a copy of all messages
4 | class Messages {
5 |
6 | static NEW_MSG = 1;
7 | static CHAN_UPDATE = 2;
8 | static MSG_UPDATE = 3;
9 |
10 | constructor() {
11 | if (! this.__instance) {
12 | this.messages = new Map();
13 | this.message_order = new Array();
14 | this.delete_list = new Array(); // future enhancement
15 | this.db_version = 0;
16 | this.last_update_time = 0;
17 | this._updating = false;
18 | this._message_checksum = null; // only messages in channel
19 |
20 | this.__current_channel = "";
21 | this.__channels = new Array();
22 | this.__observers = new Array();
23 |
24 | this.__instance = this;
25 | }
26 | return this.__instance;
27 | }
28 |
29 | // return reference to singleton, creating if necessary
30 | getInstance() {
31 | if (! this.__instance) {
32 | this.__instance = new Messages();
33 | }
34 | return this.__instance;
35 | }
36 |
37 | /* check() retrieves the current message database version from the
38 | MeshChat server and compares it with the last known version.
39 | If the database version is different (i.e. database has new messages),
40 | then an update cycle is kicked off by calling fetch() */
41 | check() {
42 | console.debug("Messages.check()");
43 |
44 | var pending_db_version = 0;
45 |
46 | // currently updating, ignore this check
47 | if (this._updating == true) {
48 | console.debug("Message.check() skipped due to messages being updated.");
49 | return;
50 | }
51 |
52 | // lock out all other updates
53 | this._updating = true;
54 |
55 | $.getJSON('/cgi-bin/meshchat?action=messages_version_ui&call_sign=' + call_sign + '&id=' + meshchat_id + '&epoch=' + epoch(),
56 | (data) => {
57 | if (data == null || data == 0) {
58 | this._updating = false;
59 | } else if ('messages_version' in data && this.db_version != data.messages_version) {
60 | this.fetch(data.messages_version);
61 | } else {
62 | this._updating = false;
63 | }
64 | }).fail((error) => {
65 | // TODO error message on UI describing failure
66 | this._updating = false;
67 | });
68 | }
69 |
70 | /* fetch() is used to retrieve the messages from the message database.
71 | It is told the new database version with the pending_version param.
72 | All messages are then stored in the local message db (this.messages)
73 | and update() is called to update all the internal counters */
74 | fetch(pending_version) {
75 | console.debug("Messages.fetch(pending_version = " + pending_version + ")");
76 |
77 | $.getJSON('/cgi-bin/meshchat?action=messages&call_sign=' + call_sign + '&id=' + meshchat_id + '&epoch=' + epoch(),
78 | (data) => {
79 | if (data == null || data == 0) empty();
80 |
81 | // integrate new messages into the message DB
82 | data.forEach((entry) => { this.messages.set(entry.id, entry) });
83 |
84 | this.update();
85 | this.last_update_time = epoch();
86 | this.db_version = pending_version;
87 | this._updating = false;
88 | this.notify(Messages.MSG_UPDATE);
89 | this.notify(Messages.CHAN_UPDATE);
90 | }).fail((error) => {
91 | // TODO error message on UI describing failure
92 | this._updating = false;
93 | });
94 | }
95 |
96 | /* update the message DB with counts, channels, etc.
97 | If msg_ids is not specified, then process all messages in the
98 | DB */
99 | update(msg_ids=null) {
100 | console.debug("Messages.update(msg_ids=" + JSON.stringify(msg_ids) + " )");
101 |
102 | if (msg_ids === null) {
103 | msg_ids = Array.from(this.messages.keys());
104 | }
105 |
106 | for (var id of msg_ids.values()) {
107 | var message = this.messages.get(id);
108 |
109 | // if there is not a message don't try to process it.
110 | if (message === undefined) {
111 | // throw error message
112 | continue;
113 | }
114 |
115 | // null channel names is the Everything channel (empty string)
116 | if (message.channel === null) {
117 | message.channel = "";
118 | }
119 |
120 | // update list of available channels
121 | if (! this.__channels.includes(message.channel)) {
122 | this.__channels.push(message.channel);
123 | }
124 |
125 | // TODO not sure this is actually needed, get should be returning a reference
126 | this.messages.set(id, message);
127 | }
128 |
129 | // sort the messages by time (descending)
130 | this.message_order = Array.from(this.messages.keys()).sort(
131 | (a,b) => {
132 | let a_msg = this.messages.get(a);
133 | let b_msg = this.messages.get(b);
134 | return a_msg.epoch > b_msg.epoch ? -1 : 1;
135 | });
136 | }
137 |
138 | set_channel(chan) {
139 | console.debug("Messages.set_channel(chan=" + chan + ")");
140 | this.__current_channel = chan;
141 | this._message_checksum = null;
142 | }
143 |
144 | current_channel() {
145 | return this.__current_channel;
146 | }
147 |
148 | // return a list of channels available across all messages
149 | channels() {
150 | return Array.from(this.__channels.values());
151 | }
152 |
153 | send(message, channel, call_sign) {
154 | console.debug("Messages.send(message='" + message +"', channel=" + channel + ", call_sign=" + call_sign + ")");
155 | let params = {
156 | action: 'send_message',
157 | message: message,
158 | call_sign: call_sign,
159 | epoch: epoch(),
160 | id: this._create_id(),
161 | channel: channel
162 | };
163 |
164 | // { timeout: 5000, retryLimit: 3, dataType: "json", data: params}
165 | return new Promise((sent, error) => {
166 | $.post('/cgi-bin/meshchat', params,
167 | (data) => {
168 | if (data.status == 500) {
169 | error('Error sending message: ' + data.response);
170 | } else {
171 | // add the message to the in memory message DB
172 | this.messages.set(params['id'], {
173 | id: params['id'],
174 | message: message,
175 | call_sign: call_sign,
176 | epoch: params['epoch'],
177 | channel: channel,
178 | node: node_name(),
179 | platform: platform(),
180 | });
181 |
182 | // Add the channel to the list
183 | if (! channel in this.channels()) {
184 | this.__channels.push(channel);
185 | this.set_channel(channel);
186 | this.notify(Messages.CHAN_UPDATE);
187 | }
188 |
189 | // update internal message checksum with locally
190 | // created message ID so not to trigger alert sound
191 | this._message_checksum += parseInt(params['id'], 16);
192 | this.update();
193 | this.notify(Messages.MSG_UPDATE);
194 | sent();
195 | }
196 | }).fail((error) => {
197 | if (error == 'timeout') {
198 | this.tryCount++;
199 | if (this.tryCount <= this.retryLimit) {
200 | //try again
201 | $.ajax(this);
202 | return;
203 | }
204 | error(error);
205 | }
206 | });
207 | })
208 |
209 | }
210 |
211 | // return a rendered version of a block of messages
212 | render(channel, search_filter) {
213 | console.debug("Messages.render(channel=" + channel + ", search_filter=" + search_filter + ")");
214 | let html = '';
215 | let search = search_filter.toLowerCase();
216 | let message_checksum = 0;
217 |
218 | for (var id of this.message_order) {
219 | var message = this.messages.get(id);
220 |
221 | // calculate the date for the current message
222 | let date = new Date(0);
223 | date.setUTCSeconds(message.epoch);
224 | message.date = date;
225 |
226 | if (search != '') {
227 | //console.log(search);
228 | //console.log(message.toLowerCase());
229 | if (message.message.toLowerCase().search(search) == -1 &&
230 | message.call_sign.toLowerCase().search(search) == -1 &&
231 | message.node.toLowerCase().search(search) == -1 &&
232 | format_date(date).toLowerCase().search(search) == -1) {
233 | continue;
234 | }
235 | }
236 |
237 | if (channel == message.channel || this.__current_channel == '') {
238 | html += this.render_row(message);
239 |
240 | // add this message to the checksum
241 | message_checksum += parseInt(message.id, 16);
242 | }
243 | }
244 |
245 | // provide a message if no messages were found
246 | if (html == "") {
247 | html = "No messages found |
";
248 | }
249 |
250 | // this._message_checksum == null is the first rendering of the
251 | // message table. No need to sound an alert.
252 | if (this._message_checksum != null && message_checksum != this._message_checksum) {
253 | this.notify(Messages.NEW_MSG);
254 | }
255 | this._message_checksum = message_checksum;
256 |
257 | return html;
258 | }
259 |
260 | render_row(msg_data) {
261 | let message = msg_data.message.replace(/(\r\n|\n|\r)/g, "
");
262 |
263 | let row = '';
264 | if (false) {
265 | row += '' + msg_data.id + ' | ';
266 | }
267 | row += '' + format_date(msg_data.date) + ' | ';
268 | row += '' + message + ' | ';
269 | row += '' + msg_data.call_sign + ' | ';
270 | row += '' + msg_data.channel + ' | ';
271 | if (msg_data.platform == 'node') {
272 | row += '' + msg_data.node + ' | ';
273 | } else {
274 | row += '' + msg_data.node + ' | ';
275 | }
276 | row += '
';
277 |
278 | return row;
279 | }
280 |
281 | // generate unique message IDs
282 | _create_id() {
283 | let seed = epoch().toString() + Math.floor(Math.random() * 99999);
284 | let hash = md5(seed);
285 | return hash.substring(0,8);
286 | }
287 |
288 | // Observer functions
289 | subscribe(func) {
290 | console.debug("Messages.subscribe(func=" + func.name + ")");
291 | this.__observers.push(func);
292 | }
293 |
294 | unsubscribe(func) {
295 | console.debug("Messages.unsubscribe(func=" + func + ")");
296 | this.__observers = this.__observers.filter((observer) => observer !== func);
297 | }
298 |
299 | notify(reason) {
300 | console.debug("Messages.notify(reason=" + reason + ")");
301 | this.__observers.forEach((observer) => observer(reason));
302 | }
303 | }
304 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */
2 |
3 | /**
4 | * 1. Set default font family to sans-serif.
5 | * 2. Prevent iOS text size adjust after orientation change, without disabling
6 | * user zoom.
7 | */
8 |
9 | html {
10 | font-family: sans-serif; /* 1 */
11 | -ms-text-size-adjust: 100%; /* 2 */
12 | -webkit-text-size-adjust: 100%; /* 2 */
13 | }
14 |
15 | /**
16 | * Remove default margin.
17 | */
18 |
19 | body {
20 | margin: 0;
21 | }
22 |
23 | /* HTML5 display definitions
24 | ========================================================================== */
25 |
26 | /**
27 | * Correct `block` display not defined for any HTML5 element in IE 8/9.
28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11
29 | * and Firefox.
30 | * Correct `block` display not defined for `main` in IE 11.
31 | */
32 |
33 | article,
34 | aside,
35 | details,
36 | figcaption,
37 | figure,
38 | footer,
39 | header,
40 | hgroup,
41 | main,
42 | menu,
43 | nav,
44 | section,
45 | summary {
46 | display: block;
47 | }
48 |
49 | /**
50 | * 1. Correct `inline-block` display not defined in IE 8/9.
51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
52 | */
53 |
54 | audio,
55 | canvas,
56 | progress,
57 | video {
58 | display: inline-block; /* 1 */
59 | vertical-align: baseline; /* 2 */
60 | }
61 |
62 | /**
63 | * Prevent modern browsers from displaying `audio` without controls.
64 | * Remove excess height in iOS 5 devices.
65 | */
66 |
67 | audio:not([controls]) {
68 | display: none;
69 | height: 0;
70 | }
71 |
72 | /**
73 | * Address `[hidden]` styling not present in IE 8/9/10.
74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
75 | */
76 |
77 | [hidden],
78 | template {
79 | display: none;
80 | }
81 |
82 | /* Links
83 | ========================================================================== */
84 |
85 | /**
86 | * Remove the gray background color from active links in IE 10.
87 | */
88 |
89 | a {
90 | background-color: transparent;
91 | }
92 |
93 | /**
94 | * Improve readability when focused and also mouse hovered in all browsers.
95 | */
96 |
97 | a:active,
98 | a:hover {
99 | outline: 0;
100 | }
101 |
102 | /* Text-level semantics
103 | ========================================================================== */
104 |
105 | /**
106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome.
107 | */
108 |
109 | abbr[title] {
110 | border-bottom: 1px dotted;
111 | }
112 |
113 | /**
114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
115 | */
116 |
117 | b,
118 | strong {
119 | font-weight: bold;
120 | }
121 |
122 | /**
123 | * Address styling not present in Safari and Chrome.
124 | */
125 |
126 | dfn {
127 | font-style: italic;
128 | }
129 |
130 | /**
131 | * Address variable `h1` font-size and margin within `section` and `article`
132 | * contexts in Firefox 4+, Safari, and Chrome.
133 | */
134 |
135 | h1 {
136 | font-size: 2em;
137 | margin: 0.67em 0;
138 | }
139 |
140 | /**
141 | * Address styling not present in IE 8/9.
142 | */
143 |
144 | mark {
145 | background: #ff0;
146 | color: #000;
147 | }
148 |
149 | /**
150 | * Address inconsistent and variable font size in all browsers.
151 | */
152 |
153 | small {
154 | font-size: 80%;
155 | }
156 |
157 | /**
158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers.
159 | */
160 |
161 | sub,
162 | sup {
163 | font-size: 75%;
164 | line-height: 0;
165 | position: relative;
166 | vertical-align: baseline;
167 | }
168 |
169 | sup {
170 | top: -0.5em;
171 | }
172 |
173 | sub {
174 | bottom: -0.25em;
175 | }
176 |
177 | /* Embedded content
178 | ========================================================================== */
179 |
180 | /**
181 | * Remove border when inside `a` element in IE 8/9/10.
182 | */
183 |
184 | img {
185 | border: 0;
186 | }
187 |
188 | /**
189 | * Correct overflow not hidden in IE 9/10/11.
190 | */
191 |
192 | svg:not(:root) {
193 | overflow: hidden;
194 | }
195 |
196 | /* Grouping content
197 | ========================================================================== */
198 |
199 | /**
200 | * Address margin not present in IE 8/9 and Safari.
201 | */
202 |
203 | figure {
204 | margin: 1em 40px;
205 | }
206 |
207 | /**
208 | * Address differences between Firefox and other browsers.
209 | */
210 |
211 | hr {
212 | -moz-box-sizing: content-box;
213 | box-sizing: content-box;
214 | height: 0;
215 | }
216 |
217 | /**
218 | * Contain overflow in all browsers.
219 | */
220 |
221 | pre {
222 | overflow: auto;
223 | }
224 |
225 | /**
226 | * Address odd `em`-unit font size rendering in all browsers.
227 | */
228 |
229 | code,
230 | kbd,
231 | pre,
232 | samp {
233 | font-family: monospace, monospace;
234 | font-size: 1em;
235 | }
236 |
237 | /* Forms
238 | ========================================================================== */
239 |
240 | /**
241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited
242 | * styling of `select`, unless a `border` property is set.
243 | */
244 |
245 | /**
246 | * 1. Correct color not being inherited.
247 | * Known issue: affects color of disabled elements.
248 | * 2. Correct font properties not being inherited.
249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
250 | */
251 |
252 | button,
253 | input,
254 | optgroup,
255 | select,
256 | textarea {
257 | color: inherit; /* 1 */
258 | font: inherit; /* 2 */
259 | margin: 0; /* 3 */
260 | }
261 |
262 | /**
263 | * Address `overflow` set to `hidden` in IE 8/9/10/11.
264 | */
265 |
266 | button {
267 | overflow: visible;
268 | }
269 |
270 | /**
271 | * Address inconsistent `text-transform` inheritance for `button` and `select`.
272 | * All other form control elements do not inherit `text-transform` values.
273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
274 | * Correct `select` style inheritance in Firefox.
275 | */
276 |
277 | button,
278 | select {
279 | text-transform: none;
280 | }
281 |
282 | /**
283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
284 | * and `video` controls.
285 | * 2. Correct inability to style clickable `input` types in iOS.
286 | * 3. Improve usability and consistency of cursor style between image-type
287 | * `input` and others.
288 | */
289 |
290 | button,
291 | html input[type="button"], /* 1 */
292 | input[type="reset"],
293 | input[type="submit"] {
294 | -webkit-appearance: button; /* 2 */
295 | cursor: pointer; /* 3 */
296 | }
297 |
298 | /**
299 | * Re-set default cursor for disabled elements.
300 | */
301 |
302 | button[disabled],
303 | html input[disabled] {
304 | cursor: default;
305 | }
306 |
307 | /**
308 | * Remove inner padding and border in Firefox 4+.
309 | */
310 |
311 | button::-moz-focus-inner,
312 | input::-moz-focus-inner {
313 | border: 0;
314 | padding: 0;
315 | }
316 |
317 | /**
318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in
319 | * the UA stylesheet.
320 | */
321 |
322 | input {
323 | line-height: normal;
324 | }
325 |
326 | /**
327 | * It's recommended that you don't attempt to style these elements.
328 | * Firefox's implementation doesn't respect box-sizing, padding, or width.
329 | *
330 | * 1. Address box sizing set to `content-box` in IE 8/9/10.
331 | * 2. Remove excess padding in IE 8/9/10.
332 | */
333 |
334 | input[type="checkbox"],
335 | input[type="radio"] {
336 | box-sizing: border-box; /* 1 */
337 | padding: 0; /* 2 */
338 | }
339 |
340 | /**
341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain
342 | * `font-size` values of the `input`, it causes the cursor style of the
343 | * decrement button to change from `default` to `text`.
344 | */
345 |
346 | input[type="number"]::-webkit-inner-spin-button,
347 | input[type="number"]::-webkit-outer-spin-button {
348 | height: auto;
349 | }
350 |
351 | /**
352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome.
353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome
354 | * (include `-moz` to future-proof).
355 | */
356 |
357 | input[type="search"] {
358 | -webkit-appearance: textfield; /* 1 */
359 | -moz-box-sizing: content-box;
360 | -webkit-box-sizing: content-box; /* 2 */
361 | box-sizing: content-box;
362 | }
363 |
364 | /**
365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X.
366 | * Safari (but not Chrome) clips the cancel button when the search input has
367 | * padding (and `textfield` appearance).
368 | */
369 |
370 | input[type="search"]::-webkit-search-cancel-button,
371 | input[type="search"]::-webkit-search-decoration {
372 | -webkit-appearance: none;
373 | }
374 |
375 | /**
376 | * Define consistent border, margin, and padding.
377 | */
378 |
379 | fieldset {
380 | border: 1px solid #c0c0c0;
381 | margin: 0 2px;
382 | padding: 0.35em 0.625em 0.75em;
383 | }
384 |
385 | /**
386 | * 1. Correct `color` not being inherited in IE 8/9/10/11.
387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets.
388 | */
389 |
390 | legend {
391 | border: 0; /* 1 */
392 | padding: 0; /* 2 */
393 | }
394 |
395 | /**
396 | * Remove default vertical scrollbar in IE 8/9/10/11.
397 | */
398 |
399 | textarea {
400 | overflow: auto;
401 | }
402 |
403 | /**
404 | * Don't inherit the `font-weight` (applied by a rule above).
405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
406 | */
407 |
408 | optgroup {
409 | font-weight: bold;
410 | }
411 |
412 | /* Tables
413 | ========================================================================== */
414 |
415 | /**
416 | * Remove most spacing between table cells.
417 | */
418 |
419 | table {
420 | border-collapse: collapse;
421 | border-spacing: 0;
422 | }
423 |
424 | td,
425 | th {
426 | padding: 0;
427 | }
--------------------------------------------------------------------------------
/src/data/www/meshchat/numeral.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * numeral.js
3 | * version : 1.5.3
4 | * author : Adam Draper
5 | * license : MIT
6 | * http://adamwdraper.github.com/Numeral-js/
7 | */
8 | (function(){function a(a){this._value=a}function b(a,b,c,d){var e,f,g=Math.pow(10,b);return f=(c(a*g)/g).toFixed(b),d&&(e=new RegExp("0{1,"+d+"}$"),f=f.replace(e,"")),f}function c(a,b,c){var d;return d=b.indexOf("$")>-1?e(a,b,c):b.indexOf("%")>-1?f(a,b,c):b.indexOf(":")>-1?g(a,b):i(a._value,b,c)}function d(a,b){var c,d,e,f,g,i=b,j=["KB","MB","GB","TB","PB","EB","ZB","YB"],k=!1;if(b.indexOf(":")>-1)a._value=h(b);else if(b===q)a._value=0;else{for("."!==o[p].delimiters.decimal&&(b=b.replace(/\./g,"").replace(o[p].delimiters.decimal,".")),c=new RegExp("[^a-zA-Z]"+o[p].abbreviations.thousand+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),d=new RegExp("[^a-zA-Z]"+o[p].abbreviations.million+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),e=new RegExp("[^a-zA-Z]"+o[p].abbreviations.billion+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),f=new RegExp("[^a-zA-Z]"+o[p].abbreviations.trillion+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),g=0;g<=j.length&&!(k=b.indexOf(j[g])>-1?Math.pow(1024,g+1):!1);g++);a._value=(k?k:1)*(i.match(c)?Math.pow(10,3):1)*(i.match(d)?Math.pow(10,6):1)*(i.match(e)?Math.pow(10,9):1)*(i.match(f)?Math.pow(10,12):1)*(b.indexOf("%")>-1?.01:1)*((b.split("-").length+Math.min(b.split("(").length-1,b.split(")").length-1))%2?1:-1)*Number(b.replace(/[^0-9\.]+/g,"")),a._value=k?Math.ceil(a._value):a._value}return a._value}function e(a,b,c){var d,e,f=b.indexOf("$"),g=b.indexOf("("),h=b.indexOf("-"),j="";return b.indexOf(" $")>-1?(j=" ",b=b.replace(" $","")):b.indexOf("$ ")>-1?(j=" ",b=b.replace("$ ","")):b=b.replace("$",""),e=i(a._value,b,c),1>=f?e.indexOf("(")>-1||e.indexOf("-")>-1?(e=e.split(""),d=1,(g>f||h>f)&&(d=0),e.splice(d,0,o[p].currency.symbol+j),e=e.join("")):e=o[p].currency.symbol+j+e:e.indexOf(")")>-1?(e=e.split(""),e.splice(-1,0,j+o[p].currency.symbol),e=e.join("")):e=e+j+o[p].currency.symbol,e}function f(a,b,c){var d,e="",f=100*a._value;return b.indexOf(" %")>-1?(e=" ",b=b.replace(" %","")):b=b.replace("%",""),d=i(f,b,c),d.indexOf(")")>-1?(d=d.split(""),d.splice(-1,0,e+"%"),d=d.join("")):d=d+e+"%",d}function g(a){var b=Math.floor(a._value/60/60),c=Math.floor((a._value-60*b*60)/60),d=Math.round(a._value-60*b*60-60*c);return b+":"+(10>c?"0"+c:c)+":"+(10>d?"0"+d:d)}function h(a){var b=a.split(":"),c=0;return 3===b.length?(c+=60*Number(b[0])*60,c+=60*Number(b[1]),c+=Number(b[2])):2===b.length&&(c+=60*Number(b[0]),c+=Number(b[1])),Number(c)}function i(a,c,d){var e,f,g,h,i,j,k=!1,l=!1,m=!1,n="",r=!1,s=!1,t=!1,u=!1,v=!1,w="",x="",y=Math.abs(a),z=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],A="",B=!1;if(0===a&&null!==q)return q;if(c.indexOf("(")>-1?(k=!0,c=c.slice(1,-1)):c.indexOf("+")>-1&&(l=!0,c=c.replace(/\+/g,"")),c.indexOf("a")>-1&&(r=c.indexOf("aK")>=0,s=c.indexOf("aM")>=0,t=c.indexOf("aB")>=0,u=c.indexOf("aT")>=0,v=r||s||t||u,c.indexOf(" a")>-1?(n=" ",c=c.replace(" a","")):c=c.replace("a",""),y>=Math.pow(10,12)&&!v||u?(n+=o[p].abbreviations.trillion,a/=Math.pow(10,12)):y=Math.pow(10,9)&&!v||t?(n+=o[p].abbreviations.billion,a/=Math.pow(10,9)):y=Math.pow(10,6)&&!v||s?(n+=o[p].abbreviations.million,a/=Math.pow(10,6)):(y=Math.pow(10,3)&&!v||r)&&(n+=o[p].abbreviations.thousand,a/=Math.pow(10,3))),c.indexOf("b")>-1)for(c.indexOf(" b")>-1?(w=" ",c=c.replace(" b","")):c=c.replace("b",""),g=0;g<=z.length;g++)if(e=Math.pow(1024,g),f=Math.pow(1024,g+1),a>=e&&f>a){w+=z[g],e>0&&(a/=e);break}return c.indexOf("o")>-1&&(c.indexOf(" o")>-1?(x=" ",c=c.replace(" o","")):c=c.replace("o",""),x+=o[p].ordinal(a)),c.indexOf("[.]")>-1&&(m=!0,c=c.replace("[.]",".")),h=a.toString().split(".")[0],i=c.split(".")[1],j=c.indexOf(","),i?(i.indexOf("[")>-1?(i=i.replace("]",""),i=i.split("["),A=b(a,i[0].length+i[1].length,d,i[1].length)):A=b(a,i.length,d),h=A.split(".")[0],A=A.split(".")[1].length?o[p].delimiters.decimal+A.split(".")[1]:"",m&&0===Number(A.slice(1))&&(A="")):h=b(a,null,d),h.indexOf("-")>-1&&(h=h.slice(1),B=!0),j>-1&&(h=h.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g,"$1"+o[p].delimiters.thousands)),0===c.indexOf(".")&&(h=""),(k&&B?"(":"")+(!k&&B?"-":"")+(!B&&l?"+":"")+h+A+(x?x:"")+(n?n:"")+(w?w:"")+(k&&B?")":"")}function j(a,b){o[a]=b}function k(a){var b=a.toString().split(".");return b.length<2?1:Math.pow(10,b[1].length)}function l(){var a=Array.prototype.slice.call(arguments);return a.reduce(function(a,b){var c=k(a),d=k(b);return c>d?c:d},-1/0)}var m,n="1.5.3",o={},p="en",q=null,r="0,0",s="undefined"!=typeof module&&module.exports;m=function(b){return m.isNumeral(b)?b=b.value():0===b||"undefined"==typeof b?b=0:Number(b)||(b=m.fn.unformat(b)),new a(Number(b))},m.version=n,m.isNumeral=function(b){return b instanceof a},m.language=function(a,b){if(!a)return p;if(a&&!b){if(!o[a])throw new Error("Unknown language : "+a);p=a}return(b||!o[a])&&j(a,b),m},m.languageData=function(a){if(!a)return o[p];if(!o[a])throw new Error("Unknown language : "+a);return o[a]},m.language("en",{delimiters:{thousands:",",decimal:"."},abbreviations:{thousand:"k",million:"m",billion:"b",trillion:"t"},ordinal:function(a){var b=a%10;return 1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th"},currency:{symbol:"$"}}),m.zeroFormat=function(a){q="string"==typeof a?a:null},m.defaultFormat=function(a){r="string"==typeof a?a:"0.0"},"function"!=typeof Array.prototype.reduce&&(Array.prototype.reduce=function(a,b){"use strict";if(null===this||"undefined"==typeof this)throw new TypeError("Array.prototype.reduce called on null or undefined");if("function"!=typeof a)throw new TypeError(a+" is not a function");var c,d,e=this.length>>>0,f=!1;for(1c;++c)this.hasOwnProperty(c)&&(f?d=a(d,this[c],c,this):(d=this[c],f=!0));if(!f)throw new TypeError("Reduce of empty array with no initial value");return d}),m.fn=a.prototype={clone:function(){return m(this)},format:function(a,b){return c(this,a?a:r,void 0!==b?b:Math.round)},unformat:function(a){return"[object Number]"===Object.prototype.toString.call(a)?a:d(this,a?a:r)},value:function(){return this._value},valueOf:function(){return this._value},set:function(a){return this._value=Number(a),this},add:function(a){function b(a,b){return a+c*b}var c=l.call(null,this._value,a);return this._value=[this._value,a].reduce(b,0)/c,this},subtract:function(a){function b(a,b){return a-c*b}var c=l.call(null,this._value,a);return this._value=[a].reduce(b,this._value*c)/c,this},multiply:function(a){function b(a,b){var c=l(a,b);return a*c*b*c/(c*c)}return this._value=[this._value,a].reduce(b,1),this},divide:function(a){function b(a,b){var c=l(a,b);return a*c/(b*c)}return this._value=[this._value,a].reduce(b),this},difference:function(a){return Math.abs(m(this._value).subtract(a).value())}},s&&(module.exports=m),"undefined"==typeof ender&&(this.numeral=m),"function"==typeof define&&define.amd&&define([],function(){return m})}).call(this);
--------------------------------------------------------------------------------
/src/data/www/meshchat/ohsnap.js:
--------------------------------------------------------------------------------
1 | /**
2 | * == OhSnap!.js ==
3 | * A simple jQuery/Zepto notification library designed to be used in mobile apps
4 | *
5 | * author: Justin Domingue
6 | * date: september 18, 2015
7 | * version: 0.1.4
8 | * copyright - nice copyright over here
9 | */
10 |
11 | /* Shows a toast on the page
12 | * Params:
13 | * text: text to show
14 | * color: color of the toast. one of red, green, blue, orange, yellow or custom
15 | */
16 | function ohSnap(text, color, icon) {
17 | var icon_markup = "",
18 | html,
19 | time = '5000',
20 | $container = $('#ohsnap');
21 |
22 | if (icon) {
23 | icon_markup = " ";
24 | }
25 |
26 | // Generate the HTML
27 | html = $('' + icon_markup + text + '
').fadeIn('fast');
28 |
29 | // Append the label to the container
30 | $container.append(html);
31 |
32 | // Remove the notification on click
33 | html.on('click', function() {
34 | ohSnapX($(this));
35 | });
36 |
37 | // After 'time' seconds, the animation fades out
38 | setTimeout(function() {
39 | ohSnapX(html);
40 | }, time);
41 | }
42 |
43 | function ohSnapX(element) {
44 | // Called without argument, the function removes all alerts
45 | // element must be a jQuery object
46 |
47 | if (typeof element !== "undefined") {
48 | element.fadeOut('fast', function() {
49 | $(this).remove();
50 | });
51 | } else {
52 | $('.alert').fadeOut('fast', function() {
53 | $(this).remove();
54 | });
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/shared.js:
--------------------------------------------------------------------------------
1 | var config;
2 |
3 | $(function() {
4 | $('#logout').on('click', function(e){
5 | e.preventDefault();
6 | Cookies.remove('meshchat_call_sign');
7 | window.location = '/meshchat';
8 | });
9 | });
10 |
11 | function node_name() {
12 | return config.node;
13 | }
14 |
15 | function platform() {
16 | return config.platform || 'node'; // TODO temp patch until config API is updated
17 | }
18 |
19 | function epoch() {
20 | return Math.floor(new Date() / 1000);
21 | }
22 |
23 | function format_date(date) {
24 | var string;
25 |
26 | var year = String(date.getFullYear());
27 |
28 | string = (date.getMonth()+1) + '/' + date.getDate() + '/' + year.slice(-2);
29 | string += '
';
30 |
31 | var hours = date.getHours();
32 | var minutes = date.getMinutes();
33 | var ampm = hours >= 12 ? 'PM' : 'AM';
34 |
35 | hours = hours % 12;
36 | hours = hours ? hours : 12; // the hour '0' should be '12'
37 | minutes = minutes < 10 ? '0'+minutes : minutes;
38 |
39 | string += hours + ':' + minutes + ' ' + ampm;
40 |
41 | return string;
42 | }
43 |
44 | function make_id()
45 | {
46 | var text = "";
47 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
48 |
49 | for( var i=0; i < 5; i++ )
50 | text += possible.charAt(Math.floor(Math.random() * possible.length));
51 |
52 | return text;
53 | }
54 |
55 | function aredn_domain(host) {
56 | if (host.indexOf(".") !== -1) {
57 | return host;
58 | }
59 | host = host.split(":")
60 | return host[0] + ".local.mesh" + (host[1] ? ":" + host[1] : "");
61 | }
62 |
63 | function debug(msg) {
64 | context.debug && console.debug(msg);
65 | }
66 |
67 | function error(msg) {
68 | console.error(msg);
69 | }
70 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/skeleton.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Skeleton V2.0.4
3 | * Copyright 2014, Dave Gamache
4 | * www.getskeleton.com
5 | * Free to use under the MIT license.
6 | * http://www.opensource.org/licenses/mit-license.php
7 | * 12/29/2014
8 | */
9 |
10 |
11 | /* Table of contents
12 | ––––––––––––––––––––––––––––––––––––––––––––––––––
13 | - Grid
14 | - Base Styles
15 | - Typography
16 | - Links
17 | - Buttons
18 | - Forms
19 | - Lists
20 | - Code
21 | - Tables
22 | - Spacing
23 | - Utilities
24 | - Clearing
25 | - Media Queries
26 | */
27 |
28 |
29 | /* Grid
30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
31 | .container {
32 | position: relative;
33 | width: 100%;
34 | max-width: 960px;
35 | margin: 0 auto;
36 | padding: 0 20px;
37 | box-sizing: border-box; }
38 | .column,
39 | .columns {
40 | width: 100%;
41 | float: left;
42 | box-sizing: border-box; }
43 |
44 | /* For devices larger than 400px */
45 | @media (min-width: 400px) {
46 | .container {
47 | width: 85%;
48 | padding: 0; }
49 | }
50 |
51 | /* For devices larger than 550px */
52 | @media (min-width: 550px) {
53 | .container {
54 | width: 80%; }
55 | .column,
56 | .columns {
57 | margin-left: 4%; }
58 | .column:first-child,
59 | .columns:first-child {
60 | margin-left: 0; }
61 |
62 | .one.column,
63 | .one.columns { width: 4.66666666667%; }
64 | .two.columns { width: 13.3333333333%; }
65 | .three.columns { width: 22%; }
66 | .four.columns { width: 30.6666666667%; }
67 | .five.columns { width: 39.3333333333%; }
68 | .six.columns { width: 48%; }
69 | .seven.columns { width: 56.6666666667%; }
70 | .eight.columns { width: 65.3333333333%; }
71 | .nine.columns { width: 74.0%; }
72 | .ten.columns { width: 82.6666666667%; }
73 | .eleven.columns { width: 91.3333333333%; }
74 | .twelve.columns { width: 100%; margin-left: 0; }
75 |
76 | .one-third.column { width: 30.6666666667%; }
77 | .two-thirds.column { width: 65.3333333333%; }
78 |
79 | .one-half.column { width: 48%; }
80 |
81 | /* Offsets */
82 | .offset-by-one.column,
83 | .offset-by-one.columns { margin-left: 8.66666666667%; }
84 | .offset-by-two.column,
85 | .offset-by-two.columns { margin-left: 17.3333333333%; }
86 | .offset-by-three.column,
87 | .offset-by-three.columns { margin-left: 26%; }
88 | .offset-by-four.column,
89 | .offset-by-four.columns { margin-left: 34.6666666667%; }
90 | .offset-by-five.column,
91 | .offset-by-five.columns { margin-left: 43.3333333333%; }
92 | .offset-by-six.column,
93 | .offset-by-six.columns { margin-left: 52%; }
94 | .offset-by-seven.column,
95 | .offset-by-seven.columns { margin-left: 60.6666666667%; }
96 | .offset-by-eight.column,
97 | .offset-by-eight.columns { margin-left: 69.3333333333%; }
98 | .offset-by-nine.column,
99 | .offset-by-nine.columns { margin-left: 78.0%; }
100 | .offset-by-ten.column,
101 | .offset-by-ten.columns { margin-left: 86.6666666667%; }
102 | .offset-by-eleven.column,
103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; }
104 |
105 | .offset-by-one-third.column,
106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; }
107 | .offset-by-two-thirds.column,
108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; }
109 |
110 | .offset-by-one-half.column,
111 | .offset-by-one-half.columns { margin-left: 52%; }
112 |
113 | }
114 |
115 |
116 | /* Base Styles
117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
118 | /* NOTE
119 | html is set to 62.5% so that all the REM measurements throughout Skeleton
120 | are based on 10px sizing. So basically 1.5rem = 15px :) */
121 | html {
122 | font-size: 62.5%; }
123 | body {
124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */
125 | line-height: 1.6;
126 | font-weight: 400;
127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
128 | color: #222; }
129 |
130 |
131 | /* Typography
132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
133 | h1, h2, h3, h4, h5, h6 {
134 | margin-top: 0;
135 | margin-bottom: 2rem;
136 | font-weight: 300; }
137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;}
138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; }
139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; }
140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; }
141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; }
142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; }
143 |
144 | /* Larger than phablet */
145 | @media (min-width: 550px) {
146 | h1 { font-size: 5.0rem; }
147 | h2 { font-size: 4.2rem; }
148 | h3 { font-size: 3.6rem; }
149 | h4 { font-size: 3.0rem; }
150 | h5 { font-size: 2.4rem; }
151 | h6 { font-size: 1.5rem; }
152 | }
153 |
154 | p {
155 | margin-top: 0; }
156 |
157 |
158 | /* Links
159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
160 | a {
161 | color: #1EAEDB; }
162 | a:hover {
163 | color: #0FA0CE; }
164 |
165 |
166 | /* Buttons
167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
168 | .button,
169 | button,
170 | input[type="submit"],
171 | input[type="reset"],
172 | input[type="button"] {
173 | display: inline-block;
174 | height: 38px;
175 | padding: 0 30px;
176 | color: #555;
177 | text-align: center;
178 | font-size: 11px;
179 | font-weight: 600;
180 | line-height: 38px;
181 | letter-spacing: .1rem;
182 | text-transform: uppercase;
183 | text-decoration: none;
184 | white-space: nowrap;
185 | background-color: transparent;
186 | border-radius: 4px;
187 | border: 1px solid #bbb;
188 | cursor: pointer;
189 | box-sizing: border-box; }
190 | .button:hover,
191 | button:hover,
192 | input[type="submit"]:hover,
193 | input[type="reset"]:hover,
194 | input[type="button"]:hover,
195 | .button:focus,
196 | button:focus,
197 | input[type="submit"]:focus,
198 | input[type="reset"]:focus,
199 | input[type="button"]:focus {
200 | color: #333;
201 | border-color: #888;
202 | outline: 0; }
203 | .button.button-primary,
204 | button.button-primary,
205 | input[type="submit"].button-primary,
206 | input[type="reset"].button-primary,
207 | input[type="button"].button-primary {
208 | color: #FFF;
209 | background-color: #33C3F0;
210 | border-color: #33C3F0; }
211 | .button.button-primary:hover,
212 | button.button-primary:hover,
213 | input[type="submit"].button-primary:hover,
214 | input[type="reset"].button-primary:hover,
215 | input[type="button"].button-primary:hover,
216 | .button.button-primary:focus,
217 | button.button-primary:focus,
218 | input[type="submit"].button-primary:focus,
219 | input[type="reset"].button-primary:focus,
220 | input[type="button"].button-primary:focus {
221 | color: #FFF;
222 | background-color: #1EAEDB;
223 | border-color: #1EAEDB; }
224 |
225 |
226 | /* Forms
227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
228 | input[type="email"],
229 | input[type="number"],
230 | input[type="search"],
231 | input[type="text"],
232 | input[type="tel"],
233 | input[type="url"],
234 | input[type="password"],
235 | textarea,
236 | select {
237 | height: 38px;
238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
239 | background-color: #fff;
240 | border: 1px solid #D1D1D1;
241 | border-radius: 4px;
242 | box-shadow: none;
243 | box-sizing: border-box; }
244 | /* Removes awkward default styles on some inputs for iOS */
245 | input[type="email"],
246 | input[type="number"],
247 | input[type="search"],
248 | input[type="text"],
249 | input[type="tel"],
250 | input[type="url"],
251 | input[type="password"],
252 | textarea {
253 | -webkit-appearance: none;
254 | -moz-appearance: none;
255 | appearance: none; }
256 | textarea {
257 | min-height: 65px;
258 | padding-top: 6px;
259 | padding-bottom: 6px; }
260 | input[type="email"]:focus,
261 | input[type="number"]:focus,
262 | input[type="search"]:focus,
263 | input[type="text"]:focus,
264 | input[type="tel"]:focus,
265 | input[type="url"]:focus,
266 | input[type="password"]:focus,
267 | textarea:focus,
268 | select:focus {
269 | border: 1px solid #33C3F0;
270 | outline: 0; }
271 | label,
272 | legend {
273 | display: block;
274 | margin-bottom: .5rem;
275 | font-weight: 600; }
276 | fieldset {
277 | padding: 0;
278 | border-width: 0; }
279 | input[type="checkbox"],
280 | input[type="radio"] {
281 | display: inline; }
282 | label > .label-body {
283 | display: inline-block;
284 | margin-left: .5rem;
285 | font-weight: normal; }
286 |
287 |
288 | /* Lists
289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
290 | ul {
291 | list-style: circle inside; }
292 | ol {
293 | list-style: decimal inside; }
294 | ol, ul {
295 | padding-left: 0;
296 | margin-top: 0; }
297 | ul ul,
298 | ul ol,
299 | ol ol,
300 | ol ul {
301 | margin: 1.5rem 0 1.5rem 3rem;
302 | font-size: 90%; }
303 | li {
304 | margin-bottom: 1rem; }
305 |
306 |
307 | /* Code
308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
309 | code {
310 | padding: .2rem .5rem;
311 | margin: 0 .2rem;
312 | font-size: 90%;
313 | white-space: nowrap;
314 | background: #F1F1F1;
315 | border: 1px solid #E1E1E1;
316 | border-radius: 4px; }
317 | pre > code {
318 | display: block;
319 | padding: 1rem 1.5rem;
320 | white-space: pre; }
321 |
322 |
323 | /* Tables
324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
325 | th,
326 | td {
327 | padding: 12px 15px;
328 | text-align: left;
329 | border-bottom: 1px solid #E1E1E1; }
330 | th:first-child,
331 | td:first-child {
332 | padding-left: 0; }
333 | th:last-child,
334 | td:last-child {
335 | padding-right: 0; }
336 |
337 |
338 | /* Spacing
339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
340 | button,
341 | .button {
342 | margin-bottom: 1rem; }
343 | input,
344 | textarea,
345 | select,
346 | fieldset {
347 | margin-bottom: 1.5rem; }
348 | pre,
349 | blockquote,
350 | dl,
351 | figure,
352 | table,
353 | p,
354 | ul,
355 | ol,
356 | form {
357 | margin-bottom: 2.5rem; }
358 |
359 |
360 | /* Utilities
361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
362 | .u-full-width {
363 | width: 100%;
364 | box-sizing: border-box; }
365 | .u-max-full-width {
366 | max-width: 100%;
367 | box-sizing: border-box; }
368 | .u-pull-right {
369 | float: right; }
370 | .u-pull-left {
371 | float: left; }
372 |
373 |
374 | /* Misc
375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
376 | hr {
377 | margin-top: 3rem;
378 | margin-bottom: 3.5rem;
379 | border-width: 0;
380 | border-top: 1px solid #E1E1E1; }
381 |
382 |
383 | /* Clearing
384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
385 |
386 | /* Self Clearing Goodness */
387 | .container:after,
388 | .row:after,
389 | .u-cf {
390 | content: "";
391 | display: table;
392 | clear: both; }
393 |
394 |
395 | /* Media Queries
396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */
397 | /*
398 | Note: The best way to structure the use of media queries is to create the queries
399 | near the relevant code. For example, if you wanted to change the styles for buttons
400 | on small devices, paste the mobile query code up in the buttons section and style it
401 | there.
402 | */
403 |
404 |
405 | /* Larger than mobile */
406 | @media (min-width: 400px) {}
407 |
408 | /* Larger than phablet (also point when grid becomes active) */
409 | @media (min-width: 550px) {}
410 |
411 | /* Larger than tablet */
412 | @media (min-width: 750px) {}
413 |
414 | /* Larger than desktop */
415 | @media (min-width: 1000px) {}
416 |
417 | /* Larger than Desktop HD */
418 | @media (min-width: 1200px) {}
419 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/status.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Mesh Chat
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
27 |
28 |
29 |
30 | Mesh Chat
31 |
32 |
33 |
34 |
40 |
41 |
42 |
43 |
44 |
45 | Updated:
46 |
47 | 0 secs ago
48 |
49 |
50 |
51 |
52 |
53 |
56 |
57 |
58 |
59 |
60 | Node |
61 | Last Sync |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
94 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/status.js:
--------------------------------------------------------------------------------
1 | var last_update = epoch();
2 |
3 | $(function() {
4 | load_status();
5 | setInterval(function(){ load_status() }, 5000);
6 | setInterval(function() { monitor_last_update() }, 2500);
7 | });
8 |
9 | function monitor_last_update() {
10 | var secs = epoch() - last_update;
11 | $('#last-update').html('Updated: ' + secs + ' seconds ago');
12 | }
13 |
14 | function load_status() {
15 | $.getJSON('/cgi-bin/meshchat?action=sync_status', function(data) {
16 | var html = '';
17 | var count = 0;
18 |
19 | for (var i = 0; i < data.length; i++) {
20 | var date = new Date(0);
21 | date.setUTCSeconds(data[i].epoch);
22 |
23 | //if ((epoch() - data[i].epoch) > 60 * 60) continue;
24 |
25 | html += '';
26 | html += '' + data[i].node + ' | ';
27 | html += '' + format_date(date) + ' | ';
28 | html += '
';
29 |
30 | count++;
31 | }
32 |
33 | $('#sync-table').html(html);
34 | $('#sync-count').html(count);
35 |
36 | last_update = epoch();
37 | });
38 |
39 | $.getJSON('/cgi-bin/meshchat?action=action_log', function(data) {
40 | var html = '';
41 |
42 | for (var i = 0; i < data.length; i++) {
43 | var date = new Date(0);
44 | date.setUTCSeconds(data[i].action_epoch);
45 |
46 | html += '';
47 | html += '' + format_date(date) + ' | ';
48 | html += '' + data[i].script + ' | ';
49 | html += '' + data[i].result + ' | ';
50 | html += '' + data[i].message + ' | ';
51 | html += '
';
52 | }
53 |
54 | $('#log-table').html(html);
55 |
56 | last_update = epoch();
57 | });
58 | }
59 |
--------------------------------------------------------------------------------
/src/data/www/meshchat/style.css:
--------------------------------------------------------------------------------
1 | body{ margin: 0; }
2 | input {
3 | -webkit-appearance: none;
4 | border-radius: 0;
5 | }
6 |
7 | #call-sign {
8 | text-transform: uppercase;
9 | }
10 |
11 | #copyright {
12 | margin-top: 10px;
13 | }
14 |
15 | .pull-right {
16 | float: right;
17 | }
18 |
19 | .alert {
20 | padding: 15px;
21 | margin-bottom: 20px;
22 | border: 1px solid #eed3d7;
23 | border-radius: 4px;
24 | }
25 |
26 | .alert-red {
27 | color: white;
28 | background-color: #DA4453;
29 | }
30 |
31 | .alert-green {
32 | color: white;
33 | background-color: #37BC9B;
34 | }
35 |
36 | .alert-blue {
37 | color: white;
38 | background-color: #4A89DC;
39 | }
40 |
41 | .alert-yellow {
42 | color: white;
43 | background-color: #F6BB42;
44 | }
45 |
46 | .alert-orange {
47 | color: white;
48 | background-color: #E9573F;
49 | }
50 |
51 | td:first-child {
52 | white-space: nowrap;
53 | }
54 |
55 | .hidden {
56 | display: none;
57 | }
58 |
59 | .navbar {
60 | border-top-width: 0;
61 | border-bottom: 1px solid #eee;
62 | }
63 |
64 | .navbar, .navbar-spacer {
65 | display: block;
66 | width: 100%;
67 | height: 6.5rem;
68 | background: #fff;
69 | z-index: 99;
70 | margin-bottom: 0px;
71 | }
72 |
73 | .navbar-spacer {
74 | display: none;
75 | }
76 |
77 | .navbar > .container {
78 | width: 100%;
79 | padding: 0px;
80 | }
81 |
82 | .navbar-list {
83 | list-style: none;
84 | margin-bottom: 0;
85 | }
86 |
87 | .navbar-item {
88 | position: relative;
89 | float: left;
90 | margin-bottom: 0;
91 | }
92 |
93 | .navbar-link {
94 | text-transform: uppercase;
95 | font-size: 12px;
96 | font-weight: 600;
97 | margin-right: 15px;
98 | text-decoration: none;
99 | line-height: 6.5rem;
100 | color: #222;
101 | }
102 |
103 | .navbar-link.active {
104 | color: #33C3F0;
105 | }
106 |
107 | .has-docked-nav .navbar {
108 | position: fixed;
109 | top: 0;
110 | left: 0;
111 | }
112 |
113 | .has-docked-nav .navbar-spacer {
114 | display: block;
115 | }
116 |
117 |
118 | /* Re-overiding the width 100% declaration to match size of % based container */
119 |
120 | .has-docked-nav .navbar > .container {
121 | width: 80%;
122 | }
123 |
124 | .grey-background {
125 | background-color: grey;
126 | }
127 |
128 | progress {
129 | color: #337ab7;
130 | width: 100%;
131 | }
132 |
133 | #message {
134 | min-height: 65px;
135 | font-size: 1em;
136 | }
137 |
138 | #message-parent-table {
139 | table-layout: fixed;
140 | width: 100%;
141 | }
142 |
143 | video {
144 | width: 100% !important;
145 | height: auto !important;
146 | }
147 |
148 | body {
149 | font-size: 1.2em;
150 | }
151 |
152 | th, td {
153 | padding-top: 5px;
154 | padding-bottom: 5px;
155 | padding-right: 0px;
156 | }
157 |
158 | table {
159 | border-collapse: collapse;
160 | }
161 |
162 | table td {
163 | word-wrap: break-word;
164 | overflow-wrap: break-word;
165 | }
166 |
167 | textarea:focus {
168 | border: 1px solid #337ab7;
169 | }
170 |
171 | textarea {
172 | resize: none;
173 | }
174 |
175 | button.button-primary:hover {
176 | background-color: #337ab7;
177 | border-color: #337ab7;
178 | }
179 |
180 | .button, button, input[type="submit"], input[type="reset"], input[type="button"]{ line-height: 1; }
181 |
182 | .center {
183 | text-align: center;
184 | }
185 |
186 | .right {
187 | text-align: right;
188 | }
189 |
190 | .info {
191 | margin-top: 5px;
192 | }
193 |
194 | .updated {
195 | margin-bottom: 5px;
196 | }
197 |
198 | #send-channel-label {
199 | display: inline;
200 | }
201 |
202 | #send-channel {
203 | margin-right: 10px;
204 | max-width: 250px;
205 | }
206 |
207 | #channels {
208 | vertical-align: top;
209 | margin-top: -5px;
210 | max-width: 250px;
211 | }
212 |
213 | #search {
214 | vertical-align: top;
215 | margin-top: -5px;
216 | width: 200px;
217 | }
218 |
219 | .panel {
220 | border-color: #337ab7;
221 | box-shadow: none;
222 | border: 1px solid #337ab7;
223 | border-radius: 5px;
224 | margin-bottom: 20px;
225 | }
226 |
227 | .panel-header {
228 | background-image: linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);
229 | color: #fff;
230 | font-size: 18px;
231 | padding: 10px;
232 | }
233 |
234 | .panel-body {
235 | padding: 15px;
236 | padding-top: 0px;
237 | }
238 |
239 | .file-panel {
240 | height: 110px;
241 | }
242 |
243 | #files-parent-table {
244 | table-layout: fixed;
245 | }
246 |
247 | button.button-primary {
248 | background-color: #337ab7;
249 | border-color: #337ab7;
250 | }
251 |
252 | button, .button {
253 | margin-bottom: 0px;
254 | }
255 |
256 | input[type="submit"].button-primary {
257 | background-color: #337ab7;
258 | border-color: #337ab7;
259 | }
260 |
261 | input[type="submit"].button-primary:hover {
262 | background-color: #337ab7;
263 | border-color: #337ab7;
264 | }
265 |
266 | a {
267 | color: #337ab7;
268 | }
269 |
270 | select {
271 | height: 35px;
272 | }
273 |
274 | .users-panel {
275 | min-height: 220px;
276 | overflow: hidden;
277 | }
278 |
279 | .send-message-panel {
280 | min-height: 220px;
281 | }
282 |
283 | .users-panel-body {
284 | overflow-y: auto;
285 | max-height: 160px;
286 | }
287 |
288 | #new-message-label {
289 | margin-top: 5px;
290 | }
291 |
292 | textarea {
293 | margin-bottom: 10px;
294 | }
295 |
296 | form {
297 | margin-bottom: 0px;
298 | }
299 |
300 | .loading {
301 | border: 4px solid #FFF;
302 | border-top-color: transparent;
303 | border-left-color: transparent;
304 | width: 20px;
305 | height: 20px;
306 | //opacity: 0.8;
307 | border-radius: 50%;
308 | animation: loading 0.7s infinite linear;
309 | -webkit-animation: loading 0.7s infinite linear;
310 | }
311 |
312 | .message-panel-collapse {
313 | height: inherit;
314 | }
315 |
316 | .message-panel-body-collapse {
317 | display: none;
318 | }
319 |
320 | .users-panel-collapse {
321 | height: inherit;
322 | }
323 |
324 | .users-panel-body-collapse {
325 | display: none;
326 | }
327 |
328 | #users-expand {
329 | cursor: pointer;
330 | }
331 |
332 | #message-expand {
333 | cursor: pointer;
334 | }
335 |
336 | th, td{ padding-right: 10px; padding-left: 10px; }
337 | .message td:nth-child(1),
338 | .message th:nth-child(1){ width: 10%; }
339 | .message td:nth-child(2),
340 | .message th:nth-child(2){ width: 50%; }
341 | .message td:nth-child(3),
342 | .message th:nth-child(3){ width: 12%; }
343 | .message td:nth-child(4),
344 | .message th:nth-child(4){ width: 16%; }
345 | .message td:nth-child(5),
346 | .message th:nth-child(5){ width: 12%; }
347 | .message td { word-wrap: break-word; }
348 |
349 | @media (max-width: 1000px) {
350 | .users-panel {
351 | height: 260px;
352 | }
353 |
354 | .send-message-panel {
355 | height: 260px;
356 | }
357 |
358 | .users-panel-body {
359 | max-height: 200px;
360 | }
361 |
362 | .delete-button {
363 | display: none;
364 | }
365 | }
366 |
367 | @media (max-width: 820px) {
368 | .has-docked-nav .navbar > .container,
369 | .container{ width: 100%; padding: 0 10px; }
370 | .logout .navbar-link{ margin-right: 0; }
371 | th, td{ padding-right: 10px; padding-left: 10px; }
372 | }
373 | @media (max-width: 767px) {
374 | th, td{ padding-right: 4px; padding-left: 4px; }
375 |
376 | th.col_node, td.col_node, th.col_channel, td.col_channel, th.col_time, td.col_time {
377 | display:none;
378 | width:0;
379 | height:0;
380 | opacity:0;
381 | visibility: collapse;
382 | }
383 |
384 | .message td:nth-child(1),
385 | .message th:nth-child(1){ width: 20%; }
386 | .message td:nth-child(2),
387 | .message th:nth-child(2){ width: 55%; }
388 | .message td:nth-child(3),
389 | .message th:nth-child(3){ width: 25%; }
390 | .message td:nth-child(4),
391 | .message th:nth-child(4){ width: 0%; }
392 |
393 | body {
394 | font-size: 1.3em;
395 | }
396 |
397 | .users-panel {
398 | height: 260px;
399 | }
400 |
401 | .users-panel-body {
402 | max-height: 200px;
403 | }
404 |
405 | .send-message-panel {
406 | height: 260px;
407 | }
408 | }
409 |
410 | @media (max-width: 600px) {
411 | #search-span {
412 | display: none;
413 | }
414 |
415 | #files-parent-table {
416 | font-size: 1.0em;
417 | }
418 |
419 | #files-parent-table td:first-child {
420 | width: 80%;
421 | }
422 | }
423 |
424 | @media (max-width: 460px) {
425 | #files-parent-table {
426 | table-layout: auto;
427 | font-size: 1.0em;
428 | }
429 | }
430 |
431 | @media (min-width: 400px) {
432 | th, td {
433 | padding-top: 5px;
434 | padding-right: 15px;
435 | }
436 |
437 | body {
438 | font-size: 1.5em;
439 | }
440 | .logout {
441 | float: right;
442 | }
443 | .navbar-link {
444 | font-size: 13px;
445 | text-transform: uppercase;
446 | font-weight: 600;
447 | letter-spacing: .2rem;
448 | margin-right: 35px;
449 | text-decoration: none;
450 | line-height: 6.5rem;
451 | color: #222;
452 | }
453 | .last-updated {
454 | /*float: right;*/
455 | margin-top: 12px;
456 | }
457 | }
458 |
459 | .col_time {
460 | width: 70px;
461 | max-width: 70px;
462 | }
463 |
464 | @keyframes loading {
465 | from {
466 | transform: rotate(0deg);
467 | }
468 | to {
469 | transform: rotate(360deg);
470 | }
471 | }
472 | @-webkit-keyframes loading {
473 | from {
474 | -webkit-transform: rotate(0deg);
475 | }
476 | to {
477 | -webkit-transform: rotate(360deg);
478 | }
479 | }
480 |
--------------------------------------------------------------------------------