diff --git a/Makefile b/Makefile
index d337c71..894574b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
BUILDDIR?=./build
-PROJS?=grs info blog os doc ci
+PROJS?=grs info blog os doc ci talk
copyfiles = index.ls mimes.json
main: copy
for f in $(PROJS); do BUILDDIR=$(BUILDDIR)/"$${f}" make -C "$${f}" ; done
diff --git a/blog/views/default/layout.ls b/blog/views/default/layout.ls
index cce6170..99334b5 100644
--- a/blog/views/default/layout.ls
+++ b/blog/views/default/layout.ls
@@ -21,6 +21,10 @@
+
+
+
+
@@ -54,6 +58,15 @@
hljs.highlightBlock(block);
hljs.lineNumbersBlock(block);
});
+ // comment
+
+ var options = {
+ target: "quick_talk_comment_thread",
+ api_uri: "https://chat.iohub.dev/comment",
+ uri: "=url?>",
+ page: $("#desktop")[0]
+ };
+ new QuickTalk(options);
});
window.twttr = (function(d, s, id) {
diff --git a/blog/views/default/post/detail.ls b/blog/views/default/post/detail.ls
index acc511e..79ed4e4 100644
--- a/blog/views/default/post/detail.ls
+++ b/blog/views/default/post/detail.ls
@@ -52,44 +52,10 @@
end
echo("")
end?>
-
-
-
-
-
-
+
Comments
+
+ The comment editor supports Markdown document format. Your email is necessary to notify you of further updates on the discussion. It will be hidden from the public.
+
+
diff --git a/silk/BaseModel.lua b/silk/BaseModel.lua
index bf7e6a8..f26b304 100644
--- a/silk/BaseModel.lua
+++ b/silk/BaseModel.lua
@@ -1,52 +1,51 @@
-
-- create class
BaseObject:subclass("BaseModel", {registry = {}})
function BaseModel:initialize()
self.db = self.registry.db
- if self.db and self.name and self.name ~= "" and self.fields and not self.db:available(self.name) then
+ if self.db and self.name and self.name ~= "" and self.fields and
+ not self.db:available(self.name) then
self.db:createTable(self.name, self.fields)
end
end
function BaseModel:create(m)
- if self.db and m then
- return self.db:insert(self.name,m)
- end
+ if self.db and m then return self.db:insert(self.name, m) end
return false
end
function BaseModel:update(m)
- if self.db and m then
- return self.db:update(self.name,m)
- end
+ if self.db and m then return self.db:update(self.name, m) end
return false
end
function BaseModel:delete(cond)
- if self.db and cond then
- return self.db:delete(self.name,cond)
- end
+ if self.db and cond then return self.db:delete(self.name, cond) end
return false
end
-
function BaseModel:find(cond)
- if self.db and cond then
- return self.db:find(self.name, cond)
- end
+ if self.db and cond then return self.db:find(self.name, cond) end
return false
end
function BaseModel:get(id)
- local data, order = self:find({exp = {["="] = { id = id}} })
+ local data, order = self:find({exp = {["="] = {id = id}}})
if not data or #order == 0 then return false end
return data[1]
end
function BaseModel:findAll()
- if self.db then
- return self.db:getAll(self.name)
- end
+ if self.db then return self.db:getAll(self.name) end
return false
-end
\ No newline at end of file
+end
+
+function BaseModel:query(sql)
+ if self.db then return self.db:query(sql) end
+ return false
+end
+
+function BaseModel:select(sel, sql_cnd)
+ if self.db then return self.db:select(self.name, sel, sql_cnd) end
+ return nil
+end
diff --git a/silk/DBHelper.lua b/silk/DBHelper.lua
index bd41bf3..5ee9c3f 100644
--- a/silk/DBHelper.lua
+++ b/silk/DBHelper.lua
@@ -2,148 +2,143 @@ sqlite = modules.sqlite()
if sqlite == nil then return 0 end
-- create class
- BaseObject:subclass("DBHelper",{db={}})
+BaseObject:subclass("DBHelper", {db = {}})
function DBHelper:createTable(tbl, m)
- if self:available(tbl) then return true end
- local sql = "CREATE TABLE "..tbl.."(id INTEGER PRIMARY KEY"
- for k, v in pairs(m) do
- if k ~= "id" then
- sql = sql..","..k.." "..v
- end
- end
- sql = sql..");"
- return sqlite.query(self.db,sql) == 1
+ if self:available(tbl) then return true end
+ local sql = "CREATE TABLE " .. tbl .. "(id INTEGER PRIMARY KEY"
+ for k, v in pairs(m) do
+ if k ~= "id" then sql = sql .. "," .. k .. " " .. v end
+ end
+ sql = sql .. ");"
+ return sqlite.query(self.db, sql) == 1
end
function DBHelper:insert(tbl, m)
- local keys = {}
- local values = {}
- for k,v in pairs(m) do
- if k ~= "id" then
- table.insert(keys,k)
- if type(v) == "number" then
- table.insert(values, v)
- else
- local t = "\""..v:gsub('"', '""').."\""
- table.insert(values,t)
- end
- end
- end
- local sql = "INSERT INTO "..tbl.." ("..table.concat(keys,',')..') VALUES ('
- sql = sql..table.concat(values,',')..');'
- return sqlite.query(self.db, sql) == 1
+ local keys = {}
+ local values = {}
+ for k, v in pairs(m) do
+ if k ~= "id" then
+ table.insert(keys, k)
+ if type(v) == "number" then
+ table.insert(values, v)
+ else
+ local t = "\"" .. v:gsub('"', '""') .. "\""
+ table.insert(values, t)
+ end
+ end
+ end
+ local sql = "INSERT INTO " .. tbl .. " (" .. table.concat(keys, ',') ..
+ ') VALUES ('
+ sql = sql .. table.concat(values, ',') .. ');'
+ return sqlite.query(self.db, sql) == 1
end
function DBHelper:get(tbl, id)
- return sqlite.select(self.db, tbl, "*","id="..id)[1]
+ return sqlite.select(self.db, tbl, "*", "id=" .. id)[1]
end
function DBHelper:getAll(tbl)
- local data = sqlite.select(self.db, tbl, "*", "1=1")
- if data == nil then return nil end
- local a = {}
- for n in pairs(data) do table.insert(a, n) end
- table.sort(a)
- return data, a
+ local data = sqlite.select(self.db, tbl, "*", "1=1")
+ if data == nil then return nil end
+ local a = {}
+ for n in pairs(data) do table.insert(a, n) end
+ table.sort(a)
+ return data, a
end
function DBHelper:find(tbl, cond)
- local cnd = "1=1"
- local sel = "*"
- if cond.exp then
- cnd = self:gencond(cond.exp)
- end
- if cond.order then
- cnd = cnd.." ORDER BY "
- local l = {}
- local i = 1
- for k,v in pairs(cond.order) do
- l[i] = k.." "..v
- i = i+1
- end
- cnd = cnd..table.concat(l, ",")
- end
- if cond.limit then
- cnd = cnd.." LIMIT "..cond.limit
- end
- if cond.fields then
- sel = table.concat(cond.fields, ",")
- --print(sel)
- end
- local data = sqlite.select(self.db, tbl, sel, cnd)
- if data == nil then return nil end
- local a = {}
- for n in pairs(data) do table.insert(a, n) end
- table.sort(a)
- return data, a
+ local cnd = "1=1"
+ local sel = "*"
+ if cond.exp then cnd = self:gencond(cond.exp) end
+ if cond.order then
+ cnd = cnd .. " ORDER BY "
+ local l = {}
+ local i = 1
+ for k, v in pairs(cond.order) do
+ l[i] = k .. " " .. v
+ i = i + 1
+ end
+ cnd = cnd .. table.concat(l, ",")
+ end
+ if cond.limit then cnd = cnd .. " LIMIT " .. cond.limit end
+ if cond.fields then
+ sel = table.concat(cond.fields, ",")
+ -- print(sel)
+ end
+ local data = sqlite.select(self.db, tbl, sel, cnd)
+ if data == nil then return nil end
+ local a = {}
+ for n in pairs(data) do table.insert(a, n) end
+ table.sort(a)
+ return data, a
end
-function DBHelper:query(sql)
- return sqlite.query(self.db, sql) == 1
+function DBHelper:select(tbl, sel, cnd)
+ local data = sqlite.select(self.db, tbl, sel, cnd)
+ if data == nil then return nil end
+ local a = {}
+ for n in pairs(data) do table.insert(a, n) end
+ table.sort(a)
+ return data, a
end
+function DBHelper:query(sql) return sqlite.query(self.db, sql) == 1 end
+
function DBHelper:update(tbl, m)
- local id = m['id']
- if id ~= nil then
- local lst = {}
- for k,v in pairs(m) do
- if(type(v)== "number") then
- table.insert(lst,k.."="..v)
- else
- table.insert(lst,k.."=\""..v:gsub('"', '""').."\"")
- end
- end
- local sql = "UPDATE "..tbl.." SET "..table.concat(lst,",").." WHERE id="..id..";"
- return sqlite.query(self.db, sql) == 1
- end
- return false
+ local id = m['id']
+ if id ~= nil then
+ local lst = {}
+ for k, v in pairs(m) do
+ if (type(v) == "number") then
+ table.insert(lst, k .. "=" .. v)
+ else
+ table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"")
+ end
+ end
+ local sql = "UPDATE " .. tbl .. " SET " .. table.concat(lst, ",") ..
+ " WHERE id=" .. id .. ";"
+ return sqlite.query(self.db, sql) == 1
+ end
+ return false
end
-function DBHelper:available(tbl)
- return sqlite.hasTable(self.db, tbl) == 1
-end
-function DBHelper:deleteByID(tbl,id)
- local sql = "DELETE FROM "..tbl.." WHERE id="..id..";"
- return sqlite.query(self.db, sql) == 1
+function DBHelper:available(tbl) return sqlite.hasTable(self.db, tbl) == 1 end
+function DBHelper:deleteByID(tbl, id)
+ local sql = "DELETE FROM " .. tbl .. " WHERE id=" .. id .. ";"
+ return sqlite.query(self.db, sql) == 1
end
function DBHelper:gencond(o)
- for k,v in pairs(o) do
- if k == "and" or k == "or" then
- local cnd = {}
- local i = 1
- for k1,v1 in pairs(v) do
- cnd[i] = self:gencond(v1)
- i = i + 1
- end
- return " ("..table.concat(cnd, " "..k.." ")..") "
- else
- for k1,v1 in pairs(v) do
- local t = type(v1)
- if(t == "string") then
- return " ("..k1.." "..k..' "'..v1:gsub('"','""')..'") '
- end
- return " ("..k1.." "..k.." "..v1..") "
- end
- end
- end
+ for k, v in pairs(o) do
+ if k == "and" or k == "or" then
+ local cnd = {}
+ local i = 1
+ for k1, v1 in pairs(v) do
+ cnd[i] = self:gencond(v1)
+ i = i + 1
+ end
+ return " (" .. table.concat(cnd, " " .. k .. " ") .. ") "
+ else
+ for k1, v1 in pairs(v) do
+ local t = type(v1)
+ if (t == "string") then
+ return
+ " (" .. k1 .. " " .. k .. ' "' .. v1:gsub('"', '""') ..
+ '") '
+ end
+ return " (" .. k1 .. " " .. k .. " " .. v1 .. ") "
+ end
+ end
+ end
end
function DBHelper:delete(tbl, cond)
- local sql = "DELETE FROM "..tbl.." WHERE "..self:gencond(cond)..";"
- return sqlite.query(self.db, sql) == 1
+ local sql = "DELETE FROM " .. tbl .. " WHERE " .. self:gencond(cond) .. ";"
+ return sqlite.query(self.db, sql) == 1
end
-function DBHelper:lastInsertID()
- return sqlite.lastInsertID(self.db)
-end
+function DBHelper:lastInsertID() return sqlite.lastInsertID(self.db) end
-function DBHelper:close()
- if self.db then
- sqlite.dbclose(self.db)
- end
-end
+function DBHelper:close() if self.db then sqlite.dbclose(self.db) end end
function DBHelper:open()
- if self.db ~= nil then
- self.db = sqlite.getdb(self.db)
- end
-end
\ No newline at end of file
+ if self.db ~= nil then self.db = sqlite.getdb(self.db) end
+end
diff --git a/talk/Makefile b/talk/Makefile
new file mode 100644
index 0000000..a740c56
--- /dev/null
+++ b/talk/Makefile
@@ -0,0 +1,6 @@
+copyfiles = router.lua models controllers assets
+
+main:
+ - mkdir -p $(BUILDDIR)
+ cp -rvf $(copyfiles) $(BUILDDIR)
+ - mkdir -p $(BUILDDIR)/log
\ No newline at end of file
diff --git a/talk/assets/quicktalk.css b/talk/assets/quicktalk.css
new file mode 100644
index 0000000..a311c23
--- /dev/null
+++ b/talk/assets/quicktalk.css
@@ -0,0 +1,122 @@
+.quick-talk-compose {
+ display: block;
+ border: 1px solid #e5e5e5;
+ padding: 10px;
+}
+
+.quick-talk-compose input {
+ display: block;
+ width: 100%;
+ border: 0;
+ outline: 0;
+ font-style: italic;
+ border-bottom: 1px solid #e5e5e5;
+ font-size: 13px;
+ color: #878887;
+}
+
+.quick-talk-compose textarea {
+ display: block;
+ width: 100%;
+ min-height: 100px;
+ border: 0;
+ padding: 0;
+ outline: none;
+ margin: 0;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 13px;
+ color: #2c2c2c;
+}
+
+.quick-talk-compose .quick-talk-preview {
+ margin-top: 10px;
+ margin-bottom: 10px;
+}
+.quick-talk-compose .quick-talk-compose-footer {
+ display: flex;
+ flex-direction: row;
+}
+
+.quick-talk-compose .quick-talk-compose-footer div {
+ display: block;
+ flex: 1;
+ color: orangered;
+ font-style: italic;
+}
+
+.quick-talk-compose .quick-talk-compose-footer button,
+.quick-talk-compose-button {
+ color: white;
+ background-color: steelblue;
+ padding: 5px;
+ font-weight: bold;
+ border: 1px solid #e5e5e5;
+ outline: 0;
+ border-radius: 5px;
+ margin-left: 3px;
+}
+
+.quick-talk-compose-button {
+ margin-bottom: 10px;
+ margin-top: 10px;
+}
+.quick-talk-comment-thread {
+ display: block;
+}
+.quick-talk-comment-thread .quick-talk-comment {
+ margin-top: 10px;
+}
+
+.quick-talk-comment-thread .quick-talk-sub-comment {
+ margin-top: 10px;
+ margin-left: 20px;
+}
+
+.quick-talk-comment-thread .quick-talk-comment-header {
+ border-top: 1px solid #e5e5e5;
+ display: flex;
+ padding-top: 10px;
+ width: 100%;
+ flex-direction: row;
+}
+.quick-talk-comment-thread .quick-talk-comment-footer {
+ display: block;
+ margin: 0;
+ margin-left: 20px;
+ margin-bottom: 10px;
+ padding: 0;
+}
+.quick-talk-comment-thread .quick-talk-comment-footer span {
+ display: block;
+ color: steelblue;
+ cursor: pointer;
+ /* text-align: right; */
+ width: 100%;
+}
+.quick-talk-comment-thread .quick-talk-comment-footer span:hover {
+ text-decoration: underline;
+}
+.quick-talk-comment-thread .quick-talk-comment-user {
+ font-weight: bold;
+ color: steelblue;
+ padding-left: 3px;
+ padding-right: 3px;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ border-radius: 5px;
+ background-color: #e1ecf4;
+}
+
+.quick-talk-comment-thread .quick-talk-comment-time {
+ font-style: italic;
+ color: steelblue;
+ margin-left: 5px;
+ padding-top: 1px;
+ font-size: 13px;
+}
+.quick-talk-comment-thread .quick-talk-comment-content {
+ display: block;
+ margin-left: 20px;
+ color: #2c2c2c;
+}
diff --git a/talk/assets/quicktalk.js b/talk/assets/quicktalk.js
new file mode 100644
index 0000000..8d33f4c
--- /dev/null
+++ b/talk/assets/quicktalk.js
@@ -0,0 +1,248 @@
+class QuickTalk {
+ constructor(opt) {
+ this.options = opt;
+ if (typeof this.options.target === "string") {
+ this.options.target = document.getElementById(this.options.target);
+ }
+ this.preview_on = false;
+ this.instant_compose = undefined;
+ let editor = document.createElement("div");
+ let compose_button = document.createElement("button");
+ compose_button.setAttribute("class", "quick-talk-compose-button");
+ compose_button.textContent = "Write a comment";
+ this.options.target.appendChild(compose_button);
+ this.options.target.appendChild(editor);
+ let container = this.load_thread(this.options.target);
+ compose_button.addEventListener("click", (event) => {
+ if (this.instant_compose) {
+ this.instant_compose.parentNode.removeChild(this.instant_compose);
+ }
+ this.instant_compose = this.compose(editor, 0, true, (data) => {
+ this.show_comment(container, data, true).scrollIntoView();
+ });
+ });
+ }
+ request(uri, data, callback) {
+ let xhttp = new XMLHttpRequest();
+ xhttp.open("POST", uri, true);
+ xhttp.setRequestHeader("Content-type", "application/json");
+ xhttp.onreadystatechange = () => {
+ if (xhttp.readyState == 4) {
+ if (xhttp.status == 200) {
+ if (callback) {
+ callback(JSON.parse(xhttp.responseText));
+ }
+ }
+ else {
+ this.error(xhttp.statusText);
+ }
+ }
+ };
+ if (data) {
+ xhttp.send(JSON.stringify(data));
+ }
+ else {
+ xhttp.send();
+ }
+ }
+ set_status(type, msg) {
+ if (this.status_el) {
+ this.status_el.innerHTML = type + ": " + msg;
+ }
+ else {
+ console.log(type + ": " + msg);
+ }
+ }
+ clear_status() {
+ this.status_el.innerHTML = "";
+ }
+ info(msg) {
+ this.set_status("INFO", msg);
+ }
+ error(obj) {
+ console.log(obj);
+ this.set_status("ERROR", obj.toString());
+ }
+ compose(at, cid, auto_hide, callback) {
+ let preview = document.createElement("div");
+ preview.setAttribute("class", "quick-talk-preview");
+ preview.style.display = "none";
+ let container = document.createElement("div");
+ container.setAttribute("class", "quick-talk-compose");
+ let name = document.createElement("input");
+ name.value = "Name";
+ name.addEventListener("focus", (event) => {
+ if (name.value == "Name") {
+ name.value = "";
+ }
+ });
+ name.addEventListener("blur", (event) => {
+ if (name.value.trim() == "") {
+ name.value = "Name";
+ }
+ });
+ let email = document.createElement("input");
+ email.value = "Email";
+ email.addEventListener("focus", (event) => {
+ if (email.value == "Email") {
+ email.value = "";
+ }
+ });
+ email.addEventListener("blur", (event) => {
+ if (email.value.trim() == "") {
+ email.value = "Email";
+ }
+ });
+ let ta = document.createElement("textarea");
+ let footer = document.createElement("div");
+ footer.setAttribute("class", "quick-talk-compose-footer");
+ this.status_el = document.createElement("div");
+ let bt_preview = document.createElement("button");
+ bt_preview.textContent = "Preview";
+ bt_preview.addEventListener("click", (event) => {
+ let md = { data: ta.value };
+ if (!this.preview_on && ta.value.trim() != "") {
+ this.request(this.options.api_uri + "/preview", md, (ret) => {
+ if (ret.result) {
+ ta.style.display = "none";
+ preview.innerHTML = ret.result;
+ preview.style.display = "block";
+ bt_preview.textContent = "Edit";
+ this.preview_on = !this.preview_on;
+ }
+ else {
+ this.error(ret.error);
+ }
+ });
+ }
+ else {
+ ta.style.display = "block";
+ preview.style.display = "none";
+ bt_preview.textContent = "Preview";
+ this.preview_on = !this.preview_on;
+ }
+ });
+ let bt_submit = document.createElement("button");
+ bt_submit.textContent = "Send";
+ bt_submit.addEventListener("click", (event) => {
+ this.clear_status();
+ if (name.value.trim() == "" ||
+ name.value.trim() == "Name" ||
+ email.value.trim() == "" ||
+ email.value.trim() == "Email" ||
+ ta.value.trim() == "") {
+ this.info("Please enter all the fields");
+ return;
+ }
+ // check for email
+ let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ if (!re.test(String(email.value).toLowerCase()))
+ return this.info("Email is not correct");
+ // send the post request
+ let data = {
+ page: {
+ uri: this.options.uri,
+ },
+ comment: {
+ name: name.value,
+ email: email.value,
+ rid: cid,
+ content: ta.value,
+ },
+ };
+ this.request(this.options.api_uri + "/post", data, (ret) => {
+ if (ret.result) {
+ // TODO: more check goes here
+ name.value = "Name";
+ email.value = "Email";
+ ta.value = "";
+ if (auto_hide) {
+ at.removeChild(container);
+ this.instant_compose = undefined;
+ }
+ if (callback) {
+ callback(ret.result);
+ }
+ }
+ else {
+ this.error(ret.error);
+ }
+ });
+ });
+ footer.appendChild(this.status_el);
+ footer.appendChild(bt_preview);
+ footer.appendChild(bt_submit);
+ container.appendChild(name);
+ container.appendChild(email);
+ container.appendChild(ta);
+ container.appendChild(preview);
+ container.appendChild(footer);
+ at.appendChild(container);
+ container.scrollIntoView();
+ return container;
+ }
+ load_thread(at) {
+ let container = document.createElement("div");
+ container.setAttribute("class", "quick-talk-comment-thread");
+ at.appendChild(container);
+ this.request(this.options.api_uri, { page: this.options.uri }, (ret) => {
+ if (ret.result) {
+ ret.result.forEach((comment) => {
+ this.show_comment(container, comment, true);
+ });
+ }
+ else {
+ this.error(ret.error);
+ }
+ });
+ return container;
+ }
+ show_comment(at, comment, show_footer) {
+ let container = document.createElement("div");
+ container.setAttribute("class", "quick-talk-comment");
+ let header = document.createElement("div");
+ header.setAttribute("class", "quick-talk-comment-header");
+ let username = document.createElement("span");
+ username.setAttribute("class", "quick-talk-comment-user");
+ let time = document.createElement("span");
+ time.setAttribute("class", "quick-talk-comment-time");
+ let content = document.createElement("div");
+ content.setAttribute("class", "quick-talk-comment-content");
+ username.innerHTML = comment.name;
+ let date = new Date(parseInt(comment.time) * 1000);
+ time.innerHTML = `on ${date.getDate()}/${date.getMonth()}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}, wrote:`;
+ content.innerHTML = comment.content;
+ header.appendChild(username);
+ header.appendChild(time);
+ container.appendChild(header);
+ container.appendChild(content);
+ let sub_comments = document.createElement("div");
+ sub_comments.setAttribute("class", "quick-talk-sub-comment");
+ if (comment.children && comment.children.length > 0) {
+ comment.children.forEach((cmt) => {
+ this.show_comment(sub_comments, cmt, false);
+ });
+ }
+ container.appendChild(sub_comments);
+ if (show_footer) {
+ let footer = document.createElement("div");
+ footer.setAttribute("class", "quick-talk-comment-footer");
+ let span = document.createElement("span");
+ span.innerText = "Reply";
+ footer.appendChild(span);
+ let editor = document.createElement("div");
+ footer.appendChild(editor);
+ span.addEventListener("click", (event) => {
+ if (this.instant_compose) {
+ this.instant_compose.parentNode.removeChild(this.instant_compose);
+ }
+ this.instant_compose = this.compose(editor, parseInt(comment.id), true, (data) => {
+ this.show_comment(sub_comments, data, false);
+ });
+ });
+ container.appendChild(footer);
+ }
+ at.appendChild(container);
+ return container;
+ }
+}
diff --git a/talk/controllers/CommentController.lua b/talk/controllers/CommentController.lua
new file mode 100644
index 0000000..7e09685
--- /dev/null
+++ b/talk/controllers/CommentController.lua
@@ -0,0 +1,146 @@
+BaseController:subclass("CommentController",
+ {registry = {}, models = {"comment", "pages"}})
+
+local function process_md(input)
+ local md = require("md")
+ local content = ""
+ local callback = function(s) content = content .. s end
+ md.to_html(input, callback)
+ return content
+end
+
+local function sendmail(to, subject, content)
+ local from = "From: contact@iohub.dev\n"
+ local suject = "Subject: " .. subject .. "\n"
+
+ local cmd = 'echo "' .. utils.escape(from .. suject .. content) ..
+ '"| sendmail ' .. to
+ local r = os.execute(cmd)
+
+ if r then return true end
+ return false
+end
+function CommentController:index(...)
+ if (REQUEST.method == "OPTIONS") then
+ result("")
+ return false
+ end
+ local rq = (JSON.decodeString(REQUEST.json))
+ if (rq) then
+ local pages, order = self.pages:find({exp = {["="] = {uri = rq.page}}})
+ if not pages or #order == 0 then
+ fail("Be the first to comment")
+ else
+ local pid = pages[1].id
+ local comments, order = self.comment:find(
+ {
+ exp = {
+ ["and"] = {{["="] = {pid = pid}}, {[" = "] = {rid = 0}}}
+ },
+ order = {time = "ASC"},
+ fields = {"id", "time", "name", "rid", "pid", "content"}
+ })
+ if not comments or #order == 0 then
+ fail("Be the first to comment")
+ else
+ for idx, v in pairs(order) do
+ local data = comments[v]
+ data.content = process_md(data.content)
+ data.children = {}
+ -- find all the replies to this thread
+ local sub_comments, suborder =
+ self.comment:find(
+ {
+ exp = {
+ ["and"] = {
+ {["="] = {pid = pid}},
+ {[" = "] = {rid = data.id}}
+ }
+ },
+ order = {time = "ASC"}
+
+ })
+ if sub_comments and #suborder ~= 0 then
+ for i, subc in pairs(suborder) do
+ sub_comments[subc].content =
+ process_md(sub_comments[subc].content)
+ end
+ data.children = sub_comments
+ end
+ end
+ result(comments)
+ end
+ end
+ else
+ fail("Invalid request")
+ end
+ return false
+end
+
+function CommentController:post(...)
+ if (REQUEST.method == "OPTIONS") then
+ result("")
+ return false
+ end
+ local rq = (JSON.decodeString(REQUEST.json))
+ if rq then
+ local pages, order = self.pages:find({exp = {["="] = rq.page}})
+ if not pages or #order == 0 then
+ -- insert data
+ if self.pages:create(rq.page) then
+ rq.comment.pid = self.pages.db:lastInsertID()
+ else
+ fail("Unable to initialize comment thread for page: " ..
+ rq.page.uri)
+ return false
+ end
+ else
+ rq.comment.pid = pages[1].id
+ end
+ -- now insert the comment
+ rq.comment.time = os.time(os.date("!*t"))
+ if (self.comment:create(rq.comment)) then
+ rq.comment.id = self.comment.db:lastInsertID()
+
+ rq.comment.content = process_md(rq.comment.content)
+
+ -- send mail to all users of current page
+ local cmts, cmti = self.comment:select("MIN(id) as id,email",
+ "pid=" .. rq.comment.pid ..
+ " AND email != '" ..
+ rq.comment.email ..
+ "' GROUP BY email")
+ if cmts and #cmti > 0 then
+ for idx, v in pairs(cmti) do
+ sendmail(cmts[v].email, rq.comment.name ..
+ " has written something on a page that you've commented on",
+ rq.comment.name ..
+ " has written something on a page that you've commented. \nPlease visit this page: " ..
+ rq.page.uri ..
+ " for updates on the discussion.\nBest regard,\nEmail automatically sent by QuickTalk API")
+ end
+ end
+ rq.comment.email = ""
+ result(rq.comment)
+ else
+ fail("Unable to save comment")
+ end
+ else
+ fail("Invalid request")
+ end
+ return false
+end
+
+function CommentController:preview(...)
+ if (REQUEST.method == "OPTIONS") then
+ result("")
+ return false
+ end
+ local rq = (JSON.decodeString(REQUEST.json))
+ if (rq and rq.data) then
+ result(process_md(rq.data))
+ else
+ fail("Invalid request")
+ end
+ return false
+end
diff --git a/talk/controllers/IndexController.lua b/talk/controllers/IndexController.lua
new file mode 100644
index 0000000..946c8b8
--- /dev/null
+++ b/talk/controllers/IndexController.lua
@@ -0,0 +1,6 @@
+BaseController:subclass("IndexController", {registry = {}})
+
+function IndexController:index(...)
+ result("Quicktalk API")
+ return false
+end
diff --git a/talk/models/CommentModel.lua b/talk/models/CommentModel.lua
new file mode 100644
index 0000000..617a804
--- /dev/null
+++ b/talk/models/CommentModel.lua
@@ -0,0 +1,12 @@
+BaseModel:subclass("CommentModel", {
+ registry = {},
+ name = "comments",
+ fields = {
+ pid = "INTEGER",
+ name = "TEXT",
+ email = "TEXT",
+ content = "TEXT",
+ time = "NUMERIC",
+ rid = "INTEGER DEFAULT 0"
+ }
+})
diff --git a/talk/models/PagesModel.lua b/talk/models/PagesModel.lua
new file mode 100644
index 0000000..df4116f
--- /dev/null
+++ b/talk/models/PagesModel.lua
@@ -0,0 +1,2 @@
+BaseModel:subclass("PagesModel",
+ {registry = {}, name = "pages", fields = {uri = "TEXT"}})
diff --git a/talk/router.lua b/talk/router.lua
new file mode 100644
index 0000000..daadb25
--- /dev/null
+++ b/talk/router.lua
@@ -0,0 +1,56 @@
+-- the rewrite rule for the framework
+-- should be something like this
+-- ^\/apps\/+(.*)$ = /apps/router.lua?r=<1>&
+-- some global variables
+function fail(msg)
+ std.json()
+ std.t(JSON.encode({error = msg}))
+end
+
+function result(obj)
+ std.json()
+ std.t(JSON.encode({result = obj, error = false}))
+end
+DIR_SEP = "/"
+WWW_ROOT = __ROOT__ .. "/talk"
+if HEADER.Host then
+ HTTP_ROOT = "https://" .. HEADER.Host
+else
+ HTTP_ROOT = "https://talk.iohub.dev"
+end
+-- class path: path.to.class
+BASE_FRW = ""
+-- class path: path.to.class
+CONTROLLER_ROOT = BASE_FRW .. "talk.controllers"
+MODEL_ROOT = BASE_FRW .. "talk.models"
+-- file path: path/to/file
+VIEW_ROOT = WWW_ROOT .. DIR_SEP .. "views"
+LOG_ROOT = WWW_ROOT .. DIR_SEP .. "logs"
+
+-- require needed library
+require(BASE_FRW .. "silk.api")
+
+-- registry object store global variables
+local REGISTRY = {}
+-- set logging level
+REGISTRY.logger = Logger:new{
+ levels = {INFO = false, ERROR = false, DEBUG = false}
+}
+
+REGISTRY.layout = 'default'
+REGISTRY.fileaccess = true
+REGISTRY.db = DBHelper:new{db = "quicktalk"}
+REGISTRY.db:open()
+
+local router = Router:new{registry = REGISTRY}
+REGISTRY.router = router
+router:setPath(CONTROLLER_ROOT)
+-- router:route('edit', 'post/edit', "ALL" )
+
+std.header("Access-Control-Allow-Origin", "*")
+std.header("Access-Control-Allow-Methods", "POST")
+std.header("Access-Control-Allow-Headers", "content-type")
+
+-- router:route('default', nil)
+router:delegate()
+if REGISTRY.db then REGISTRY.db:close() end