ikibooru/db.lua
2024-07-18 22:53:44 +03:00

827 lines
22 KiB
Lua

local SQL = require"luasql.mysql"
local BigGlobe = require"bigglobe"
local SQLEnv = SQL.mysql()
local SQLConn
local Rand = require"openssl.rand"
assert(Rand.ready())
local SessionsKey = Rand.bytes(64)
local ObjHideKey = io.open("objkey", "rb"):read"*a"
local AES128ECB = require"openssl.cipher".new("AES-128-ECB") -- Can be ECB without reprecussions
local Escapes = require"html"
local LFS = require"lfs"
local unistd = require"posix.unistd"
local stdio = require"posix.stdio"
local USER_PRIVS_BANNED = 0
local USER_PRIVS_UNAPPROVED = 1
local USER_PRIVS_APPROVED = 2
local USER_PRIVS_MOD = 128
local USER_PRIVS_ADMIN = 255
local function pingdb()
if not SQLConn or not SQLConn:ping() then
SQLConn = SQLEnv:connect("ikibooru", BigGlobe.cfg.sqlu, BigGlobe.cfg.sqlp, BigGlobe.cfg.sqlh)
end
return SQLConn ~= nil
end
-- UTC to store in database
local function getnow()
return "'" .. SQLConn:escape(os.date("!%Y-%m-%d %H:%M:%S")) .. "'"
end
local function autocomplete(liek, over18)
pingdb()
local ret = {}
local cursor = SQLConn:execute("SELECT * FROM tags WHERE name LIKE '" .. SQLConn:escape("%" .. liek .. "%") .. "'" .. (over18 and "" or " AND adultonly = 0") .. ";")
if not cursor then return "" end
local row = cursor:fetch({})
while row do
ret[#ret + 1] = table.concat(row, ",") .. "\n"
row = cursor:fetch(row)
end
cursor:close()
return table.concat(ret)
end
local function searchobjs(taglist, namefilter, offset, adultstuff)
-- Security check
for k,v in pairs(taglist) do
if type(taglist[k]) ~= "number" then
return nil
end
end
if not adultstuff then
adultstuff = -1
end
pingdb()
offset = tonumber(offset)
local sqlq
if #taglist > 0 and namefilter then
sqlq = string.format(
"SELECT * FROM objects WHERE %s id IN (SELECT objid FROM objtag WHERE tagid IN (%s) GROUP BY objid HAVING COUNT(tagid) >= %s) AND MATCH(name) AGAINST('%s') AND id > %s AND published = 1 ORDER BY id ASC LIMIT 50;",
({[-1] = "id NOT IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND", [0] = "", [1] = "id IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND"})[adultstuff],
table.concat(taglist, ","),
#taglist,
SQLConn:escape(namefilter),
offset
)
elseif #taglist > 0 and not namefilter then
sqlq = string.format(
"SELECT * FROM objects WHERE %s id IN (SELECT objid FROM objtag WHERE tagid IN (%s) GROUP BY objid HAVING COUNT(tagid) >= %s) AND id > %s AND published = 1 ORDER BY id ASC LIMIT 50;",
({[-1] = "id NOT IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND", [0] = "", [1] = "id IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND"})[adultstuff],
table.concat(taglist, ","),
#taglist,
offset
)
elseif #taglist == 0 and namefilter then
sqlq = string.format(
"SELECT * FROM objects WHERE %s MATCH(name) AGAINST('%s') AND id > %s AND published = 1 ORDER BY id ASC LIMIT 50;",
({[-1] = "id NOT IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND", [0] = "", [1] = "id IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND"})[adultstuff],
SQLConn:escape(namefilter),
offset
)
else
sqlq = string.format(
"SELECT * FROM objects WHERE %s id > %s AND published = 1 ORDER BY id ASC LIMIT 50;",
({[-1] = "id NOT IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND", [0] = "", [1] = "id IN (SELECT objid FROM objtag WHERE tagid IN (SELECT id FROM tags WHERE adultonly = 1)) AND"})[adultstuff],
offset
)
end
local cursor = SQLConn:execute(sqlq)
if not cursor then return {} end
local ret = {}
while true do
local row = cursor:fetch({}, "a")
if not row then break end
table.insert(ret, row)
end
return ret
end
local function getobj(objid)
if type(objid) ~= "number" then return nil end
pingdb()
local cursor = SQLConn:execute("SELECT * FROM objects WHERE id = " .. objid .. ";")
if not cursor then return nil end
local ret = cursor:fetch({}, "a")
if not ret then return nil end
ret.id = tonumber(ret.id) --for some reason..
ret.owner = tonumber(ret.owner)
ret.published = ret.published:byte(1) ~= 0
ret.approved = ret.approved:byte(1) ~= 0
return ret
end
local hextointandinttohex = {}
for i=0,255 do local s = string.format("%02x", i); hextointandinttohex[s] = i; hextointandinttohex[i] = s; end
local function b256toreadable(src)
local r = {}
src:gsub(".", function(b) table.insert(r, hextointandinttohex[b:byte()]) end)
return table.concat(r)
end
local function readabletob256(src)
if #src % 2 == 1 then error("gotta be even length, man") end
local r = {}
for i=1,#src,2 do
table.insert(r, string.char(hextointandinttohex[src:sub(i, i + 1)]))
end
return table.concat(r)
end
local function isobjhexvalid(objhex)
return #objhex == 32 and not objhex:find("[^0-9a-f]")
end
local function objhideid(objid)
local t = {}
for i=1,16 do
table.insert(t, objid % 256)
objid = objid // 256
end
local c = AES128ECB:encrypt(ObjHideKey, nil, false):final(string.char(table.unpack(t)))
return b256toreadable(c)
end
local function objshowid(objhex)
if not isobjhexvalid(objhex) then return nil end
local c = AES128ECB:decrypt(ObjHideKey, nil, false):final(readabletob256(objhex))
local r = 0
for i=16,1,-1 do
r = r * 256 + c:byte(i)
end
return r
end
local function testobjfilename(fname)
if fname:match"^%s+.*$" or fname:match"^%.+$" or fname:match".*%s+$" or fname:match".*%.+$" then return false end
for k, v in pairs{"CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"} do
if fname == v or fname:match("^" .. v .. "%..+") then
return false
end
end
return not fname:find"[/\\\0-\31%<%>%:%\"%|%?%*%[%]%%]" and #fname <= 56
end
local function urltophysical(p)
if p:sub(1, 6) == "/objd/" then
if isobjhexvalid(p:sub(7, 7 + 31)) and testobjfilename(p:sub(7 + 33)) then
return "objd/" .. p:sub(7, 7 + 31):gsub("..", "%0/") .. p:sub(7 + 32)
end
return nil
else
return p:sub(2)
end
end
-- PERFORMS NO QUOTA CHECKING; THATS DONE IN obje.html.l
local function createfile(objid, fname, fsz)
if not testobjfilename(fname) or fname == ".thumb.jpg" then return false end
if type(fsz) ~= "number" then return false end
local f = io.open(urltophysical("/objd/" .. objhideid(objid) .. "/" .. fname), "wb")
if not f then return false end
if unistd.ftruncate(stdio.fileno(f), fsz) ~= 0 then return false end
f:close()
return true
end
local function updatefile(objid, fname, offset, data)
if not testobjfilename(fname) or fname == ".thumb.jpg" then return false end
local fn = urltophysical("/objd/" .. objhideid(objid) .. "/" .. fname)
if offset + #data > lfs.attributes(fn).size then
return false
end
local f = io.open(fn, "r+b")
if not f then
return false
end
if not f:seek("set", offset) then return false end
if not f:write(data) then return false end
f:close()
return true
end
local function remfilefromobj(objid, fname)
if not testobjfilename(fname) then return false end
if not os.remove(urltophysical("/objd/" .. objhideid(objid) .. "/" .. fname)) then return false end
return true
end
local function genobjthumbnail(objid, fname, maxsize)
if not testobjfilename(fname) then return false end
local objhex = objhideid(objid)
local srcfn = urltophysical("/objd/" .. objhex .. "/" .. fname)
local tempfn = "/tmp/ikibooruthumb" .. b256toreadable(Rand.bytes(16)) .. ".jpg"
local destfn = urltophysical("/objd/" .. objhex .. "/.thumb.jpg")
local ret, killa, exitcode = os.execute(BigGlobe.cfg.magickprefix .. "convert " .. Escapes.shellescape(srcfn) .. " -background white -strip -interlace Plane -sampling-factor 4:2:0 -flatten -define jpeg:extent=100kb " .. Escapes.shellescape(tempfn))
if not ret or killa ~= "exit" or exitcode ~= 0 then return false end
if LFS.attributes(tempfn).size > maxsize then
os.remove(tempfn)
return false
end
local inn = io.open(tempfn, "rb")
local outt = io.open(destfn, "wb")
if not inn or not outt then
os.remove(tempfn)
return false
end
outt:write(inn:read"*a")
outt:close()
inn:close()
os.remove(tempfn)
return true
end
local function isdisplaynamevalid(str)
return not str:find"[\0-\31]" and #str <= 32
end
local function isemailvalid(str)
return not str:match"^%s*$" and not str:find"[\0-\31]" and #str <= 64
end
local function getuserbyemail(email)
if type(email) ~= "string" then return nil end
pingdb()
local cursor = SQLConn:execute("SELECT * FROM users WHERE email = '" .. SQLConn:escape(email) .. "';")
if not cursor then return nil end
local ret = cursor:fetch({}, "a")
if not ret then return nil end
ret.id = tonumber(ret.id)
ret.privs = tonumber(ret.privs)
return ret
end
local function getuserbyid(userid)
userid = tonumber(userid)
if not userid then return nil end
pingdb()
local cursor = SQLConn:execute("SELECT * FROM users WHERE id = " .. userid .. ";")
if not cursor then return nil end
local ret = cursor:fetch({}, "a")
if not ret then return nil end
ret.id = tonumber(ret.id)
ret.privs = tonumber(ret.privs)
return ret
end
local function reguser(email, displayname, privs)
if not isdisplaynamevalid(displayname) then return false, "displayname" end
privs = tonumber(privs)
if not privs then return false, "privs" end
pingdb()
local cursor = SQLConn:execute("INSERT INTO users (displayname, email, createtime, privs) VALUES ('" .. SQLConn:escape(displayname) .. "', '" .. SQLConn:escape(email) .. "', " .. getnow() .. ", " .. privs ..");")
if not cursor then return false, "db" end
return tonumber(SQLConn:getlastautoid())
end
local function userregverify(str)
local start, sep = str:find"^[^%;]+;"
-- Malformity check
if not start then return nil end
local digest = str:sub(1, sep - 1)
local msg = str:sub(sep + 1)
local thetime = msg:match"t%=(%d+)%;"
if not thetime then return nil end
if os.difftime(thetime, os.time()) < 0 then return false end
if b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(msg)) ~= digest then
return nil
end
if not msg:match"^reg%;" then return nil end
return { em = readabletob256(msg:match"em=([^%;]*)") }
end
local function userregcode(em)
local t = os.date"*t"
t.day = t.day + 7 --7 day expiry
local str = "reg;em="..b256toreadable(em) .. ";t=" .. os.time(t) .. ";"
return b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(str)) .. ";" .. str
end
local function userauth(user)
local t = os.date"*t"
t.day = t.day + 7 --7 day expiry
local str = "id=" .. user.id .. ";t=" .. os.time(t) .. ";"
return b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(str)) .. ";" .. str
end
local function userverify(str)
local start, sep = str:find"^[^%;]+;";
-- Malformity check
if not start then return nil end
local digest = str:sub(1, sep - 1)
local msg = str:sub(sep + 1)
local thetime = msg:match("t%=(%d+)%;")
-- Malformity check
if not thetime then return nil end
-- Expiry check
if os.difftime(thetime, os.time()) < 0 then return false end
if b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(msg)) ~= digest then
return nil
end
if not msg:match"^id%=" then return nil end
return getuserbyid(msg:match"^id%=(%d+)")
end
local function tagadd(name, category, adultonly)
category = tonumber(category)
if not category then return false end
category = category - 1
pingdb()
local cur = SQLConn:execute("INSERT INTO tags (name, category, adultonly) VALUES ('" .. SQLConn:escape(name) .. "', " .. category .. ", " .. (adultonly and 1 or 0) .. ");")
if not cur then return nil end
return tonumber(SQLConn:getlastautoid())
end
local function regnewobj(ownerid, approved, published)
ownerid = tonumber(ownerid)
if not ownerid then return nil end
if type(approved) ~= "boolean" then return nil end
if type(published) ~= "boolean" then return nil end
pingdb()
local cur = SQLConn:execute("INSERT INTO objects (name, owner, approved, published, createtime) VALUES ('', " .. ownerid .. ", " .. (approved and 1 or 0) .. ", " .. (published and 1 or 0) .. ", " .. getnow() .. ");")
if not cur then return nil end
local objid = tonumber(SQLConn:getlastautoid())
local objpath = "objd/"
for objpathpart in objhideid(objid):gmatch("..") do
objpath = objpath .. objpathpart .. "/"
LFS.mkdir(objpath)
end
return objid
end
local function getownedobjs(ownerid)
ownerid = tonumber(ownerid)
if not ownerid then return nil end
pingdb()
local cur = SQLConn:execute("SELECT * FROM objects WHERE owner = " .. ownerid .. " ORDER BY id DESC;")
if not cur then return {} end
local ret = {}
while true do
local row = cur:fetch({}, "a")
if not row then break end
row.id = tonumber(row.id)
row.owner = tonumber(row.owner)
table.insert(ret, row)
end
return ret
end
local function objupdate(objid, newname, newpublished)
if not isdisplaynamevalid(newname) then return false end
objid = tonumber(objid)
if not objid then return false end
if type(newpublished) ~= "boolean" then return false end
pingdb()
if not SQLConn:execute("UPDATE objects SET name = '" .. SQLConn:escape(newname) .. "', published = " .. (newpublished and 1 or 0) .. " WHERE id = " .. objid .. ";") then return false end
return true
end
local function updatetags(tagarr, newname, newcategory, newadultonly, del)
for k,v in pairs(tagarr) do
if type(v) ~= "number" then
return false
end
end
if #tagarr == 0 then return false end
if newcategory and type(newcategory) ~= "number" then
return false
end
pingdb()
if del then
return not not SQLConn:execute("DELETE FROM tags WHERE id IN (" .. table.concat(tagarr, ",") .. ");")
end
local cols = {"adultonly = " .. (newadultonly and 1 or 0)}
if newname then
table.insert(cols, newname and ("name = '" .. SQLConn:escape(newname) .. "'") or "")
end
if newcategory then
newcategory = newcategory - 1
table.insert(cols, newcategory and ("category = " .. newcategory) or "")
end
return not not SQLConn:execute("UPDATE tags SET " .. table.concat(cols, ", ") .. " WHERE id IN (" .. table.concat(tagarr, ",") .. ");")
end
local function csrf(userid)
local str = table.concat({os.time(), tonumber(userid)}, ";")
return b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(str)) .. ";" .. str
end
local function csrfverify(userid, booboo)
if not booboo:find";" then return false end
local digest = booboo:sub(1, booboo:find";" - 1)
local msg = booboo:sub(booboo:find";" + 1)
if b256toreadable(require"openssl.hmac".new(SessionsKey, "sha256"):final(msg)) ~= digest then
return false
end
return tonumber(msg:match";(%d+)$") == userid
end
local function nexttoapproveobj()
pingdb()
local cur = SQLConn:execute("SELECT id FROM objects WHERE published = 1 AND approved = 0 ORDER BY id ASC LIMIT 1;")
if not cur then return nil end
return tonumber(cur:fetch())
end
local function banuser(uid)
if type(uid) ~= "number" then
return false
end
pingdb()
return not not SQLConn:execute("UPDATE users SET privs = " .. USER_PRIVS_BANNED .. " WHERE id = " .. uid .. ";")
end
local function approveuser(uid)
if type(uid) ~= "number" then
return false
end
pingdb()
return not not SQLConn:execute("UPDATE users SET privs = " .. USER_PRIVS_APPROVED .. " WHERE id = " .. uid .. " AND privs = " .. USER_PRIVS_UNAPPROVED .. ";")
end
local function delobj(oid, delfiles)
if type(oid) ~= "number" then
return false
end
pingdb()
if not SQLConn:execute("DELETE FROM objects WHERE id = " .. oid .. ";") then
return false
end
if delfiles then
local d = urltophysical("/objd/" .. objhideid(oid))
for f in LFS.dir(d) do
if f ~= "." and f ~= ".." then
os.remove(d .. f)
end
end
end
return true
end
local function approveobj(oid)
if type(oid) ~= "number" then
return false
end
pingdb()
return not not SQLConn:execute("UPDATE objects SET approved = 1 WHERE id = " .. oid .. ";")
end
local function userinfoupdate(userid, displayname)
if type(userid) ~= "number" then return false end
if not isdisplaynamevalid(displayname) then return false end
pingdb()
return not not SQLConn:execute("UPDATE users SET displayname = '" .. SQLConn:escape(displayname) .. "' WHERE id = " .. userid .. ";")
end
local function getobjtags(objid)
if type(objid) ~= "number" then return nil end
pingdb()
local cur = SQLConn:execute("SELECT * FROM tags WHERE id IN (SELECT tagid FROM objtag WHERE objid = " .. objid .. ");")
if not cur then return nil end
local ret = {}
local n = 0
while true do
local t = cur:fetch({}, "*a")
if not t then break end
t.id = tonumber(t.id)
t.category = tonumber(t.category)
ret[t.id] = t
n = n + 1
end
return ret, n
end
local function addtagstoobj(objid, taglist)
if type(objid) ~= "number" then return false end
local temp = {}
for k, v in pairs(taglist) do
if type(v) ~= "number" then return false end
table.insert(temp, "(" .. objid .. "," .. v .. ")")
end
pingdb()
return not not SQLConn:execute("INSERT INTO objtag (objid, tagid) VALUES " .. table.concat(temp, ",") .. ";")
end
local function remtagsfromobj(objid, taglist)
if type(objid) ~= "number" then return false end
local temp = {}
for k, v in pairs(taglist) do
if type(v) ~= "number" then return false end
table.insert(temp, "tagid = " .. v)
end
pingdb()
return not not SQLConn:execute("DELETE FROM objtag WHERE objid = " .. objid .. " AND (" .. table.concat(temp, " OR ") .. ");")
end
local function getmoderators()
pingdb()
local cur = SQLConn:execute("SELECT * from users WHERE privs = " .. USER_PRIVS_MOD .. ";")
if not cur then return {} end
local ret = {}
while true do
local t = cur:fetch({}, "a")
if not t then break end
t.id = tonumber(t.id)
t.privs = tonumber(t.privs)
table.insert(ret, t)
end
return ret
end
local function setmodsviaemails(emails)
pingdb()
if not SQLConn:execute("UPDATE users SET privs = " .. USER_PRIVS_APPROVED .. " WHERE privs = " .. USER_PRIVS_MOD .. ";") then
return false
end
for k,v in pairs(emails) do
emails[k] = "'" .. SQLConn:escape(v) .. "'"
end
return not not SQLConn:execute("UPDATE users SET privs = " .. USER_PRIVS_MOD .. " WHERE privs < " .. USER_PRIVS_MOD .. " AND email IN (" .. table.concat(emails, ",") .. ");")
end
local function getcomments(objid)
if type(objid) ~= "number" then return nil end
pingdb()
local cur = SQLConn:execute("SELECT comment.id AS id, comment.content AS content, comment.authorid AS authorid, user.displayname AS authordisplayname, comment.createtime AS createtime FROM comments comment INNER JOIN users user ON comment.authorid = user.id WHERE comment.objid = " .. objid .. " ORDER BY id DESC;")
if not cur then return {} end
local ret = {}
while true do
local t = cur:fetch({}, "a")
if not t then break end
t.id = tonumber(t.id)
t.authorid = tonumber(t.authorid)
table.insert(ret, t)
end
return ret
end
local function postcomment(objid, userid, content)
if #content > BigGlobe.MAX_COMMENT_SIZE then
return false
end
pingdb()
return not not SQLConn:execute("INSERT INTO comments (objid, authorid, content, createtime) VALUES (" .. objid .. ", " .. userid .. ", '" .. SQLConn:escape(content) .. "', " .. getnow() .. ")")
end
local function addreport(reporterid, reporteeid, content)
if type(reporterid) ~= "number" then return false end
if type(reporteeid) ~= "number" then return false end
pingdb()
return not not SQLConn:execute("INSERT INTO reports (reporter, reportee, content, createtime, status) VALUES (" .. reporterid .. ", " .. reporteeid .. ", '" .. SQLConn:escape(content) .. "', " .. getnow() .. ", " .. BigGlobe.REPORT_STATUS_OPEN .. ")")
end
local function getreportcount()
pingdb()
local cur = SQLConn:execute("SELECT COUNT(*) FROM reports WHERE status = 0;")
if not cur then return nil end
return cur:fetch()
end
local function getreports(status, offset)
if type(status) ~= "number" then return nil end
if type(offset) ~= "number" then offset = 0 end
pingdb()
local cur = SQLConn:execute("SELECT * FROM reports WHERE status = " .. status .. " AND id > " .. offset .. " ORDER BY id ASC LIMIT 50;")
if not cur then return {} end
local ret = {}
while true do
local t = cur:fetch({}, "a")
if not t then break end
t.id = tonumber(t.id)
t.reporter = tonumber(t.reporter)
t.reportee = tonumber(t.reportee)
t.status = tonumber(t.status)
table.insert(ret, t)
end
return ret
end
local function setreportstatus(rid, newstatus)
if type(rid) ~= "number" then return false end
if type(newstatus) ~= "number" then return false end
pingdb()
return not not SQLConn:execute("UPDATE reports SET status = " .. newstatus .. " WHERE id = " .. rid .. ";")
end
local function getobjcount()
pingdb()
local cur = SQLConn:execute("SELECT COUNT(*) FROM objects;")
if not cur then return nil end
return cur:fetch()
end
local function gettagcount()
pingdb()
local cur = SQLConn:execute("SELECT COUNT(*) FROM tags;")
if not cur then return nil end
return cur:fetch()
end
return {
USER_PRIVS_BANNED = USER_PRIVS_BANNED,
USER_PRIVS_UNAPPROVED = USER_PRIVS_UNAPPROVED,
USER_PRIVS_APPROVED = USER_PRIVS_APPROVED,
USER_PRIVS_MOD = USER_PRIVS_MOD,
USER_PRIVS_ADMIN = USER_PRIVS_ADMIN,
autocomplete = autocomplete,
searchobjs = searchobjs,
getobj = getobj,
objhideid = objhideid,
objshowid = objshowid,
isobjhexvalid = isobjhexvalid,
urltophysical = urltophysical,
createfile = createfile,
updatefile = updatefile,
remfilefromobj = remfilefromobj,
genobjthumbnail = genobjthumbnail,
isdisplaynamevalid = isdisplaynamevalid,
isemailvalid = isemailvalid,
getuserbyemail = getuserbyemail,
getuserbyid = getuserbyid,
userauth = userauth,
userverify = userverify,
userregcode = userregcode,
userregverify = userregverify,
reguser = reguser,
tagadd = tagadd,
regnewobj = regnewobj,
getownedobjs = getownedobjs,
objupdate = objupdate,
updatetags = updatetags,
b256toreadable = b256toreadable,
readabletob256 = readabletob256,
csrf = csrf,
csrfverify = csrfverify,
nexttoapproveobj = nexttoapproveobj,
banuser = banuser,
approveuser = approveuser,
delobj = delobj,
approveobj = approveobj,
userinfoupdate = userinfoupdate,
getobjtags = getobjtags,
addtagstoobj = addtagstoobj,
remtagsfromobj = remtagsfromobj,
getmoderators = getmoderators,
setmodsviaemails = setmodsviaemails,
getcomments = getcomments,
postcomment = postcomment,
addreport = addreport,
getreportcount = getreportcount,
getreports = getreports,
setreportstatus = setreportstatus,
getobjcount = getobjcount,
gettagcount = gettagcount,
}