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: "", + 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