1
0
mirror of https://github.com/lxsang/antd-web-apps synced 2024-12-26 01:18:21 +01:00

use quicktalk as comment API

This commit is contained in:
lxsang 2020-09-22 16:18:12 +02:00
parent e955d8a244
commit 5dd6da0b29
13 changed files with 747 additions and 176 deletions

View File

@ -1,5 +1,5 @@
BUILDDIR?=./build BUILDDIR?=./build
PROJS?=grs info blog os doc ci PROJS?=grs info blog os doc ci talk
copyfiles = index.ls mimes.json copyfiles = index.ls mimes.json
main: copy main: copy
for f in $(PROJS); do BUILDDIR=$(BUILDDIR)/"$${f}" make -C "$${f}" ; done for f in $(PROJS); do BUILDDIR=$(BUILDDIR)/"$${f}" make -C "$${f}" ; done

View File

@ -21,6 +21,10 @@
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/rst/font-awesome.css" /> <link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/rst/font-awesome.css" />
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/rst/afx.css" /> <link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/rst/afx.css" />
<link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/assets/style.css" /> <link rel="stylesheet" type="text/css" href="<?=HTTP_ROOT?>/assets/style.css" />
<link rel="stylesheet" type="text/css" href="https://chat.iohub.dev/assets/quicktalk.css" />
<script src="https://chat.iohub.dev/assets/quicktalk.js"> </script>
<script src="<?=HTTP_ROOT?>/rst/afx.js"> </script> <script src="<?=HTTP_ROOT?>/rst/afx.js"> </script>
<script src="<?=HTTP_ROOT?>/rst/gscripts/jquery-3.2.1.min.js"> </script> <script src="<?=HTTP_ROOT?>/rst/gscripts/jquery-3.2.1.min.js"> </script>
<script src="<?=HTTP_ROOT?>/assets/main.js"></script> <script src="<?=HTTP_ROOT?>/assets/main.js"></script>
@ -54,6 +58,15 @@
hljs.highlightBlock(block); hljs.highlightBlock(block);
hljs.lineNumbersBlock(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);
}); });
<?lua end ?> <?lua end ?>
window.twttr = (function(d, s, id) { window.twttr = (function(d, s, id) {

View File

@ -52,44 +52,10 @@
end end
echo("</ul>") echo("</ul>")
end?> end?>
<h1 class = "commentsec"></h1> <h1 class = "commentsec">Comments</h1>
<div id="disqus_thread"></div> <div>
<script> The comment editor supports <b>Markdown</b> document format. Your email is necessary to notify you of further updates on the discussion. It will be hidden from the public.
</div>
/** <div id="quick_talk_comment_thread"></div>
* RECOMMENDED CONFIGURATION VARIABLES: EDIT AND UNCOMMENT THE SECTION BELOW TO INSERT DYNAMIC VALUES FROM YOUR PLATFORM OR CMS.
* LEARN WHY DEFINING THESE VARIABLES IS IMPORTANT: https://disqus.com/admin/universalcode/#configuration-variables*/
var disqus_config = function () {
this.page.url = "<?=url?>"; // Replace PAGE_URL with your page's canonical URL variable
this.page.identifier = "<?=std.md5(url)?>"; // Replace PAGE_IDENTIFIER with your
};
(function() { // DON'T EDIT BELOW THIS LINE
var d = document, s = d.createElement('script');
s.src = 'https://https-blog-lxsang-me.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
})();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<!--div class = "commentform">
<div class = "inputbox">
<div class = "label">Name:</div>
<input data-class = "data" type = "text" name = "name" />
</div>
<div class = "inputbox">
<div class = "label">Email:</div>
<input data-class = "data" type = "text" name = "email" />
</div>
<textarea data-class = "data" name = "content"></textarea>
<div class = "inputboxbt">
<div data-id="status"></div>
<button data-id = "send" >Comment</button>
</div>
</div-->
</div> </div>
</div> </div>

View File

@ -1,52 +1,51 @@
-- create class -- create class
BaseObject:subclass("BaseModel", {registry = {}}) BaseObject:subclass("BaseModel", {registry = {}})
function BaseModel:initialize() function BaseModel:initialize()
self.db = self.registry.db 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) self.db:createTable(self.name, self.fields)
end end
end end
function BaseModel:create(m) function BaseModel:create(m)
if self.db and m then if self.db and m then return self.db:insert(self.name, m) end
return self.db:insert(self.name,m)
end
return false return false
end end
function BaseModel:update(m) function BaseModel:update(m)
if self.db and m then if self.db and m then return self.db:update(self.name, m) end
return self.db:update(self.name,m)
end
return false return false
end end
function BaseModel:delete(cond) function BaseModel:delete(cond)
if self.db and cond then if self.db and cond then return self.db:delete(self.name, cond) end
return self.db:delete(self.name,cond)
end
return false return false
end end
function BaseModel:find(cond) function BaseModel:find(cond)
if self.db and cond then if self.db and cond then return self.db:find(self.name, cond) end
return self.db:find(self.name, cond)
end
return false return false
end end
function BaseModel:get(id) 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 if not data or #order == 0 then return false end
return data[1] return data[1]
end end
function BaseModel:findAll() function BaseModel:findAll()
if self.db then if self.db then return self.db:getAll(self.name) end
return self.db:getAll(self.name)
end
return false return false
end 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

View File

@ -2,148 +2,143 @@ sqlite = modules.sqlite()
if sqlite == nil then return 0 end if sqlite == nil then return 0 end
-- create class -- create class
BaseObject:subclass("DBHelper",{db={}}) BaseObject:subclass("DBHelper", {db = {}})
function DBHelper:createTable(tbl, m) function DBHelper:createTable(tbl, m)
if self:available(tbl) then return true end if self:available(tbl) then return true end
local sql = "CREATE TABLE "..tbl.."(id INTEGER PRIMARY KEY" local sql = "CREATE TABLE " .. tbl .. "(id INTEGER PRIMARY KEY"
for k, v in pairs(m) do for k, v in pairs(m) do
if k ~= "id" then if k ~= "id" then sql = sql .. "," .. k .. " " .. v end
sql = sql..","..k.." "..v end
end sql = sql .. ");"
end return sqlite.query(self.db, sql) == 1
sql = sql..");"
return sqlite.query(self.db,sql) == 1
end end
function DBHelper:insert(tbl, m) function DBHelper:insert(tbl, m)
local keys = {} local keys = {}
local values = {} local values = {}
for k,v in pairs(m) do for k, v in pairs(m) do
if k ~= "id" then if k ~= "id" then
table.insert(keys,k) table.insert(keys, k)
if type(v) == "number" then if type(v) == "number" then
table.insert(values, v) table.insert(values, v)
else else
local t = "\""..v:gsub('"', '""').."\"" local t = "\"" .. v:gsub('"', '""') .. "\""
table.insert(values,t) table.insert(values, t)
end end
end end
end end
local sql = "INSERT INTO "..tbl.." ("..table.concat(keys,',')..') VALUES (' local sql = "INSERT INTO " .. tbl .. " (" .. table.concat(keys, ',') ..
sql = sql..table.concat(values,',')..');' ') VALUES ('
return sqlite.query(self.db, sql) == 1 sql = sql .. table.concat(values, ',') .. ');'
return sqlite.query(self.db, sql) == 1
end end
function DBHelper:get(tbl, id) function DBHelper:get(tbl, id)
return sqlite.select(self.db, tbl, "*","id="..id)[1] return sqlite.select(self.db, tbl, "*", "id=" .. id)[1]
end end
function DBHelper:getAll(tbl) function DBHelper:getAll(tbl)
local data = sqlite.select(self.db, tbl, "*", "1=1") local data = sqlite.select(self.db, tbl, "*", "1=1")
if data == nil then return nil end if data == nil then return nil end
local a = {} local a = {}
for n in pairs(data) do table.insert(a, n) end for n in pairs(data) do table.insert(a, n) end
table.sort(a) table.sort(a)
return data, a return data, a
end end
function DBHelper:find(tbl, cond) function DBHelper:find(tbl, cond)
local cnd = "1=1" local cnd = "1=1"
local sel = "*" local sel = "*"
if cond.exp then if cond.exp then cnd = self:gencond(cond.exp) end
cnd = self:gencond(cond.exp) if cond.order then
end cnd = cnd .. " ORDER BY "
if cond.order then local l = {}
cnd = cnd.." ORDER BY " local i = 1
local l = {} for k, v in pairs(cond.order) do
local i = 1 l[i] = k .. " " .. v
for k,v in pairs(cond.order) do i = i + 1
l[i] = k.." "..v end
i = i+1 cnd = cnd .. table.concat(l, ",")
end end
cnd = cnd..table.concat(l, ",") if cond.limit then cnd = cnd .. " LIMIT " .. cond.limit end
end if cond.fields then
if cond.limit then sel = table.concat(cond.fields, ",")
cnd = cnd.." LIMIT "..cond.limit -- print(sel)
end end
if cond.fields then local data = sqlite.select(self.db, tbl, sel, cnd)
sel = table.concat(cond.fields, ",") if data == nil then return nil end
--print(sel) local a = {}
end for n in pairs(data) do table.insert(a, n) end
local data = sqlite.select(self.db, tbl, sel, cnd) table.sort(a)
if data == nil then return nil end return data, a
local a = {}
for n in pairs(data) do table.insert(a, n) end
table.sort(a)
return data, a
end end
function DBHelper:query(sql) function DBHelper:select(tbl, sel, cnd)
return sqlite.query(self.db, sql) == 1 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 end
function DBHelper:query(sql) return sqlite.query(self.db, sql) == 1 end
function DBHelper:update(tbl, m) function DBHelper:update(tbl, m)
local id = m['id'] local id = m['id']
if id ~= nil then if id ~= nil then
local lst = {} local lst = {}
for k,v in pairs(m) do for k, v in pairs(m) do
if(type(v)== "number") then if (type(v) == "number") then
table.insert(lst,k.."="..v) table.insert(lst, k .. "=" .. v)
else else
table.insert(lst,k.."=\""..v:gsub('"', '""').."\"") table.insert(lst, k .. "=\"" .. v:gsub('"', '""') .. "\"")
end end
end end
local sql = "UPDATE "..tbl.." SET "..table.concat(lst,",").." WHERE id="..id..";" local sql = "UPDATE " .. tbl .. " SET " .. table.concat(lst, ",") ..
return sqlite.query(self.db, sql) == 1 " WHERE id=" .. id .. ";"
end return sqlite.query(self.db, sql) == 1
return false end
return false
end end
function DBHelper:available(tbl) function DBHelper:available(tbl) return sqlite.hasTable(self.db, tbl) == 1 end
return sqlite.hasTable(self.db, tbl) == 1 function DBHelper:deleteByID(tbl, id)
end local sql = "DELETE FROM " .. tbl .. " WHERE id=" .. id .. ";"
function DBHelper:deleteByID(tbl,id) return sqlite.query(self.db, sql) == 1
local sql = "DELETE FROM "..tbl.." WHERE id="..id..";"
return sqlite.query(self.db, sql) == 1
end end
function DBHelper:gencond(o) function DBHelper:gencond(o)
for k,v in pairs(o) do for k, v in pairs(o) do
if k == "and" or k == "or" then if k == "and" or k == "or" then
local cnd = {} local cnd = {}
local i = 1 local i = 1
for k1,v1 in pairs(v) do for k1, v1 in pairs(v) do
cnd[i] = self:gencond(v1) cnd[i] = self:gencond(v1)
i = i + 1 i = i + 1
end end
return " ("..table.concat(cnd, " "..k.." ")..") " return " (" .. table.concat(cnd, " " .. k .. " ") .. ") "
else else
for k1,v1 in pairs(v) do for k1, v1 in pairs(v) do
local t = type(v1) local t = type(v1)
if(t == "string") then if (t == "string") then
return " ("..k1.." "..k..' "'..v1:gsub('"','""')..'") ' return
end " (" .. k1 .. " " .. k .. ' "' .. v1:gsub('"', '""') ..
return " ("..k1.." "..k.." "..v1..") " '") '
end end
end return " (" .. k1 .. " " .. k .. " " .. v1 .. ") "
end end
end
end
end end
function DBHelper:delete(tbl, cond) function DBHelper:delete(tbl, cond)
local sql = "DELETE FROM "..tbl.." WHERE "..self:gencond(cond)..";" local sql = "DELETE FROM " .. tbl .. " WHERE " .. self:gencond(cond) .. ";"
return sqlite.query(self.db, sql) == 1 return sqlite.query(self.db, sql) == 1
end end
function DBHelper:lastInsertID() function DBHelper:lastInsertID() return sqlite.lastInsertID(self.db) end
return sqlite.lastInsertID(self.db)
end
function DBHelper:close() function DBHelper:close() if self.db then sqlite.dbclose(self.db) end end
if self.db then
sqlite.dbclose(self.db)
end
end
function DBHelper:open() function DBHelper:open()
if self.db ~= nil then if self.db ~= nil then self.db = sqlite.getdb(self.db) end
self.db = sqlite.getdb(self.db)
end
end end

6
talk/Makefile Normal file
View File

@ -0,0 +1,6 @@
copyfiles = router.lua models controllers assets
main:
- mkdir -p $(BUILDDIR)
cp -rvf $(copyfiles) $(BUILDDIR)
- mkdir -p $(BUILDDIR)/log

122
talk/assets/quicktalk.css Normal file
View File

@ -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;
}

248
talk/assets/quicktalk.js Normal file
View File

@ -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;
}
}

View File

@ -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

View File

@ -0,0 +1,6 @@
BaseController:subclass("IndexController", {registry = {}})
function IndexController:index(...)
result("Quicktalk API")
return false
end

View File

@ -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"
}
})

View File

@ -0,0 +1,2 @@
BaseModel:subclass("PagesModel",
{registry = {}, name = "pages", fields = {uri = "TEXT"}})

56
talk/router.lua Normal file
View File

@ -0,0 +1,56 @@
-- the rewrite rule for the framework
-- should be something like this
-- ^\/apps\/+(.*)$ = /apps/router.lua?r=<1>&<query>
-- 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