🐳chore(库):裁剪 Lapis-chan 项目

develop
cloudfreexiao 5 years ago
parent 4cb90fbc60
commit 37dcdacc05

@ -1,5 +1,7 @@
# Lapis-chan # Lapis-chan
https://github.com/karai17/lapis-chan
Lapis-chan is a text and image board written in Lua using the Lapis web framework. Lapis-chan is a text and image board written in Lua using the Lapis web framework.
# Features # Features

@ -41,6 +41,6 @@ end
--]] --]]
app:include("apps.api") app:include("apps.api")
app:include("apps.web") -- app:include("apps.web")
return app return app

@ -6,63 +6,63 @@ local secret = assert(loadfile("../data/secrets/token.lua"))()
local subdomains = false local subdomains = false
-- Maximum file size (update this in scripts.js too!) -- Maximum file size (update this in scripts.js too!)
local body_size = "15m" local body_size = "15m"
-- Maximum comment size (update this in scripts.js too!) -- Maximum comment size (update this in scripts.js too!)
local text_size = 10000 local text_size = 10000
-- Path to your lua libraries (LuaRocks and OpenResty) -- Path to your lua libraries (LuaRocks and OpenResty)
local lua_path = "./src/?.lua;./src/?/init.lua" local lua_path = "./src/?.lua;./src/?/init.lua"
local lua_cpath = "" local lua_cpath = ""
config("development", { config("development", {
site_name = "[DEVEL] Lapis-chan", site_name = "[DEVEL] Lapis-chan",
port = 80, port = 80,
secret = secret, secret = secret,
subdomains = subdomains, subdomains = subdomains,
body_size = body_size, body_size = body_size,
text_size = text_size, text_size = text_size,
lua_path = lua_path, lua_path = lua_path,
lua_cpath = lua_cpath, lua_cpath = lua_cpath,
postgres = { postgres = {
host = "psql", host = "psql",
user = "postgres", user = "postgres",
password = "", password = "",
database = "lapischan" database = "lapischan",
}, },
}) })
config("production", { config("production", {
code_cache = "on", code_cache = "on",
site_name = "Lapis-chan", site_name = "Lapis-chan",
port = 80, port = 80,
secret = secret, secret = secret,
subdomains = subdomains, subdomains = subdomains,
body_size = body_size, body_size = body_size,
text_size = text_size, text_size = text_size,
lua_path = lua_path, lua_path = lua_path,
lua_cpath = lua_cpath, lua_cpath = lua_cpath,
postgres = { postgres = {
host = "psql", host = "psql",
user = "postgres", user = "postgres",
password = "", password = "",
database = "lapischan" database = "lapischan",
}, },
}) })
config("test", { config("test", {
site_name = "[DEVEL] Lapis-chan", site_name = "[DEVEL] Lapis-chan",
port = 80, port = 80,
secret = secret, secret = secret,
subdomains = subdomains, subdomains = subdomains,
body_size = body_size, body_size = body_size,
text_size = text_size, text_size = text_size,
lua_path = lua_path, lua_path = lua_path,
lua_cpath = lua_cpath, lua_cpath = lua_cpath,
postgres = { postgres = {
host = "psql", host = "psql",
user = "postgres", user = "postgres",
password = "", password = "",
database = "lapischan_test" database = "lapischan_test",
}, },
}) })

@ -21,106 +21,11 @@ return {
{ "time", types.integer }, { "time", types.integer },
{ "duration", types.integer { default=259200 }}, -- 3 days { "duration", types.integer { default=259200 }}, -- 3 days
}) })
schema.create_table("boards", {
{ "id", types.serial { unique=true, primary_key=true }},
{ "short_name", types.varchar { unique=true }},
{ "name", types.varchar { unique=true }},
{ "subtext", types.varchar { null=true }},
{ "rules", types.text { null=true }},
{ "ban_message", types.varchar { default="USER WAS BANNED FOR THIS POST" }},
{ "anon_name", types.varchar { default="Anonymous" }},
{ "theme", types.varchar { default="yotsuba_b" }},
{ "posts", types.integer { default=0 }},
{ "pages", types.integer { default=10 }},
{ "threads_per_page", types.integer { default=10 }},
{ "text_only", types.boolean { default=false }},
{ "draw", types.boolean { default=false }},
{ "thread_file", types.boolean { default=true }},
{ "thread_comment", types.boolean { default=false }},
{ "thread_file_limit", types.integer { default=100 }},
{ "post_file", types.boolean { default=false }},
{ "post_comment", types.boolean { default=false }},
{ "post_limit", types.integer { default=250 }},
{ "archive", types.boolean { default=true }},
{ "archive_time", types.integer { default=2592000 }}, -- 30 days
{ "group", types.integer { default=1 }}
})
schema.create_table("threads", {
{ "id", types.serial { unique=true, primary_key=true }},
{ "board_id", types.integer },
{ "last_active", types.integer },
{ "sticky", types.boolean { default=false }},
{ "lock", types.boolean { default=false }},
{ "archive", types.boolean { default=false }},
{ "size_override", types.boolean { default=false }},
{ "save", types.boolean { default=false }}
})
schema.create_table("posts", {
{ "id", types.serial { unique=true, primary_key=true }},
{ "post_id", types.integer },
{ "thread_id", types.integer },
{ "board_id", types.integer },
{ "timestamp", types.integer },
{ "ip", types.varchar },
{ "comment", types.text { null=true }},
{ "name", types.varchar { null=true }},
{ "trip", types.varchar { null=true }},
{ "subject", types.varchar { null=true }},
{ "password", types.varchar { null=true }},
{ "file_name", types.varchar { null=true }},
{ "file_path", types.varchar { null=true }},
{ "file_md5", types.varchar { null=true }},
{ "file_size", types.integer { null=true }},
{ "file_width", types.integer { null=true }},
{ "file_height", types.integer { null=true }},
{ "file_spoiler", types.boolean { null=true }},
{ "banned", types.boolean { default=false }}
})
schema.create_table("announcements", {
{ "id", types.serial { unique=true, primary_key=true }},
{ "board_id", types.integer { null=true }},
{ "text", types.varchar }
})
schema.create_table("reports", {
{ "id", types.serial { unique=true, primary_key=true }},
{ "board_id", types.integer },
{ "thread_id", types.integer },
{ "post_id", types.integer },
{ "timestamp", types.integer },
{ "num_reports", types.integer }
})
schema.create_table("pages", {
{ "id", types.serial { unique=true, primary_key=true }},
{ "url", types.varchar { unique=true }},
{ "name", types.varchar },
{ "content", types.text }
})
end, end,
[120] = function() [120] = function()
schema.add_column("boards", "filetype_image", types.boolean { default=true })
schema.add_column("boards", "filetype_audio", types.boolean { default=false })
schema.add_column("posts", "file_type", types.varchar { default="image" })
schema.add_column("posts", "file_duration", types.varchar { null=true })
end, end,
[200] = function() [200] = function()
schema.rename_column("boards", "posts", "total_posts")
schema.rename_column("boards", "name", "title")
schema.rename_column("boards", "short_name", "name")
schema.rename_column("pages", "url", "slug")
schema.rename_column("pages", "name", "title")
schema.rename_column("posts", "post_id", "post_number")
db.query("ALTER TABLE posts ALTER COLUMN file_type DROP DEFAULT")
db.query("ALTER TABLE posts ALTER COLUMN file_type DROP NOT NULL")
schema.add_column("boards", "anon_only", types.boolean { default=false })
local Users = require "src.models.users" local Users = require "src.models.users"
local uuid = require "resty.jit-uuid" local uuid = require "resty.jit-uuid"
uuid.seed() uuid.seed()
@ -140,9 +45,5 @@ return {
schema.drop_column("users", "janitor") schema.drop_column("users", "janitor")
schema.drop_column("users", "mod") schema.drop_column("users", "mod")
schema.drop_column("users", "admin") schema.drop_column("users", "admin")
schema.add_column("posts", "reports", types.integer)
db.query("UPDATE posts SET reports=reports.num_reports FROM reports WHERE posts.id=reports.post_id")
schema.drop_table("reports")
end end
} }

@ -30,7 +30,7 @@ http {
require "socket" require "socket"
} }
resolver 127.0.0.11; resolver 114.114.114.114; #域名解析地址
server { server {
listen ${{PORT}}; listen ${{PORT}};
@ -42,7 +42,7 @@ http {
} }
location /static/ { location /static/ {
alias static/; alias ../data/static/;
} }
location /files/ { location /files/ {

@ -1,17 +1,16 @@
local mock_request = require("lapis.spec.request").mock_request local mock_request = require("lapis.spec.request").mock_request
local app = require("app") local app = require("app")
describe("lapischan", function() describe("lapischan", function()
require("lapis.spec").use_test_env() require("lapis.spec").use_test_env()
setup(function() setup(function()
require("lapis.db.migrations").run_migrations(require("migrations")) require("lapis.db.migrations").run_migrations(require("migrations"))
end) end)
it("loads install page", function() it("loads install page", function()
local status, body = mock_request(app, "/") local status, body = mock_request(app, "/")
assert.same(200, status) assert.same(200, status)
assert.truthy(body:find("Install Lapis-chan", 1, true)) assert.truthy(body:find("Install Lapis-chan", 1, true))
end) end)
end) end)

@ -12,10 +12,7 @@ app:before_filter(capture({ on_error=handle, require "apps.api.internal.before_a
app:before_filter(capture({ on_error=handle, require "apps.api.internal.before_locale" })) app:before_filter(capture({ on_error=handle, require "apps.api.internal.before_locale" }))
app:include("apps.api.core") app:include("apps.api.core")
app:include("apps.api.announcements")
app:include("apps.api.bans") app:include("apps.api.bans")
app:include("apps.api.boards")
app:include("apps.api.pages")
app:include("apps.api.users") app:include("apps.api.users")
return app return app

@ -1,14 +0,0 @@
local lapis = require "lapis"
local capture = require("lapis.application").capture_errors_json
local r2 = require("lapis.application").respond_to
local handle = require("utils.error").handle
local app = lapis.Application()
app.__base = app
app.name = "api.announcements."
app.path = "/api/announcements"
app:match("announcements", "", capture({ on_error=handle, r2(require "apps.api.announcements.announcements") }))
app:match("announcement", "/:uri_announcement[%d]", capture({ on_error=handle, r2(require "apps.api.announcements.announcement") }))
app:match("global", "/global", capture({ on_error=handle, r2(require "apps.api.announcements.global") }))
return app

@ -1,64 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role"
local models = require "models"
local Announcements = models.announcements
function action:GET()
-- Get Announcement
local announcement = assert_error(Announcements:get(self.params.uri_announcement))
Announcements:format_from_db(announcement)
return {
status = ngx.HTTP_OK,
json = announcement
}
end
function action:PUT()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Validate parameters
local params = {
id = self.params.uri_announcement,
board_id = tonumber(self.params.board_id),
text = self.params.text,
}
trim_filter(params)
Announcements:format_to_db(params)
assert_valid(params, Announcements.valid_record)
-- Modify Announcement
local announcement = assert_error(Announcements:modify(params))
Announcements:format_from_db(announcement)
return {
status = ngx.HTTP_OK,
json = announcement
}
end
function action:DELETE()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Delete Announcement
local announcement = assert_error(Announcements:delete(self.params.uri_announcement))
return {
status = ngx.HTTP_OK,
json = {
id = announcement.id,
text = announcement.text
}
}
end
return action

@ -1,51 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role"
local models = require "models"
local Announcements = models.announcements
function action:GET()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Get all Announcements
local announcements = assert_error(Announcements:get_all())
for _, announcement in ipairs(announcements) do
Announcements:format_from_db(announcement)
end
return {
status = ngx.HTTP_OK,
json = announcements
}
end
function action:POST()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Validate parameters
local params = {
board_id = tonumber(self.params.board_id),
text = self.params.text,
}
trim_filter(params)
Announcements:format_to_db(params)
assert_valid(params, Announcements.valid_record)
-- Create Announcement
local announcement = assert_error(Announcements:new(params))
Announcements:format_from_db(announcement)
return {
status = ngx.HTTP_OK,
json = announcement
}
end
return action

@ -1,21 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local models = require "models"
local Announcements = models.announcements
function action.GET()
-- Get global Announcements
local announcements = assert_error(Announcements:get_global())
for _, announcement in ipairs(announcements) do
Announcements:format_from_db(announcement)
end
return {
status = ngx.HTTP_OK,
json = announcements
}
end
return action

@ -1,14 +0,0 @@
local lapis = require "lapis"
local capture = require("lapis.application").capture_errors_json
local r2 = require("lapis.application").respond_to
local handle = require("utils.error").handle
local app = lapis.Application()
app.__base = app
app.name = "api.bans."
app.path = "/api/bans"
app:match("bans", "", capture({ on_error=handle, r2(require "apps.api.bans.bans") }))
app:match("ban", "/:uri_ban[%d]", capture({ on_error=handle, r2(require "apps.api.bans.ban") }))
app:match("bans_ip", "/ip/:uri_ip", capture({ on_error=handle, r2(require "apps.api.bans.bans_ip") }))
return app

@ -1,70 +1,70 @@
local ngx = _G.ngx local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base") local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role" local role = require "utils.role"
local models = require "models" local models = require "models"
local Bans = models.bans local Bans = models.bans
function action:GET() function action:GET()
-- Verify the User's permissions -- Verify the User's permissions
assert_error(role.mod(self.api_user)) assert_error(role.mod(self.api_user))
-- Get Ban -- Get Ban
local ban = assert_error(Bans:get(self.params.uri_ban)) local ban = assert_error(Bans:get(self.params.uri_ban))
Bans:format_from_db(ban) Bans:format_from_db(ban)
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = ban json = ban,
} }
end end
function action:PUT() function action:PUT()
-- Verify the User's permissions -- Verify the User's permissions
assert_error(role.mod(self.api_user)) assert_error(role.mod(self.api_user))
-- Validate parameters -- Validate parameters
local params = { local params = {
id = self.params.uri_ban, id = self.params.uri_ban,
board_id = tonumber(self.params.board_id), board_id = tonumber(self.params.board_id),
ip = self.params.ip, ip = self.params.ip,
reason = self.params.reason, reason = self.params.reason,
time = os.time(), time = os.time(),
duration = tonumber(self.params.duration) duration = tonumber(self.params.duration),
} }
trim_filter(params) trim_filter(params)
Bans:format_to_db(params) Bans:format_to_db(params)
assert_valid(params, Bans.valid_record) assert_valid(params, Bans.valid_record)
-- Modify Ban -- Modify Ban
local ban = assert_error(Bans:modify(params)) local ban = assert_error(Bans:modify(params))
Bans:format_from_db(ban) Bans:format_from_db(ban)
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = ban json = ban,
} }
end end
function action:DELETE() function action:DELETE()
-- Verify the User's permissions -- Verify the User's permissions
assert_error(role.mod(self.api_user)) assert_error(role.mod(self.api_user))
-- Delete Ban -- Delete Ban
local ban = assert_error(Bans:delete(self.params.uri_ban)) local ban = assert_error(Bans:delete(self.params.uri_ban))
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = { json = {
id = ban.id, id = ban.id,
ip = ban.ip, ip = ban.ip,
} },
} }
end end
return action return action

@ -1,54 +1,54 @@
local ngx = _G.ngx local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base") local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role" local role = require "utils.role"
local models = require "models" local models = require "models"
local Bans = models.bans local Bans = models.bans
function action:GET() function action:GET()
-- Verify the User's permissions -- Verify the User's permissions
assert_error(role.mod(self.api_user)) assert_error(role.mod(self.api_user))
-- Get all Bans -- Get all Bans
local bans = assert_error(Bans:get_all()) local bans = assert_error(Bans:get_all())
for _, ban in ipairs(bans) do for _, ban in ipairs(bans) do
Bans:format_from_db(ban) Bans:format_from_db(ban)
end end
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = bans json = bans,
} }
end end
function action:POST() function action:POST()
-- Verify the User's permissions -- Verify the User's permissions
assert_error(role.mod(self.api_user)) assert_error(role.mod(self.api_user))
-- Validate parameters -- Validate parameters
local params = { local params = {
board_id = tonumber(self.params.board_id), board_id = tonumber(self.params.board_id),
ip = self.params.ip, ip = self.params.ip,
reason = self.params.reason, reason = self.params.reason,
time = os.time(), time = os.time(),
duration = tonumber(self.params.duration) duration = tonumber(self.params.duration),
} }
trim_filter(params) trim_filter(params)
Bans:format_to_db(params) Bans:format_to_db(params)
assert_valid(params, Bans.valid_record) assert_valid(params, Bans.valid_record)
-- Create Ban -- Create Ban
local ban = assert_error(Bans:new(params)) local ban = assert_error(Bans:new(params))
Bans:format_from_db(ban) Bans:format_from_db(ban)
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = ban json = ban,
} }
end end
return action return action

@ -1,25 +1,25 @@
local ngx = _G.ngx local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base") local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error local assert_error = require("lapis.application").assert_error
local role = require "utils.role" local role = require "utils.role"
local models = require "models" local models = require "models"
local Bans = models.bans local Bans = models.bans
function action:GET() function action:GET()
-- Verify the User's permissions -- Verify the User's permissions
assert_error(role.mod(self.api_user)) assert_error(role.mod(self.api_user))
-- Get Bans -- Get Bans
local bans = assert_error(Bans:get_ip(self.params.uri_ip)) local bans = assert_error(Bans:get_ip(self.params.uri_ip))
for _, ban in ipairs(bans) do for _, ban in ipairs(bans) do
Bans:format_from_db(ban) Bans:format_from_db(ban)
end end
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = bans json = bans,
} }
end end
return action return action

@ -1,23 +0,0 @@
local lapis = require "lapis"
local capture = require("lapis.application").capture_errors_json
local r2 = require("lapis.application").respond_to
local handle = require("utils.error").handle
local app = lapis.Application()
app.__base = app
app.name = "api.boards."
app.path = "/api/boards"
app:match("boards", "", capture({ on_error=handle, r2(require "apps.api.boards.boards") }))
app:match("board", "/:uri_board", capture({ on_error=handle, r2(require "apps.api.boards.board") }))
app:match("announcements", "/:uri_board/announcements", capture({ on_error=handle, r2(require "apps.api.boards.announcements") }))
app:match("bans", "/:uri_board/bans", capture({ on_error=handle, r2(require "apps.api.boards.bans") }))
app:match("reports", "/:uri_board/reports", capture({ on_error=handle, r2(require "apps.api.boards.reports") }))
app:match("threads", "/:uri_board/threads(/pages/:uri_page[%d])", capture({ on_error=handle, r2(require "apps.api.boards.threads") }))
app:match("archived", "/:uri_board/threads/archived", capture({ on_error=handle, r2(require "apps.api.boards.archived") }))
app:match("thread", "/:uri_board/threads/:uri_thread[%d]", capture({ on_error=handle, r2(require "apps.api.boards.thread") }))
app:match("thread.reports", "/:uri_board/threads/:uri_thread[%d]/reports", capture({ on_error=handle, r2(require "apps.api.boards.thread_reports") }))
app:match("posts", "/:uri_board(/threads/:uri_thread[%d])/posts", capture({ on_error=handle, r2(require "apps.api.boards.posts") }))
app:match("post", "/:uri_board/threads/:uri_thread[%d]/posts/:uri_post[%d]", capture({ on_error=handle, r2(require "apps.api.boards.post") }))
app:match("post.reports", "/:uri_board/threads/:uri_thread[%d]/posts/:uri_post[%d]/reports", capture({ on_error=handle, r2(require "apps.api.boards.post_reports") }))
return app

@ -1,25 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local models = require "models"
local Announcements = models.announcements
local Boards = models.boards
function action:GET()
-- Get Board
local board = assert_error(Boards:get(self.params.uri_board))
-- Get Announcements
local announcements = Announcements:get_board(board.id)
for _, announcement in ipairs(announcements) do
Announcements:format_from_db(announcement)
end
return {
status = ngx.HTTP_OK,
json = announcements
}
end
return action

@ -1,24 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local models = require "models"
local Boards = models.boards
local Threads = models.threads
function action:GET()
local board = assert_error(Boards:get(self.params.uri_name))
-- Get Threads
local threads = board:get_archived()
for _, thread in ipairs(threads) do
--Threads:format_from_db(thread)
end
return {
status = ngx.HTTP_OK,
json = threads
}
end
return action

@ -1,29 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local role = require "utils.role"
local models = require "models"
local Bans = models.bans
local Boards = models.boards
function action:GET()
-- Verify the User's permissions
assert_error(role.mod(self.api_user))
-- Get Board
local board = assert_error(Boards:get(self.params.uri_board))
-- Get Bans
local bans = Bans:get_board(board.id)
for _, ban in ipairs(bans) do
Bans:format_from_db(ban)
end
return {
status = ngx.HTTP_OK,
json = bans
}
end
return action

@ -1,114 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local yield_error = require("lapis.application").yield_error
local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role"
local models = require "models"
local Boards = models.boards
function action:GET()
-- Get Board
local board = assert_error(Boards:get(self.params.uri_board))
Boards:format_from_db(board)
return {
status = ngx.HTTP_OK,
json = board
}
end
function action:PUT()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Validate parameters
local params = {
name = self.params.name,
title = self.params.title,
subtext = self.params.subtext,
rules = self.params.rules,
ban_message = self.params.ban_message,
anon_name = self.params.anon_name,
theme = self.params.theme,
total_posts = tonumber(self.params.total_posts),
pages = tonumber(self.params.pages),
threads_per_page = tonumber(self.params.threads_per_page),
anon_only = self.params.anon_only,
text_only = self.params.text_only,
draw = self.params.draw,
thread_file = self.params.thread_file,
thread_comment = self.params.thread_comment,
thread_file_limit = self.params.thread_file_limit,
post_file = self.params.post_file,
post_comment = self.params.post_comment,
post_limit = tonumber(self.params.post_limit),
archive = self.params.archive,
archive_time = tonumber(self.params.archive_time),
group = self.params.group,
filetype_image = self.params.filetype_image,
filetype_audio = self.params.filetype_audio
}
trim_filter(params)
Boards:format_to_db(params)
assert_valid(params, Boards.valid_record)
-- Check if board being modified is reusing name and title
local reuse_name = false
local reuse_title = false
if params.name or params.title then
local board = Boards:get(self.params.uri_board)
reuse_name = board.name == params.name
reuse_title = board.title == params.title
end
-- Verify unique or current name and title
if not (reuse_name and reuse_title) then
local boards = Boards:get_all()
for _, board in ipairs(boards) do
if not reuse_name and board.name == params.name then
yield_error("FIXME")
end
if not reuse_title and board.title == params.title then
yield_error("FIXME")
end
end
end
-- Modify board
local board = assert_error(Boards:modify(params, self.params.uri_board))
Boards:format_from_db(board)
return {
status = ngx.HTTP_OK,
json = board
}
end
function action:DELETE()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Delete board
local board = assert_error(Boards:delete(self.params.uri_board))
-- TODO: delete adjacent data (announcements, bans, threads, posts, files)
-- probably best done by adding actual FK constraints with "ON DELETE CASCADE"
return {
status = ngx.HTTP_OK,
json = {
id = board.id,
name = board.name,
title = board.title
}
}
end
return action

@ -1,79 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local yield_error = require("lapis.application").yield_error
local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role"
local models = require "models"
local Boards = models.boards
function action.GET()
-- Get Boards
local boards = assert_error(Boards:get_all())
for _, board in ipairs(boards) do
Boards:format_from_db(board)
end
return {
status = ngx.HTTP_OK,
json = boards
}
end
function action:POST()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Validate parameters
local params = {
name = self.params.name,
title = self.params.title,
subtext = self.params.subtext,
rules = self.params.rules,
ban_message = self.params.ban_message,
anon_name = self.params.anon_name,
theme = self.params.theme,
total_posts = tonumber(self.params.total_posts),
pages = tonumber(self.params.pages),
threads_per_page = tonumber(self.params.threads_per_page),
anon_only = self.params.anon_only,
text_only = self.params.text_only,
draw = self.params.draw,
thread_file = self.params.thread_file,
thread_comment = self.params.thread_comment,
thread_file_limit = self.params.thread_file_limit,
post_file = self.params.post_file,
post_comment = self.params.post_comment,
post_limit = tonumber(self.params.post_limit),
archive = self.params.archive,
archive_time = tonumber(self.params.archive_time),
group = self.params.group,
filetype_image = self.params.filetype_image,
filetype_audio = self.params.filetype_audio
}
trim_filter(params)
Boards:format_to_db(params)
assert_valid(params, Boards.valid_record)
-- Verify unique name and title
local boards = Boards:get_all()
for _, board in ipairs(boards) do
if board.name == params.name or board.title == params.title then
yield_error("FIXME")
end
end
-- Create board
local board = assert_error(Boards:new(params))
Boards:format_from_db(board)
return {
status = ngx.HTTP_OK,
json = board
}
end
return action

@ -1,92 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter
local models = require "models"
local Boards = models.boards
local Threads = models.threads
local Posts = models.posts
function action:GET()
local board = assert_error(Boards:get(self.params.uri_name))
-- Get Post
local post = assert_error(Posts:get(board.id, self.params.uri_id))
--Posts:format_from_db(post)
return {
status = ngx.HTTP_OK,
json = post
}
end
function action:PUT()
-- Validate parameters
local params = {
comment = self.params.comment,
subject = self.params.subject,
file_spoiler = self.params.file_spoiler
}
Posts:format_to_db(params)
trim_filter(params)
assert_valid(params, Posts.valid_record)
-- Modify post
local post = assert_error(Posts:modify(params))
Posts:format_from_db(post)
return {
status = ngx.HTTP_OK,
json = post
}
end
function action:DELETE()
--[[ FIXME: needs proper auth!
-- MODS = FAGS
if type(session) == "table" and
(session.admin or session.mod or session.janitor) then
rm_post(board.name)
success = true
-- Override password
elseif type(session) == "string" and
session == "override" then
rm_post(board.name)
success = true
-- Password has to match!
elseif post and session.password and
post.password == session.password then
rm_post(board.name)
success = true
end
--]]
-- Delete post
local post = assert_error(Posts:get_post_by_id(self.params.uri_id))
local thread = post:get_thread()
local op = thread:get_op()
if post.id == op.id then
assert_error(Threads:delete(thread.id))
local posts = thread:get_posts()
for _, p in ipairs(posts) do
assert_error(Posts:delete(p.id))
end
else
assert_error(Posts:delete(post.id))
end
return {
status = ngx.HTTP_OK,
json = {
id = post.id
}
}
end
return action

@ -1,21 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local models = require "models"
local Boards = models.boards
function action:GET()
-- Get Board
local board = assert_error(Boards:get(self.params.uri_name))
-- Get Reports
local reports = board:get_reports()
return {
status = ngx.HTTP_OK,
json = reports
}
end
return action

@ -1,105 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter
local models = require "models"
local Boards = models.boards
local Threads = models.threads
local Posts = models.posts
function action:GET()
local posts
if self.params.uri_thread then
local thread = assert_error(Threads:get(self.params.uri_thread))
posts = thread:get_posts()
else
local board = assert_error(Boards:get(self.params.uri_board))
posts = board:get_posts()
end
return {
status = ngx.HTTP_OK,
json = posts
}
end
function action:POST()
local now = os.time()
local board = assert_error(Boards:get(self.params.uri_name))
local thread = Threads:get(self.params.uri_id)
local op = thread and false or true
-- Create a new thread if no thread exists
if not thread then
-- Validate parameters
local params = {
board_id = board.id,
last_active = now,
sticky = self.params.sticky,
lock = self.params.lock,
size_override = self.params.size_override,
save = self.params.save
}
-- Only admins and mods can flag threads
-- FIXME: API has no session, need proper auth!
if not self.session.admin or self.session.mod then
params.sticky = nil
params.lock = nil
params.size_override = nil
params.save = nil
end
--Threads:format_to_db(params)
trim_filter(params)
assert_valid(params, Threads.valid_record)
-- Create thread
thread = assert_error(Threads:new(params))
--Threads:format_from_db(thread)
end
-- FIXME: there needs to be a better way to do this to avoid race conditions...
board.total_posts = board.total_posts + 1
-- Validate parameters
local params = {
post_id = board.total_posts,
thread_id = thread.id,
board_id = board.id,
timestamp = now,
ip = self.params.ip,
comment = self.params.comment,
name = self.params.name,
trip = self.params.trip,
subject = self.params.subject,
password = self.params.password,
file_name = self.params.file_name,
file_path = self.params.file_path,
file_type = self.params.file_type,
file_md5 = self.params.file_md5,
file_size = self.params.file_size,
file_width = self.params.file_width,
file_height = self.params.file_height,
file_duration = self.params.file_duration,
file_spoiler = self.params.file_spoiler,
file_content = self.params.file_content -- FIXME: we probably want to base64 decode this in format_to_db
}
Posts:format_to_db(params)
trim_filter(params)
assert_valid(params, Posts.valid_record)
-- Create post
local post = assert_error(Posts:new(params, board, op))
Posts:format_from_db(post)
return {
status = ngx.HTTP_OK,
json = post
}
end
return action

@ -1,29 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local role = require "utils.role"
local models = require "models"
local Boards = models.boards
local Posts = models.posts
function action:GET()
-- Verify the User's permissions
assert_error(role.mod(self.api_user))
-- Get Board
local board = assert_error(Boards:get(self.params.uri_board))
-- Get Reported posts
local posts = Posts:get_board_reports(board.id)
for _, post in ipairs(posts) do
Posts:format_from_db(post)
end
return {
status = ngx.HTTP_OK,
json = posts
}
end
return action

@ -1,69 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local models = require "models"
local Threads = models.threads
function action:GET()
-- Get Thread
local thread = assert_error(Threads:get(self.params.uri_id))
--Threads:format_from_db(thread)
return {
status = ngx.HTTP_OK,
json = thread
}
end
function action:PUT()
local params = { id=self.params.uri_id }
-- Extract flag from URI
if self.params.uri_value:lower() == "true" or
self.params.uri_value:lower() == "t" or
tonumber(self.params.uri_value) == 1 then
self.params.uri_value = true
elseif self.params.uri_value:lower() == "false" or
self.params.uri_value:lower() == "f" or
tonumber(self.params.uri_value) == 0 then
self.params.uri_value = false
else
return {
status = ngx.HTTP_BAD_REQUEST,
json = {}
}
end
-- Extract variable from URI
if self.params.uri_action == "sticky" then
params.sticky = self.params.uri_value
elseif self.params.uri_action == "lock" then
params.lock = self.params.uri_value
elseif self.params.uri_action == "save" then
params.save = self.params.uri_value
elseif self.params.uri_action == "size_override" then
params.size_override = self.params.uri_value
else
return {
status = ngx.HTTP_BAD_REQUEST,
json = {}
}
end
-- Modify thread
local thread = assert_error(Threads:modify(params))
--Threads:format_from_db(thread)
return {
status = ngx.HTTP_OK,
json = thread
}
end
function action:DELETE()
end
return action

@ -1,21 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local models = require "models"
local Boards = models.boards
function action:GET()
-- Get Board
local board = assert_error(Boards:get(self.params.uri_name))
-- Get Reports
local reports = board:get_reports()
return {
status = ngx.HTTP_OK,
json = reports
}
end
return action

@ -1,35 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local models = require "models"
local Boards = models.boards
local Threads = models.threads
function action:GET()
local threads, pages
local board = assert_error(Boards:get(self.params.uri_name))
-- Get Threads
if self.params.uri_page then
local paginator = board:get_threads_paginated({ per_page=board.threads_per_page })
threads = paginator:get_page(self.params.uri_page)
pages = paginator:num_pages()
else
threads = board:get_threads()
end
for _, thread in ipairs(threads) do
--Threads:format_from_db(thread)
end
return {
status = ngx.HTTP_OK,
json = {
threads = threads,
pages = pages
}
}
end
return action

@ -1,13 +1,19 @@
local lapis = require "lapis" local lapis = require "lapis"
local capture = require("lapis.application").capture_errors_json local capture = require("lapis.application").capture_errors_json
local r2 = require("lapis.application").respond_to local r2 = require("lapis.application").respond_to
local handle = require("utils.error").handle local handle = require("utils.error").handle
local app = lapis.Application() local app = lapis.Application()
app.__base = app app.__base = app
app.name = "api.core." app.name = "api.core."
app.path = "/api" app.path = "/api"
app:match("root", "", capture({ on_error=handle, r2(require "apps.api.core.root") })) app:match("root", "", capture({
app:match("login", "/login", capture({ on_error=handle, r2(require "apps.api.core.login") })) on_error = handle,
r2(require "apps.api.core.root"),
}))
app:match("login", "/login", capture({
on_error = handle,
r2(require "apps.api.core.login"),
}))
return app return app

@ -1,35 +1,35 @@
local ngx = _G.ngx local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base") local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error local assert_error = require("lapis.application").assert_error
local yield_error = require("lapis.application").yield_error local yield_error = require("lapis.application").yield_error
local models = require "models" local models = require "models"
local Users = models.users local Users = models.users
function action:POST() function action:POST()
-- Normally we'd process these inputs a bit but in the case of -- Normally we'd process these inputs a bit but in the case of
-- authentication credentials, we want to use the raw user inputs. -- authentication credentials, we want to use the raw user inputs.
local params = { local params = {
username = self.params.username, username = self.params.username,
password = self.params.password password = self.params.password,
} }
-- Early exit if credentials not sent -- Early exit if credentials not sent
if not params.username or not params.password then if not params.username or not params.password then
yield_error("FIXME") yield_error("FIXME")
end end
local user = assert_error(Users:login(params)) local user = assert_error(Users:login(params))
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = { json = {
id = user.id, id = user.id,
username = user.username, username = user.username,
role = user.role, role = user.role,
api_key = user.api_key api_key = user.api_key,
} },
} }
end end
return action return action

@ -1,11 +1,11 @@
local ngx = _G.ngx local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base") local action = setmetatable({}, require "apps.api.internal.action_base")
function action.GET() function action.GET()
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = {} json = {},
} }
end end
return action return action

@ -1,17 +1,17 @@
local ngx = _G.ngx local ngx = _G.ngx
local action = {} local action = {}
local function errors() local function errors()
return { return {
status = ngx.HTTP_NOT_ALLOWED, status = ngx.HTTP_NOT_ALLOWED,
json = {} json = {},
} }
end end
action.__index = action action.__index = action
action.GET = errors action.GET = errors
action.POST = errors action.POST = errors
action.PUT = errors action.PUT = errors
action.DELETE = errors action.DELETE = errors
return action return action

@ -1,41 +1,41 @@
local assert_error = require("lapis.application").assert_error local assert_error = require("lapis.application").assert_error
local yield_error = require("lapis.application").yield_error local yield_error = require("lapis.application").yield_error
local mime = require "mime" local mime = require "mime"
local models = require "models" local models = require "models"
local Users = models.users local Users = models.users
return function(self) return function(self)
if self.req.headers["Authorization"] then if self.req.headers["Authorization"] then
-- Decode auth info -- Decode auth info
local auth = mime.unb64(self.req.headers["Authorization"]:sub(7)) local auth = mime.unb64(self.req.headers["Authorization"]:sub(7))
local username, api_key = auth:match("^(.+)%:(.+)$") local username, api_key = auth:match("^(.+)%:(.+)$")
-- DENY if Authorization is malformed -- DENY if Authorization is malformed
if not username or not api_key then if not username or not api_key then
yield_error("FIXME: Corrupt auth!") yield_error("FIXME: Corrupt auth!")
end end
-- DENY if a user's key isn't properly set -- DENY if a user's key isn't properly set
if api_key == Users.default_key then if api_key == Users.default_key then
yield_error("FIXME: Bad auth!") yield_error("FIXME: Bad auth!")
end end
local params = { local params = {
username = username, username = username,
api_key = api_key api_key = api_key,
} }
-- Get User -- Get User
self.api_user = assert_error(Users:get_api(params)) self.api_user = assert_error(Users:get_api(params))
Users:format_from_db(self.api_user) Users:format_from_db(self.api_user)
return return
end end
-- Set basic User -- Set basic User
self.api_user = { self.api_user = {
id = -1, id = -1,
role = -1 role = -1,
} }
end end

@ -1,20 +1,20 @@
local i18n = require "i18n" local i18n = require "i18n"
local lfs = require "lfs" local lfs = require "lfs"
return function(self) return function(self)
-- Set locale -- Set locale
self.i18n = i18n self.i18n = i18n
local locale = self.req.headers["Content-Language"] or "en" local locale = self.req.headers["Content-Language"] or "en"
i18n.setLocale(locale) i18n.setLocale(locale)
i18n.loadFile("src/locale/en.lua") i18n.loadFile("src/locale/en.lua")
-- Get locale file -- Get locale file
local path = "src/locale" local path = "src/locale"
for file in lfs.dir(path) do for file in lfs.dir(path) do
local name, ext = string.match(file, "^(.+)%.(.+)$") local name, ext = string.match(file, "^(.+)%.(.+)$")
if name == locale and ext == "lua" then if name == locale and ext == "lua" then
i18n.loadFile(string.format("%s/%s.lua", path, name)) i18n.loadFile(string.format("%s/%s.lua", path, name))
end end
end end
end end

@ -1,13 +0,0 @@
local lapis = require "lapis"
local capture = require("lapis.application").capture_errors_json
local r2 = require("lapis.application").respond_to
local handle = require("utils.error").handle
local app = lapis.Application()
app.__base = app
app.name = "api.pages."
app.path = "/api/pages"
app:match("pages", "", capture({ on_error=handle, r2(require "apps.api.pages.pages") }))
app:match("page", "/:uri_page", capture({ on_error=handle, r2(require "apps.api.pages.page") }))
return app

@ -1,60 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role"
local models = require "models"
local Pages = models.pages
function action:GET()
local page = assert_error(Pages:get(self.params.uri_page))
return {
status = ngx.HTTP_OK,
json = page
}
end
function action:PUT()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Validate parameters
local params = {
slug = self.params.slug,
title = self.params.title,
content = self.params.content
}
trim_filter(params)
Pages:format_to_db(params)
assert_valid(params, Pages.valid_record)
-- Modify page
local page = assert_error(Pages:modify(params, self.params.uri_page))
return {
status = ngx.HTTP_OK,
json = page
}
end
function action:DELETE()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Delete Page
local page = assert_error(Pages:delete(self.params.uri_page))
return {
status = ngx.HTTP_OK,
json = {
slug = page.slug,
title = page.title
}
}
end
return action

@ -1,45 +0,0 @@
local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role"
local models = require "models"
local Pages = models.pages
function action.GET()
-- Get all Pages
local pages = assert_error(Pages:get_all())
return {
status = ngx.HTTP_OK,
json = pages
}
end
function action:POST()
-- Verify the User's permissions
assert_error(role.admin(self.api_user))
-- Validate parameters
local params = {
slug = self.params.slug,
title = self.params.title,
content = self.params.content,
}
trim_filter(params)
Pages:format_to_db(params)
assert_valid(params, Pages.valid_record)
-- Create Page
local page = assert_error(Pages:new(params))
return {
status = ngx.HTTP_OK,
json = page
}
end
return action

@ -1,13 +1,19 @@
local lapis = require "lapis" local lapis = require "lapis"
local capture = require("lapis.application").capture_errors_json local capture = require("lapis.application").capture_errors_json
local r2 = require("lapis.application").respond_to local r2 = require("lapis.application").respond_to
local handle = require("utils.error").handle local handle = require("utils.error").handle
local app = lapis.Application() local app = lapis.Application()
app.__base = app app.__base = app
app.name = "api.users." app.name = "api.users."
app.path = "/api/users" app.path = "/api/users"
app:match("users", "", capture({ on_error=handle, r2(require "apps.api.users.users") })) app:match("users", "", capture({
app:match("user", "/:uri_user", capture({ on_error=handle, r2(require "apps.api.users.user") })) on_error = handle,
r2(require "apps.api.users.users"),
}))
app:match("user", "/:uri_user", capture({
on_error = handle,
r2(require "apps.api.users.user"),
}))
return app return app

@ -1,108 +1,108 @@
local ngx = _G.ngx local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base") local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error local assert_error = require("lapis.application").assert_error
local yield_error = require("lapis.application").yield_error local yield_error = require("lapis.application").yield_error
local assert_valid = require("lapis.validate").assert_valid local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role" local role = require "utils.role"
local models = require "models" local models = require "models"
local Users = models.users local Users = models.users
function action:GET() function action:GET()
local user = assert_error(Users:get(self.params.uri_user)) local user = assert_error(Users:get(self.params.uri_user))
Users:format_from_db(user) Users:format_from_db(user)
-- Verify the User's permissions -- Verify the User's permissions
local is_admin = role.admin(self.api_user) local is_admin = role.admin(self.api_user)
local is_user = self.api_user.id == user.id local is_user = self.api_user.id == user.id
if not is_admin and not is_user then if not is_admin and not is_user then
yield_error("FIXME") yield_error("FIXME")
end end
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = user json = user,
} }
end end
function action:PUT() function action:PUT()
local user = assert_error(Users:get(self.params.uri_user)) local user = assert_error(Users:get(self.params.uri_user))
-- Verify the User's permissions -- Verify the User's permissions
local is_admin = role.admin(self.api_user) local is_admin = role.admin(self.api_user)
local is_user = self.api_user.id == user.id local is_user = self.api_user.id == user.id
local is_auth = self.api_user.role > user.role local is_auth = self.api_user.role > user.role
if (not is_admin and not is_user) or not is_auth then if (not is_admin and not is_user) or not is_auth then
yield_error("FIXME") yield_error("FIXME")
end end
-- Validate parameters -- Validate parameters
local params = { local params = {
username = self.params.username, username = self.params.username,
password = self.params.password, password = self.params.password,
confirm = self.params.confirm, confirm = self.params.confirm,
role = tonumber(self.params.role), role = tonumber(self.params.role),
api_key = self.params.api_key api_key = self.params.api_key,
} }
trim_filter(params) trim_filter(params)
Users:format_to_db(params) Users:format_to_db(params)
assert_valid(params, Users.valid_record) assert_valid(params, Users.valid_record)
-- If no role was sent, don't update it -- If no role was sent, don't update it
-- This is kind of dumb since we're just setting it from nil to -1 and back -- This is kind of dumb since we're just setting it from nil to -1 and back
-- to nil, but I want to keep the format_to_db in case of future formatting -- to nil, but I want to keep the format_to_db in case of future formatting
-- concerns. -- concerns.
if params.role == Users.role.INVALID then if params.role == Users.role.INVALID then
params.role = nil params.role = nil
end end
if params.role then if params.role then
-- Only admins can change a role -- Only admins can change a role
if not is_admin then if not is_admin then
yield_error("FIXME") yield_error("FIXME")
end end
-- Cannot elevate to or above own role -- Cannot elevate to or above own role
if self.api_user.role <= params.role then if self.api_user.role <= params.role then
yield_error("FIXME") yield_error("FIXME")
end end
end end
-- Modify User -- Modify User
user = assert_error(Users:modify(params, self.params.uri_user, self.params.password)) user = assert_error(Users:modify(params, self.params.uri_user, self.params.password))
Users:format_from_db(user) Users:format_from_db(user)
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = user json = user,
} }
end end
function action:DELETE() function action:DELETE()
local user = assert_error(Users:get(self.params.uri_user)) local user = assert_error(Users:get(self.params.uri_user))
-- Verify the User's permissions -- Verify the User's permissions
local is_admin = role.admin(self.api_user) local is_admin = role.admin(self.api_user)
local is_user = self.api_user.id == user.id local is_user = self.api_user.id == user.id
local is_auth = self.api_user.role > user.role local is_auth = self.api_user.role > user.role
if not is_admin and not is_user and not is_auth then if not is_admin and not is_user and not is_auth then
yield_error("FIXME") yield_error("FIXME")
end end
-- Delete User -- Delete User
user = assert_error(Users:delete(self.params.uri_user)) user = assert_error(Users:delete(self.params.uri_user))
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = { json = {
id = user.id, id = user.id,
username = user.username username = user.username,
} },
} }
end end
return action return action

@ -1,64 +1,64 @@
local ngx = _G.ngx local ngx = _G.ngx
local action = setmetatable({}, require "apps.api.internal.action_base") local action = setmetatable({}, require "apps.api.internal.action_base")
local assert_error = require("lapis.application").assert_error local assert_error = require("lapis.application").assert_error
local yield_error = require("lapis.application").yield_error local yield_error = require("lapis.application").yield_error
local assert_valid = require("lapis.validate").assert_valid local assert_valid = require("lapis.validate").assert_valid
local trim_filter = require("lapis.util").trim_filter local trim_filter = require("lapis.util").trim_filter
local role = require "utils.role" local role = require "utils.role"
local models = require "models" local models = require "models"
local Users = models.users local Users = models.users
function action:GET() function action:GET()
-- Verify the User's permissions -- Verify the User's permissions
assert_error(role.admin(self.api_user)) assert_error(role.admin(self.api_user))
-- Get all Users -- Get all Users
local users = assert_error(Users:get_all()) local users = assert_error(Users:get_all())
for _, user in ipairs(users) do for _, user in ipairs(users) do
Users:format_from_db(user) Users:format_from_db(user)
end end
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = users json = users,
} }
end end
function action:POST() function action:POST()
-- Verify the User's permissions -- Verify the User's permissions
assert_error(role.admin(self.api_user)) assert_error(role.admin(self.api_user))
-- Validate parameters -- Validate parameters
local params = { local params = {
username = self.params.username, username = self.params.username,
password = self.params.password, password = self.params.password,
confirm = self.params.confirm, confirm = self.params.confirm,
role = tonumber(self.params.role) role = tonumber(self.params.role),
} }
trim_filter(params) trim_filter(params)
Users:format_to_db(params) Users:format_to_db(params)
assert_valid(params, Users.valid_record) assert_valid(params, Users.valid_record)
-- DENY if no role was sent -- DENY if no role was sent
if params.role == Users.role.INVALID then if params.role == Users.role.INVALID then
yield_error("FIXME") yield_error("FIXME")
end end
-- Cannot elevate to or above own role -- Cannot elevate to or above own role
if self.api_user.role <= params.role then if self.api_user.role <= params.role then
yield_error("FIXME") yield_error("FIXME")
end end
-- Create user -- Create user
local user = assert_error(Users:new(params, self.params.password)) local user = assert_error(Users:new(params, self.params.password))
Users:format_from_db(user) Users:format_from_db(user)
return { return {
status = ngx.HTTP_OK, status = ngx.HTTP_OK,
json = user json = user,
} }
end end
return action return action

@ -1,16 +0,0 @@
local lapis = require "lapis"
local app = lapis.Application()
app.__base = app
app.include = function(self, a)
self.__class.include(self, a, nil, self)
end
app:before_filter(require "apps.web.internal.config_site")
app:before_filter(require "apps.web.internal.check_auth")
app:before_filter(require "apps.web.internal.check_ban")
app:include("apps.web.admin")
app:include("apps.web.pages")
app:include("apps.web.boards")
return app

@ -1,16 +0,0 @@
local lapis = require "lapis"
local r2 = require("lapis.application").respond_to
local app = lapis.Application()
app.__base = app
app.name = "web.admin."
app.path = "/admin"
app.handle_404 = require "apps.web.internal.code_404"
app:match("index", "", r2(require "apps.web.admin.index"))
app:match("users", "/:action/user(/:user)", r2(require "apps.web.admin.user"))
app:match("boards", "/:action/board(/:uri_name)", r2(require "apps.web.admin.board"))
app:match("announcements", "/:action/announcement(/:ann)", r2(require "apps.web.admin.announcement"))
app:match("pages", "/:action/page(/:page)", r2(require "apps.web.admin.page"))
app:match("reports", "/:action/report(/:report)", r2(require "apps.web.admin.report"))
return app

@ -1,128 +0,0 @@
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local csrf = require "lapis.csrf"
local capture = require "utils.capture"
local generate = require "utils.generate"
return {
before = function(self)
-- Get announcements
self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcements")))
-- Display a theme
self.board = { theme = "yotsuba_b" }
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Page title
self.page_title = self.i18n("admin_panel")
-- Verify Authorization
if self.session.name then
if not self.session.admin then
assert_error(false, "err_not_admin")
end
else
return
end
-- Display creation form
if self.params.action == "create" then
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("create_ann")
)
self.announcement = self.params
return
end
-- Display modification form
if self.params.action == "modify" then
self.announcement = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id=self.params.uri_id })))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("modify_ann")
)
return
end
-- Delete announcement
if self.params.action == "delete" then
self.announcement = assert_error(capture.delete(self:url_for("api.announcements.announcement", { uri_id=self.params.uri_id })))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("deleted_ann", { self.announcement.text })
return
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "create" then
return { render = "admin.announcement" }
elseif self.params.action == "modify" then
return { render = "admin.announcement" }
elseif self.params.action == "delete" then
return { render = "admin.admin" }
end
end,
GET = function(self)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "create" then
return { render = "admin.announcement" }
elseif self.params.action == "modify" then
return { render = "admin.announcement" }
elseif self.params.action == "delete" then
return { render = "admin.success" }
end
end,
POST = function(self)
-- Validate CSRF token
csrf.assert_token(self)
-- Validate user input
assert_valid(self.params, {
{ "text", max_length=255, exists=true }
})
-- Create announcement
if self.params.create_announcement then
self.announcement = assert_error(capture.post(self:url_for("api.announcements.announcements"), self.params))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("created_ann", { self.announcement.text })
return { render = "admin.success" }
end
-- Modify announcement
if self.params.modify_announcement then
self.announcement = assert_error(capture.put(self:url_for("api.announcements.announcement", { uri_id=self.params.uri_id }), self.params))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("modified_ann", { self.announcement.text })
return { render = "admin.success" }
end
return { redirect_to = self:url_for("web.admin.index") }
end
}

@ -1,136 +0,0 @@
local assert_error = require("lapis.application").assert_error
local csrf = require "lapis.csrf"
local lfs = require "lfs"
local capture = require "utils.capture"
local generate = require "utils.generate"
return {
before = function(self)
-- Display a theme
self.board = { theme = "yotsuba_b" }
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Page title
self.page_title = self.i18n("admin_panel")
-- Verify Authorization
if not self.session.name then return end
if not self.session.admin then
assert_error(false, "err_not_admin")
end
-- Get list of themes
self.themes = {}
for file in lfs.dir("./static/css") do
local name, ext = string.match(file, "^(.+)%.(.+)$")
if name ~= "reset" and
name ~= "posts" and
name ~= "style" and
name ~= "tegaki" and
ext == "css" then
table.insert(self.themes, name)
end
end
-- Display creation form
if self.params.action == "create" then
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("create_board")
)
self.board = self.params
if not self.board.theme then
self.board.theme = "yotsuba_b"
end
return
end
-- Display modification form
if self.params.action == "modify" then
self.board = assert_error(capture.get(self:url_for("api.boards.board", { uri_name=self.params.uri_name })))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("modify_board")
)
return
end
-- Delete board
if self.params.action == "delete" then
local board = assert_error(capture.delete(self:url_for("api.boards.board", { uri_name=self.params.uri_name })))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("deleted_board", { board.name, board.title })
return
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "create" then
return { render = "admin.board" }
elseif self.params.action == "modify" then
return { render = "admin.board" }
elseif self.params.action == "delete" then
return { render = "admin.admin" }
end
end,
GET = function(self)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "create" then
return { render = "admin.board" }
elseif self.params.action == "modify" then
return { render = "admin.board" }
elseif self.params.action == "delete" then
return { render = "admin.success" }
end
end,
POST = function(self)
-- Validate CSRF token
csrf.assert_token(self)
-- Create new board
if self.params.create_board then
local board = assert_error(capture.post(self:url_for("api.boards.boards"), self.params))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("created_board", { board.name, board.title })
return { render = "admin.success" }
end
-- Modify board
if self.params.modify_board then
local board = assert_error(capture.put(self:url_for("api.boards.board", { uri_name=self.params.uri_name }), self.params))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("modified_board", { board.name, board.title })
return { render = "admin.success" }
end
return { redirect_to = self:url_for("web.admin.index") }
end
}

@ -1,138 +0,0 @@
local assert_error = require("lapis.application").assert_error
local csrf = require "lapis.csrf"
local capture = require "utils.capture"
local generate = require "utils.generate"
local Boards = require "models.boards"
local Pages = require "models.pages"
local Posts = require "models.posts"
local Reports = require "models.reports"
local Users = require "models.users"
return {
before = function(self)
-- Get data
self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcements")))
self.pages = Pages:get_pages()
self.reports = Reports:get_reports()
self.users = Users:get_users()
-- Display a theme
self.board = { theme = "yotsuba_b" }
-- Page title
self.page_title = self.i18n("admin_panel")
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Verify Authorization
if self.session.name then
if not self.session.admin then
assert_error(false, "err_not_admin")
end
else
return
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
if not self.session.name then
return { render = "admin.login" }
end
return { render = "admin.admin" }
end,
GET = function(self)
if not self.session.name then
return { render = "admin.login" }
end
return { render = "admin.admin" }
end,
POST = function(self)
-- Validate CSRF token
csrf.assert_token(self)
-- Verify user credentials
if self.params.login then
-- Verify user
local user = assert_error(Users:verify_user(self.params))
-- Set username
self.session.name = user.username
return { redirect_to = self.admin_url }
end
-- Must be logged in as an admin!
if self.session.admin then
-- Redirect to modify user page
if self.params.modify_user then
return { redirect_to = self:url_for("web.admin.users", { action="modify", user=self.params.user }) }
end
-- Redirect to delete user page
if self.params.delete_user then
return { redirect_to = self:url_for("web.admin.users", { action="delete", user=self.params.user }) }
end
-- Redirect to modify board page
if self.params.modify_board then
return { redirect_to = self:url_for("web.admin.boards", { action="modify", uri_name=self.params.board }) }
end
-- Redirect to delete board page
if self.params.delete_board then
return { redirect_to = self:url_for("web.admin.boards", { action="delete", uri_name=self.params.board }) }
end
-- Redirect to modify announcement page
if self.params.modify_announcement then
return { redirect_to = self:url_for("web.admin.announcements", { action="modify", ann=self.params.ann }) }
end
-- Redirect to delete announcement page
if self.params.delete_announcement then
return { redirect_to = self:url_for("web.admin.announcements", { action="delete", ann=self.params.ann }) }
end
-- Redirect to modify page page
if self.params.modify_page then
return { redirect_to = self:url_for("web.admin.pages", { action="modify", page=self.params.page }) }
end
-- Redirect to delete page page
if self.params.delete_page then
return { redirect_to = self:url_for("web.admin.pages", { action="delete", page=self.params.page }) }
end
-- Redirect to reported post
if self.params.view_report then
local report = Reports:get_report_by_id(self.params.report)
local board = Boards:get_board(report.board_id)
local post = Posts:get_post(board.id, report.post_id)
local op = Posts:get_thread_op(report.thread_id)
return { redirect_to = self:url_for("web.boards.thread", { board=board.name, thread=op.post_id, anchor="p", id=post.post_id }) }
end
-- Redirect to delete report page
if self.params.delete_report then
return { redirect_to = self:url_for("web.admin.reports", { action="delete", report=self.params.report }) }
end
-- Regenerate thumbnails
if self.params.regen_thumbs then
Boards:regen_thumbs()
end
return { render = "admin.admin" }
end
return { render = "admin.login" }
end
}

@ -1,164 +0,0 @@
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local csrf = require "lapis.csrf"
local generate = require "utils.generate"
local Boards = require "models.boards"
local Pages = require "models.pages"
return {
before = function(self)
-- Get all board data
self.boards = Boards:get_boards()
-- Get all page data
self.pages = Pages:get_pages()
-- Display a theme
self.board = { theme = "yotsuba_b" }
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Page title
self.page_title = self.i18n("admin_panel")
-- Verify Authorization
if self.session.name then
if not self.session.admin then
assert_error(false, "err_not_admin")
end
else
return
end
-- Display creation form
if self.params.action == "create" then
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("create_page")
)
self.page = self.params
return
end
-- Display modification form
if self.params.action == "modify" then
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("modify_page")
)
self.page = Pages:get_page(self.params.page)
return
end
-- Delete page
if self.params.action == "delete" then
local page = Pages:get_page(self.params.page)
assert_error(Pages:delete_page(page))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("deleted_page", { page.slug, page.title })
return
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "create" then
return { render = "admin.page" }
elseif self.params.action == "modify" then
return { render = "admin.page" }
elseif self.params.action == "delete" then
return { render = "admin.admin" }
end
end,
GET = function(self)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "create" then
return { render = "admin.page" }
elseif self.params.action == "modify" then
return { render = "admin.page" }
elseif self.params.action == "delete" then
return { render = "admin.success" }
end
end,
POST = function(self)
-- Validate CSRF token
csrf.assert_token(self)
-- Validate user input
assert_valid(self.params, {
{ "slug", max_length=255, exists=true },
{ "title", max_length=255, exists=true }
})
-- Create new page
if self.params.create_page then
local sl = string.lower
-- Verify unique names
for _, page in ipairs(self.pages) do
if sl(page.slug) == sl(self.params.slug) then
assert_error(false, "err_slug_used")
end
end
-- Create page
local page = assert_error(Pages:create_page(self.params))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("created_page", { page.slug, page.title })
return { render = "admin.success" }
end
-- Modify page
if self.params.modify_page then
local discard = {
"page",
"modify_page",
"ip",
"action",
"csrf_token",
"old"
}
local page = Pages:get_page(self.params.old)
-- Fill in board with new data
for k, param in pairs(self.params) do
page[k] = param
end
-- Get rid of form trash
for _, param in ipairs(discard) do
page[param] = nil
end
assert_error(Pages:modify_page(page))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("modified_page", { page.slug, page.title })
return { render = "admin.success" }
end
return { redirect_to = self:url_for("web.admin.index") }
end
}

@ -1,64 +0,0 @@
local assert_error = require("lapis.application").assert_error
local csrf = require "lapis.csrf"
local generate = require "utils.generate"
local Boards = require "models.boards"
local Reports = require "models.reports"
return {
before = function(self)
-- Get data
self.boards = Boards:get_boards()
self.reports = Reports:get_reports()
-- Display a theme
self.board = { theme = "yotsuba_b" }
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Page title
self.page_title = self.i18n("admin_panel")
-- Verify Authorization
if self.session.name then
if not self.session.admin then
assert_error(false, "err_not_admin")
end
else
return
end
-- Delete report
if self.params.action == "delete" then
local report = Reports:get_report_by_id(self.params.report)
assert_error(Reports:delete_report(report))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("deleted_report", { report.id })
return
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "delete" then
return { render = "admin.admin" }
end
end,
GET = function(self)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "delete" then
return { render = "admin.success" }
end
end,
POST = function(self)
return { redirect_to = self:url_for("web.admin.index") }
end
}

@ -1,190 +0,0 @@
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local csrf = require "lapis.csrf"
local generate = require "utils.generate"
local Boards = require "models.boards"
local Users = require "models.users"
return {
before = function(self)
-- Get all board data
self.boards = Boards:get_boards()
-- Get all user data
self.users = Users:get_users()
-- Display a theme
self.board = { theme = "yotsuba_b" }
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Page title
self.page_title = self.i18n("admin_panel")
-- Verify Authorization
if self.session.name then
if not self.session.admin then
assert_error(false, "err_not_admin")
end
else
return
end
-- Display creation form
if self.params.action == "create" then
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("create_user")
)
self.user = self.params
return
end
-- Display modification form
if self.params.action == "modify" then
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("modify_user")
)
self.user = Users:get_user_by_id(self.params.user)
return
end
-- Delete user
if self.params.action == "delete" then
local user = Users:get_user_by_id(self.params.user)
assert_error(Users:delete_user(user))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("deleted_user", { user.username })
return
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "create" then
return { render = "admin.user" }
elseif self.params.action == "modify" then
return { render = "admin.user" }
elseif self.params.action == "delete" then
return { render = "admin.admin" }
end
end,
GET = function(self)
if not self.session.name then
return { render = "admin.login" }
elseif self.params.action == "create" then
return { render = "admin.user" }
elseif self.params.action == "modify" then
return { render = "admin.user" }
elseif self.params.action == "delete" then
return { render = "admin.success" }
end
end,
POST = function(self)
-- Validate CSRF token
csrf.assert_token(self)
-- Create new user
if self.params.create_user then
local sl = string.lower
-- Validate user input
assert_valid(self.params, {
{ "username", exists=true, max_length=255 },
{ "new_password", exists=true, equals=self.params.retype_password },
{ "retype_password", exists=true }
})
self.params.password = self.params.new_password
-- Verify unique name
for _, user in ipairs(self.users) do
if sl(user.username) == sl(self.params.username) then
assert_error(false, "err_user_used")
end
end
-- Create user
local user = assert_error(Users:create_user(self.params))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("created_user", { user.username })
return { render = "admin.success" }
end
-- Modify user
if self.params.modify_user then
-- Validate user input
assert_valid(self.params, {
{ "username", exists=true, max_length=255 },
{ "new_password", equals=self.params.retype_password },
{ "retype_password", }
})
local discard = {
"user",
"modify_user",
"ip",
"action",
"csrf_token",
"old_password",
"new_password",
"retype_password",
}
local user = Users:get_user(self.params.username)
-- Validate user
if #self.params.old_password > 0 then
-- Validate user input
assert_valid(self.params, {
{ "new_password", exists=true },
{ "retype_password", exists=true }
})
-- TODO: verify user's old password in non-admin setting
self.params.password = self.params.new_password
end
-- Fill in board with new data
for k, param in pairs(self.params) do
user[k] = param
end
-- Get rid of form trash
for _, param in ipairs(discard) do
user[param] = nil
end
assert_error(Users:modify_user(user))
self.page_title = string.format(
"%s - %s",
self.i18n("admin_panel"),
self.i18n("success")
)
self.action = self.i18n("modified_user", { user.username })
return { render = "admin.success" }
end
return { redirect_to = self:url_for("web.admin.index") }
end
}

@ -1,14 +0,0 @@
local lapis = require "lapis"
local r2 = require("lapis.application").respond_to
local app = lapis.Application()
app.__base = app
app.name = "web.boards."
app.path = "/board"
app.handle_404 = require "apps.web.internal.code_404"
app:match("board", "/:uri_name(/page/:page)", r2(require "apps.web.boards.board"))
app:match("catalog", "/:uri_name/catalog", r2(require "apps.web.boards.catalog"))
app:match("archive", "/:uri_name/archive", require "apps.web.boards.archive")
app:match("thread", "/:uri_name/thread/:thread(#:anchor:id)", r2(require "apps.web.boards.thread"))
return app

@ -1,76 +0,0 @@
local assert_error = require("lapis.application").assert_error
local capture = require "utils.capture"
local format = require "utils.text_formatter"
local Posts = require "models.posts"
local Threads = require "models.threads"
return function(self)
-- Get board
for _, board in ipairs(self.boards) do
if board.name == self.params.uri_name then
self.board = board
break
end
end
-- Board not found
if not self.board then
return self:write({ redirect_to = self:url_for("web.pages.index") })
end
-- Get announcements
-- TODO: Consolidate these into a single call
self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id="global" })))
local board_announcements = assert_error(capture.get(self:url_for("api.boards.announcements", { uri_name=self.params.uri_name })))
for _, announcement in ipairs(board_announcements) do
table.insert(self.announcements, announcement)
end
-- Page title
self.page_title = string.format(
"/%s/ - %s",
self.board.name,
self.board.title
)
-- Nav links link to sub page if available
self.sub_page = "archive"
-- Get threads
self.threads = assert_error(capture.get(self:url_for("api.boards.archived", { uri_name=self.params.uri_name })))
-- Get time
self.days = math.floor(self.board.archive_time / 24 / 60 / 60)
-- Get stats
for _, thread in ipairs(self.threads) do
thread.op = Posts:get_thread_op(thread.id)
thread.replies = Posts:count_posts(thread.id) - 1
thread.url = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=thread.op.post_id })
-- Process name
thread.op.name = thread.op.name or self.board.anon_name
-- Process tripcode
thread.op.trip = thread.op.trip or ""
-- Process comment
if thread.op.comment then
local comment = thread.op.comment
comment = format.sanitize(comment)
comment = format.spoiler(comment)
if #comment > 110 then
comment = comment:sub(1,100)
comment = comment .. "..."
end
thread.op.comment = comment
else
thread.op.comment = ""
end
end
return { render = "archive" }
end

@ -1,242 +0,0 @@
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local csrf = require "lapis.csrf"
local capture = require "utils.capture"
local format = require "utils.text_formatter"
local generate = require "utils.generate"
local process = require "utils.request_processor"
local Posts = require "models.posts"
return {
before = function(self)
-- Get board
for _, board in ipairs(self.boards) do
if board.name == self.params.uri_name then
self.board = board
break
end
end
-- Board not found
if not self.board or self.params.page and not tonumber(self.params.page) then
return self:write({ redirect_to = self:url_for("web.pages.index") })
end
-- Get announcements
-- TODO: Consolidate these into a single call
self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id="global" })))
local board_announcements = assert_error(capture.get(self:url_for("api.boards.announcements", { uri_name=self.params.uri_name })))
for _, announcement in ipairs(board_announcements) do
table.insert(self.announcements, announcement)
end
-- Page title
self.page_title = string.format(
"/%s/ - %s",
self.board.name,
self.board.title
)
-- Flag comments as required or not
self.comment_flag = self.board.thread_comment
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Current page
self.params.page = self.params.page or 1
-- Get threads
local response = assert_error(capture.get(self:url_for("api.boards.threads", { uri_name=self.params.uri_name, uri_page=self.params.page })))
self.threads = response.threads
self.pages = response.pages
-- Get posts
for _, thread in ipairs(self.threads) do
-- Get posts visible on the board index
thread.posts = Posts:get_index_posts(thread.id)
-- Get hidden posts
thread.hidden = Posts:count_hidden_posts(thread.id)
-- Get op
local op = thread.posts[#thread.posts]
if not op then
assert_error(false, { "err_orphaned", { thread.id } })
end
thread.url = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id })
-- Format comments
for _, post in ipairs(thread.posts) do
-- OP gets a thread tag
if post.post_id == op.post_id then
post.thread = post.post_id
end
post.name = post.name or self.board.anon_name
post.reply = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id, anchor="q", id=post.post_id })
post.link = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id, anchor="p", id=post.post_id })
post.remix = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id, anchor="r", id=post.post_id })
post.timestamp = os.date("%Y-%m-%d (%a) %H:%M:%S", post.timestamp)
post.file_size = math.floor(post.file_size / 1024)
post.file_dimensions = ""
if post.file_width > 0 and post.file_height > 0 then
post.file_dimensions = string.format(", %dx%d", post.file_width, post.file_height)
end
if not post.file_duration or post.file_duration == "0" then
post.file_duration = ""
else
post.file_duration = string.format(", %s", post.file_duration)
end
if post.file_path then
local name, ext = post.file_path:match("^(.+)(%..+)$")
ext = string.lower(ext)
-- Get thumbnail URL
if post.file_type == "audio" then
if post == thread.posts[#thread.posts] then
post.thumb = self:format_url(self.static_url, "op_audio.png")
else
post.thumb = self:format_url(self.static_url, "post_audio.png")
end
elseif post.file_type == "image" then
if post.file_spoiler then
if post == thread.posts[#thread.posts] then
post.thumb = self:format_url(self.static_url, "op_spoiler.png")
else
post.thumb = self:format_url(self.static_url, "post_spoiler.png")
end
else
if ext == ".webm" or ext == ".svg" then
post.thumb = self:format_url(self.files_url, self.board.name, 's' .. name .. '.png')
else
post.thumb = self:format_url(self.files_url, self.board.name, 's' .. post.file_path)
end
end
end
post.file_path = self:format_url(self.files_url, self.board.name, post.file_path)
end
-- Process comment
if post.comment then
local comment = post.comment
comment = format.sanitize(comment)
comment = format.quote(comment, self, self.board, post)
comment = format.green_text(comment)
comment = format.blue_text(comment)
comment = format.spoiler(comment)
comment = format.new_lines(comment)
post.comment = comment
else
post.comment = ""
end
end
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
return { render = "board"}
end,
GET = function()
return { render = "board" }
end,
POST = function(self)
-- Validate CSRF token
csrf.assert_token(self)
local board_url = self:url_for("web.boards.board", { uri_name=self.board.name })
-- Submit new thread
if self.params.submit then
-- Validate user input
assert_valid(self.params, {
{ "name", max_length=255 },
{ "subject", max_length=255 },
{ "options", max_length=255 },
{ "comment", max_length=self.text_size }
})
-- Validate post
local post = assert_error(process.create_thread(self.params, self.session, self.board))
return { redirect_to = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=post.post_id, anchor="p", id=post.post_id }) }
end
-- Delete thread
if self.params.delete and self.params.thread_id then
-- Validate user input
assert_valid(self.params, {
{ "post_id", exists=true }
})
-- Validate deletion
assert_error(process.delete_thread(self.params, self.session, self.board))
return { redirect_to = board_url }
end
-- Delete post
if self.params.delete and not self.params.thread_id then
-- Validate user input
assert_valid(self.params, {
{ "post_id", exists=true }
})
-- Validate deletion
assert_error(process.delete_post(self.params, self.session, self.board))
return { redirect_to = board_url }
end
-- Report post
if self.params.report then
-- Validate user input
assert_valid(self.params, {
{ "board", exists=true },
{ "post_id", exists=true }
})
-- Validate report
assert_error(process.report_post(self.params, self.board))
return { redirect_to = board_url }
end
-- Admin commands
if self.session.admin or self.session.mod then
-- Sticky thread
if self.params.sticky then
assert_error(process.sticky_thread(self.params, self.board))
return { redirect_to = board_url }
end
-- Lock thread
if self.params.lock then
assert_error(process.lock_thread(self.params, self.board))
return { redirect_to = board_url }
end
-- Save thread
if self.params.save then
assert_error(process.save_thread(self.params, self.board))
return { redirect_to = board_url }
end
-- Override thread
if self.params.override then
assert_error(process.override_thread(self.params, self.board))
return { redirect_to = board_url }
end
-- Ban user
if self.params.ban then
assert_error(process.ban_user(self.params, self.board))
return { redirect_to = board_url }
end
end
return { redirect_to = board_url }
end
}

@ -1,128 +0,0 @@
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local process = require "utils.request_processor"
local capture = require "utils.capture"
local csrf = require "lapis.csrf"
local format = require "utils.text_formatter"
local generate = require "utils.generate"
local Posts = require "models.posts"
return {
before = function(self)
-- Get board
for _, board in ipairs(self.boards) do
if board.name == self.params.uri_name then
self.board = board
break
end
end
-- Board not found
if not self.board then
return self:write({ redirect_to = self:url_for("web.pages.index") })
end
-- Get announcements
-- TODO: Consolidate these into a single call
self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id="global" })))
local board_announcements = assert_error(capture.get(self:url_for("api.boards.announcements", { uri_name=self.params.uri_name })))
for _, announcement in ipairs(board_announcements) do
table.insert(self.announcements, announcement)
end
-- Page title
self.page_title = string.format(
"/%s/ - %s",
self.board.name,
self.board.title
)
-- Nav links link to sub page if available
self.sub_page = "catalog"
-- Flag comments as required or not
self.comment_flag = self.board.thread_comment
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Get threads
local response = assert_error(capture.get(self:url_for("api.boards.threads", { uri_name=self.params.uri_name })))
self.threads = response.threads
-- Get stats
for _, thread in ipairs(self.threads) do
thread.op = Posts:get_thread_op(thread.id)
thread.replies = Posts:count_posts(thread.id) - 1
thread.files = Posts:count_files(thread.id)
thread.url = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=thread.op.post_id })
if thread.op.file_path then
local name, ext = thread.op.file_path:match("^(.+)(%..+)$")
ext = string.lower(ext)
-- Get thumbnail URL
if thread.op.file_type == "audio" then
thread.op.thumb = self:format_url(self.static_url, "post_audio.png")
elseif thread.op.file_type == "image" then
if thread.op.file_spoiler then
thread.op.thumb = self:format_url(self.static_url, "post_spoiler.png")
else
if ext == ".webm" or ext == ".svg" then
thread.op.thumb = self:format_url(self.files_url, self.board.name, 's' .. name .. '.png')
else
thread.op.thumb = self:format_url(self.files_url, self.board.name, 's' .. thread.op.file_path)
end
end
end
thread.op.file_path = self:format_url(self.files_url, self.board.name, thread.op.file_path)
end
-- Process comment
if thread.op.comment then
local comment = thread.op.comment
comment = format.sanitize(comment)
comment = format.spoiler(comment)
comment = format.new_lines(comment)
if #comment > 260 then
comment = comment:sub(1, 250) .. "..."
end
thread.op.comment = comment
else
thread.op.comment = ""
end
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
return { render = "catalog"}
end,
GET = function()
return { render = "catalog" }
end,
POST = function(self)
-- Validate CSRF token
csrf.assert_token(self)
-- Submit new thread
if self.params.submit and not self.thread then
-- Validate user input
assert_valid(self.params, {
{ "name", max_length=255 },
{ "subject", max_length=255 },
{ "options", max_length=255 },
{ "comment", max_length=self.text_size }
})
-- Validate post
local post = assert_error(process.create_thread(self.params, self.session, self.board))
return { redirect_to = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=post.post_id, anchor="p", id=post.post_id }) }
end
return { redirect_to = self:url_for("web.boards.catalog", { uri_name=self.board.name }) }
end
}

@ -1,242 +0,0 @@
local assert_error = require("lapis.application").assert_error
local assert_valid = require("lapis.validate").assert_valid
local csrf = require "lapis.csrf"
local capture = require "utils.capture"
local format = require "utils.text_formatter"
local generate = require "utils.generate"
local process = require "utils.request_processor"
local Posts = require "models.posts"
return {
before = function(self)
-- Get board
for _, board in ipairs(self.boards) do
if board.name == self.params.uri_name then
self.board = board
break
end
end
-- Board not found
if not self.board then
return self:write({ redirect_to = self:url_for("web.pages.index") })
end
-- Get current thread data
local post = Posts:get(self.board.id, self.params.thread)
if not post then
return self:write({ redirect_to = self:url_for("web.boards.board", { uri_name=self.board.name }) })
end
self.thread = post:get_thread()
if not self.thread then
return self:write({ redirect_to = self:url_for("web.boards.board", { uri_name=self.board.name }) })
end
local op = self.thread:get_op()
if post.post_id ~= op.post_id then
return self:write({ redirect_to = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=op.post_id, anchor="p", id=post.post_id }) })
end
-- Get announcements
-- TODO: Consolidate these into a single call
self.announcements = assert_error(capture.get(self:url_for("api.announcements.announcement", { uri_id="global" })))
local board_announcements = assert_error(capture.get(self:url_for("api.boards.announcements", { uri_name=self.params.uri_name })))
for _, announcement in ipairs(board_announcements) do
table.insert(self.announcements, announcement)
end
-- Page title
self.page_title = string.format(
"/%s/ - %s",
self.board.name,
self.board.title
)
-- Flag comments as required or not
self.comment_flag = self.board.thread_comment
-- Generate CSRF token
self.csrf_token = csrf.generate_token(self)
-- Determine if we allow a user to upload a file
self.num_files = Posts:count_files(self.thread.id)
-- Get posts
self.posts = self.thread:get_posts()
-- Format comments
for i, post in ipairs(self.posts) do
-- OP gets a thread tag
if i == 1 then
post.thread = post.post_id
end
post.name = post.name or self.board.anon_name
post.reply = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=self.posts[1].post_id, anchor="q", id=post.post_id })
post.link = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=self.posts[1].post_id, anchor="p", id=post.post_id })
post.timestamp = os.date("%Y-%m-%d (%a) %H:%M:%S", post.timestamp)
post.file_size = math.floor(post.file_size / 1024)
post.file_dimensions = ""
if post.file_width > 0 and post.file_height > 0 then
post.file_dimensions = string.format(", %dx%d", post.file_width, post.file_height)
end
if not post.file_duration or post.file_duration == "0" then
post.file_duration = ""
else
post.file_duration = string.format(", %s", post.file_duration)
end
if post.file_path then
local name, ext = post.file_path:match("^(.+)(%..+)$")
ext = string.lower(ext)
-- Get thumbnail URL
if post.file_type == "audio" then
if post == self.posts[1] then
post.thumb = self:format_url(self.static_url, "op_audio.png")
else
post.thumb = self:format_url(self.static_url, "post_audio.png")
end
elseif post.file_type == "image" then
if post.file_spoiler then
if post == self.posts[1] then
post.thumb = self:format_url(self.static_url, "op_spoiler.png")
else
post.thumb = self:format_url(self.static_url, "post_spoiler.png")
end
else
if ext == ".webm" or ext == ".svg" then
post.thumb = self:format_url(self.files_url, self.board.name, 's' .. name .. '.png')
else
post.thumb = self:format_url(self.files_url, self.board.name, 's' .. post.file_path)
end
end
end
post.file_path = self:format_url(self.files_url, self.board.name, post.file_path)
end
-- Process comment
if post.comment then
local comment = post.comment
comment = format.sanitize(comment)
comment = format.quote(comment, self, self.board, post)
comment = format.green_text(comment)
comment = format.blue_text(comment)
comment = format.spoiler(comment)
comment = format.new_lines(comment)
post.comment = comment
else
post.comment = ""
end
end
end,
on_error = function(self)
self.errors = generate.errors(self.i18n, self.errors)
return { render = "thread"}
end,
GET = function()
return { render = "thread" }
end,
POST = function(self)
-- Validate CSRF token
csrf.assert_token(self)
local board_url = self:url_for("web.boards.board", { uri_name=self.board.name })
local thread_url = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=self.posts[1].post_id })
-- Submit new post
if self.params.submit and self.thread then
-- Validate user input
assert_valid(self.params, {
{ "thread", exists=true },
{ "name", max_length=255 },
{ "subject", max_length=255 },
{ "options", max_length=255 },
{ "comment", max_length=self.text_size }
})
-- Validate post
local post = assert_error(process.create_post(self.params, self.session, self.board, self.thread))
return { redirect_to = self:url_for("web.boards.thread", { uri_name=self.board.name, thread=self.posts[1].post_id, anchor="p", id=post.post_id }) }
end
-- Delete thread
if self.params.delete and self.params.thread_id then
-- Validate user input
assert_valid(self.params, {
{ "post_id", exists=true }
})
-- Validate deletion
assert_error(process.delete_thread(self.params, self.session, self.board))
return { redirect_to = board_url }
end
-- Delete post
if self.params.delete and not self.params.thread_id then
-- Validate user input
assert_valid(self.params, {
{ "post_id", exists=true }
})
-- Validate deletion
assert_error(process.delete_post(self.params, self.session, self.board))
return { redirect_to = thread_url }
end
-- Report post
if self.params.report then
-- Validate user input
assert_valid(self.params, {
{ "board", exists=true },
{ "post_id", exists=true }
})
-- Validate report
assert_error(process.report_post(self.params, self.board))
return { redirect_to = thread_url }
end
-- Admin commands
if self.session.admin or self.session.mod then
-- Sticky thread
if self.params.sticky then
assert_error(process.sticky_thread(self.params, self.board))
return { redirect_to = thread_url }
end
-- Lock thread
if self.params.lock then
assert_error(process.lock_thread(self.params, self.board))
return { redirect_to = thread_url }
end
-- Save thread
if self.params.save then
assert_error(process.save_thread(self.params, self.board))
return { redirect_to = thread_url }
end
-- Override thread
if self.params.override then
assert_error(process.override_thread(self.params, self.board))
return { redirect_to = thread_url }
end
-- Ban user
if self.params.ban then
assert_error(process.ban_user(self.params, self.board))
return { redirect_to = thread_url }
end
return { redirect_to = thread_url }
end
return { redirect_to = thread_url }
end
}

@ -1,29 +0,0 @@
local Users = require "models.users"
return function(self)
-- Prepare session names
self.session.names = self.session.names or {}
-- Verify Authorization
if self.session.name then
local user = Users:get_user(self.session.name)
if user then
user.password = nil
self.session.admin = user.admin
self.session.mod = user.mod
self.session.janitor = user.janitor
else
self.session.admin = nil
self.session.mod = nil
self.session.janitor = nil
end
else
self.session.admin = nil
self.session.mod = nil
self.session.janitor = nil
end
-- Get IP from ngx
self.params.ip = self.req.headers["X-Real-IP"] or self.req.remote_addr
end

@ -1,41 +0,0 @@
local assert_error = require("lapis.application").assert_error
local capture = require "utils.capture"
return function(self)
-- MODS = FAGS
if self.session.admin or
self.session.mod or
self.session.janitor or
self.route_name == "admin" then
return
end
-- Get list of bans by ip
local bans = assert_error(capture.get(self:url_for("api.bans.bans_ip", { uri_ip=self.params.ip })))
-- Get current board
local board = {}
if self.params.uri_name then
board = assert_error(capture.get(self:url_for("api.boards.board", { uri_name=self.params.uri_name })))
end
-- If you are banned, gtfo
for _, ban in ipairs(bans) do
if ban.board_id == 0 or
ban.board_id == board.id then
-- Ban data
self.ip = ban.ip
self.reason = ban.reason or self.i18n("err_ban_reason")
self.expire = os.date("%Y-%m-%d (%a) %H:%M:%S", ban.time + ban.duration)
-- Page title
self.page_title = self.i18n("ban_title")
-- Display a theme
self.board = { theme = "yotsuba_b" }
return self:write({ render = "banned" })
end
end
end

@ -1,14 +0,0 @@
local Boards = require "models.boards"
return function(self)
-- Get all board data
self.boards = Boards:get_boards()
-- Page title
self.page_title = self.i18n("404")
-- Display a theme
self.board = { theme = "yotsuba_b" }
return { render = "code_404", status = "404" }
end

@ -1,45 +0,0 @@
local config = require("lapis.config").get()
local assert_error = require("lapis.application").assert_error
local i18n = require "i18n"
local lfs = require "lfs"
local capture = require "utils.capture"
return function(self)
-- Set basic information
self.software = "Lapis-chan"
self.version = "1.2.5"
self.site_name = config.site_name
self.text_size = _G.text_size
-- Get localization files
self.locales = {}
for file in lfs.dir("src/locale") do
local name, ext = string.match(file, "^(.+)%.(.+)$")
if ext == "lua" then
table.insert(self.locales, name)
end
end
-- Set localization
if self.params.locale then
self.session.locale = self.params.locale
end
i18n.setLocale(self.session.locale or "en")
i18n.loadFile("src/locale/en.lua")
if i18n.getLocale() ~= "en" then
i18n.loadFile("locale/" .. i18n.getLocale() .. ".lua")
end
self.i18n = i18n
-- Get all boards
self.boards = assert_error(capture.get(self:url_for("api.boards.boards")))
-- Static
self.static_url = "/static/%s"
self.files_url = "/files/%s/%s"
function self.format_url(_, pattern, ...)
return self:build_url(string.format(pattern, ...))
end
end

@ -1,333 +0,0 @@
local Users = require "models.users"
local Boards = require "models.boards"
local Pages = require "models.pages"
local lfs = require "lfs"
local validate = require("lapis.validate").validate
local faq = [[
<div class="table_of_contents">
<ol>
<li><a href="#what-is-lapis-chan">What is Lapis-chan?</a></li>
<li><a href="#what-is-lapchan">What is Lapchan?</a></li>
<li><a href="#what-should-i-know">What should I know before posting?</a></li>
<li><a href="#chan-101">What are the basics?</a></li>
<li><a href="#anonymous">How do I post anonymously?</a></li>
<li><a href="#image">Do I have to post an image?</a></li>
<li><a href="#quote">How do I quote a post?</a></li>
<li><a href="#tripcode">What is a tripcode?</a></li>
<li><a href="#spoiler">Can I mark an image as a spoiler?</a></li>
<li><a href="#post-options">What are post options?</a></li>
<li><a href="#post-menu">How can I interact with posts?</a></li>
<li><a href="#board-types">What types of boards are supported?</a></li>
</ol>
</div>
<div class="answers">
<div id="what-is-lapis-chan">
<h2>What is Lapis-chan?</h2>
<p>
Lapis-chan is an open source imageboard web application written in Lua
using the Lapis web framework.
</p>
</div>
<div id="what-is-lapchan">
<h2>What is Lapchan?</h2>
<p>
Lapchan is a website that runs the latest version of Lapis-chan. It is
both used as a small community and a testing platform for new and
experimental features.
</p>
</div>
<div id="what-should-i-know">
<h2>What Should I Know Before Posting?</h2>
<p>
Before posting, you should read the rules for whichever board you want to
post in. If you break the rules, your post may be deleted and you may
also earn yourself a temporary or permanent ban from the board or entire
website. Please read the rules!
</p>
</div>
<div id="chan-101">
<h2>What are the Basics?</h2>
<p>
In general, "blue" boards are considered safe for work (SFW) and "red"
boards are considered not safe for work (NSFW). The definition of
work-safety is often loose, but in general it means there shouldn't be
any direct pornographic material on a blue board. This may differ from
site to site, but Lapis-chan by default offers blue and red themes that
are nearly identical to 4chan's themes. New themes are expected in later
releases.
</p>
<p>
It is also worth noting that chan culture can be both friendly and
abrasive. More often than not, users will be posting anonymously and are
able to speak freely because if this. Be prepared for the best and worst
of society when interacting with others in an anonymous forum.
</p>
</div>
<div id="anonymous">
<h2>How Do I Post Anonymously?</h2>
<p>
By default, all users are anonymous to each other. By leaving the "Name"
field empty in a post, your name will simply be fille din with the
board's default name. Identifiable information such as your IP address is
recorded to the Lapis-chan database when you post for legal reasons, but
all posts are permanently purged after some time unless otherwise noted,
including your IP address and any other information attached to your
post.
</p>
</div>
<div id="image">
<h2>Do I Have to Post an Image?</h2>
<p>
Some boards require you to post an image or a comment, others do not. By
default, Lapis-chan will place "(Required)" in or near a field that
requires data. Currently, Lapis-chan's only optionally required data
include a comment and an image.
</p>
</div>
<div id="quote">
<h2>How Do I Quote a Post?</h2>
<p>
To quote (and link to) another post, simply type "&gt;&gt;" followed by
the post number (e.g. &gt;&gt;2808). To quote a post that is on a
different board, You must type "&gt;&gt;&gt;" follow by a slash, the
name of the board, another slash, and then the post number
(e.g. &gt;&gt;&gt;/a/2808).
</p>
</div>
<div id="tripcode">
<h2>What is a Tripcode?</h2>
<p>
A tripcode is a uniquely identifiable hash attached to the end of your
name, or in lieu of a name. It is a completely optional feature that
allows users to de-anonymize if they so choose. Some boards benefit from
de-anonymization such as content-creation boards where being named can
help get your content seen and recognized.
</p>
<p>
Tripcodes come in two sizes: insecure and secure. Insecure tripcodes use
a weak hashing method that allows users to game the algorithm to generate
a hash that reads out something similar to what they want. A secure
tripcode uses a very secure hashing algorithm and a server-specific
secret token that is not gameable, but also significantly more difficult
to impersonate.
</p>
<p>
To use an insecure tripcode, place a hash ("#") sign at the end of your
name (or leave a name out entirely) and type your password after the
hash. To use a secure tripcode, simply use two hashes instead of one
(e.g. lapchan#insecure, lapchan##secure, #nameless, ##nameless).
</p>
</div>
<div id="spoiler">
<h2>Can I Mark an Image as a Spoiler?</h2>
<p>
Yes. When you upload an image, there should be a check box beside the
file input field. Checking that box will replace the thumbnail of your
image with a spoiler image.
</p>
<p>
You can also tag text within your post as a spoiler by writing your text
with [spoiler]a spoiler tag[/spoiler].
</p>
</div>
<div id="post-options">
<h2>What Are Post Options?</h2>
<p>
Post options are optional features you can use to modify how your post
affects the thread or board you are posting in. To apply an options,
simply type the option code into the options field in your post.
Currently, the options available include:
</p>
<ul>
<li>sage - Do not bump the thread with your post.</li>
</ul>
</div>
<div id="post-menu">
<h2>How Can I Interact With Posts?</h2>
<p>
To interact with a post, click on the menu icon ("") on the left of the
post. The menu currently has the following interactions:
</p>
<ul>
<li>
Report Post - Report a post to moderators that you believe is breaking
the rules of the board.
</li>
<li>
Delete Post - Delete your own post. Lapis-chan saves a unique password
to your user session when you make your first post and will allow you
to delete any post you make as long as you are using the same session.
</li>
<li>
Remix Image - Draw boards have a remix feature that allows you to copy
the image from a post into the drawing canvas and draw on top of it.
You can then post your new image in the thread to show off your
updated image.
</li>
</ul>
</div>
<div id="board-types">
<h2>What Types of Boards are Supported?</h2>
<p>
Lapis-chan has several different types of boards with more planned in the
future. Currently, Lapis-chan supports the following boards:
</p>
<ul>
<li>
Image boards - Upload images and chat with other people about various
topics or sub-topics. Common image boards include discussing your
favourite TV shows, video games, or characters within.
</li>
<li>
Text boards - Strictly text. Common text boards include discussing
latest events, breaking news, politics, or writing stories.
</li>
<li>
Draw boards - Upload, draw, and remix images. Common draw boards
include art critiquing and art remixing.
</li>
</ul>
</div>
</div>
]]
local success = [[
<h2>
Congratulations! Lapis-chan is now installed! Please rename or delete the
`install.lua` file to see your new board. Visit "/admin" to get started!
<h2>
<h2>Thank you for installing Lapis-chan! &lt;3</h2>
]]
return {
GET = function(self)
self.page_title = "Install Lapis-chan"
self.board = { theme = "yotsuba_b" }
-- Do we already have data?
local users = Users:get_users()
local boards = Boards:get_boards()
local pages = Pages:get_pages()
-- We did it!
if #users > 0 and #boards > 0 and #pages > 0 then
return success
end
-- Get list of themes
self.themes = {}
for file in lfs.dir("."..self.styles_url) do
local name, ext = string.match(file, "^(.+)(%..+)$")
if name ~= "reset" and
name ~= "posts" and
name ~= "style" and
name ~= "tegaki" and
ext == ".css" then
table.insert(self.themes, name)
end
end
return { render = "install" }
end,
POST = function(self)
self.page_title = "Install Lapis-chan"
self.board = { theme = "yotsuba_b" }
-- Do we already have data?
local users = Users:get_users()
local boards = Boards:get_boards()
local pages = Pages:get_pages()
-- We did it!
if #users > 0 and #boards > 0 and #pages > 0 then
return success
end
local errs = validate(self.params, {
{ "user_username", exists=true, max_length=255 },
{ "user_password", min_length=4, max_length=255 },
{ "name", exists=true, max_length=10 },
{ "title", min_length=2, max_length=255 },
{ "subtext", max_length=255 },
{ "rules" },
{ "ban_message", max_length=255 },
{ "anon_name", max_length=255 },
{ "theme", exists=true },
{ "pages", exists=true },
{ "threads_per_page", exists=true },
{ "thread_file_limit", exists=true },
{ "post_limit", exists=true },
{ "thread_file", exists=true },
{ "thread_comment", exists=true },
{ "post_file", exists=true },
{ "post_comment", exists=true },
{ "text_only", exists=true },
{ "filetype_image", exists=true },
{ "filetype_audio", exists=true },
{ "draw", exists=true },
{ "archive", exists=true },
{ "archive_time", exists=true },
{ "group", exists=true }
})
local out
if errs then
out = "<div class='install'>\n"
for _, err in ipairs(errs) do
out = out .. "<h2>" .. err .. "</h2>\n"
end
out = out .. [[
<form action="" method="get">
<button>Return</button>
</form>
</div>
]]
end
if out then
return out
end
-- Add new user
Users:create_user {
username = self.params.user_username,
password = self.params.user_password,
admin = true,
mod = false,
janitor = false
}
-- Add new board
Boards:create_board {
name = self.params.name,
title = self.params.title,
subtext = self.params.subtext,
rules = self.params.rules,
anon_name = self.params.anon_name,
theme = self.params.theme,
posts = 0,
pages = self.params.pages,
threads_per_page = self.params.threads_per_page,
text_only = self.params.text_only,
filetype_image = self.params.filetype_image,
filetype_audio = self.params.filetype_audio,
draw = self.params.draw,
thread_file = self.params.thread_file,
thread_comment = self.params.thread_comment,
thread_file_limit = self.params.thread_file_limit,
post_file = self.params.post_file,
post_comment = self.params.post_comment,
post_limit = self.params.post_limit,
archive = self.params.archive,
archive_time = self.params.archive_time * 24 * 60 * 60,
group = self.params.group
}
-- Add FAQ page
Pages:create_page {
title = "Frequently Asked Questions",
slug = "faq",
content = faq
}
end
}

@ -1,14 +0,0 @@
local lapis = require "lapis"
local app = lapis.Application()
app.__base = app
app.name = "web.pages."
app.handle_404 = require "apps.web.internal.code_404"
app:match("index", "/", require "apps.web.pages.index")
app:match("c404", "/404", require "apps.web.internal.code_404") -- FIXME: remove this route
app:match("faq", "/faq", require "apps.web.pages.rules") -- FIXME: need a faq page
app:match("rules", "/rules", require "apps.web.pages.rules")
app:match("logout", "/logout", require "apps.web.pages.logout")
app:match("page", "/:page", require "apps.web.pages.page")
return app

@ -1,10 +0,0 @@
return function(self)
-- Page title
self.page_title = self.i18n("index")
-- Display a theme
self.board = { theme = "yotsuba_b" }
return { render = "index" }
end

@ -1,9 +0,0 @@
return function(self)
-- Logout
self.session.name = nil
self.session.admin = nil
self.session.mod = nil
self.session.janitor = nil
return { redirect_to = self:url_for("web.pages.index") }
end

@ -1,28 +0,0 @@
local Boards = require "models.boards"
local Pages = require "models.pages"
local markdown = require "markdown"
return function(self)
-- Get all board data
self.boards = Boards:get_boards()
-- Get page
self.page = Pages:get_page(self.params.page)
if not self.page then
return self:write({ redirect_to = self:url_for("web.pages.c404") })
end
-- Page title
self.page_title = self.page.title
-- Markdown
if self.page.content then
self.page.content = markdown(self.page.content)
end
-- Display a theme
self.board = { theme = "yotsuba_b" }
return { render = "page" }
end

@ -1,19 +0,0 @@
return function(self)
-- Page title
self.page_title = self.i18n("rules")
-- Display a theme
self.board = { theme = "yotsuba_b" }
for _, board in ipairs(self.boards) do
board.url = self:url_for("web.boards.board", { board=board.name })
if board.rules then
board.rules = _G.markdown(board.rules)
else
board.rules = ""
end
end
return { render = "rules" }
end

@ -1,234 +0,0 @@
return { fr = {
--==[[ Navigation ]]==--
archive = "Archive",
bottom = "Bas",
catalog = "Catalogue",
index = "Index",
refresh = "Rafraîchir",
["return"] = "Retourner",
return_board = "Retourner au babillard",
return_index = "Retourner à l'index",
return_thread = "Retourner au fil de discussion",
top = "Haut",
--==[[ Error Messages ]]==--
-- Controller error messages
err_ban_reason = "Aucune Reçue.",
err_board_used = "Le nom du Babillard est déjà utilisé.",
err_not_admin = "Vous n'êtes pas un administrateur.",
err_orphaned = "Le Fil de Discussion nº%s est orphelin.",
err_slug_used = "Le slug de la page est déjà utilisée.",
err_user_used = "Le nom d'utilisateur est dèja utilisé.",
-- Model error messages
err_contribute = "Vous devez poster un commentaire ou un fichier.",
err_locked_thread = "Le fil de discussion No.%s est verrouillé.",
err_no_files = "Les fichiers ne sont pas acceptés sur ce babillard.",
err_comment_post = "Un commentaire est requis pour poster sur ce babillard.",
err_comment_thread = "Un commentaire est requis pour poster un fil de discussion sur ce babillard.",
err_create_ann = "Impossible de créer un annoncement: %s.",
err_create_ban = "Impossible de bannir l'IP: %s.",
err_create_board = "Impossible de créer le babillard: /%s/ - %s.",
err_create_page = "Impossible de créer la page: /%s/ - %s.",
err_create_post = "Impossible de soumettre le poste.",
err_create_report = "Impossible de reporter le post No.%s.",
err_create_thread = "Impossible de créer un nouveau fil de discussion.",
err_delete_board = "Impossible de supprimer le babillard: /%s/ - %s.",
err_delete_post = "Impossible de supprimer le poste No.%s.",
err_create_user = "Impossible de créer l'utilisateur: %s.",
err_file_exists = "Le fichier existe déjà sur ce babillard.",
err_file_limit = "Le fil de discussion No.%s a atteint sa limite de fichier.",
err_file_post = "Un fichier est requis pour poster sur ce babillard.",
err_file_thread = "Un fichier est requis pour poster un fil de discussion sur ce babillard.",
err_invalid_board = "Babillard invalide: /%s/.",
err_invalid_ext = "Type de fichier invalide: %s.",
err_invalid_image = "Données d'image invalides.",
err_invalid_post = "Le poste No.%s n'est pas valide.",
err_invalid_user = "Nom d'utilisateur ou mot de passe invalide.",
--==[[ 404 ]]==--
["404"] = "404 - Page Introuvable",
--==[[ Administration ]]==--
-- General
admin_panel = "Panneau Administratif",
administrator = "Administrateur",
announcement = "Annoncement",
archive_days = "Nombre de jours à garder les Fils de Discussion dans l'Archive",
archive_pruned = "Archiver les Fils de Discussion Réduites",
board = "Babillard",
board_group = "Group de babillard",
board_title = "Nom de Babillard",
bump_limit = "Limite pour remonter un fil de discussion",
content_md = "Contenu (Markdown)",
default_name = "Nom par Défaut",
draw_board = "Babillard à Dessin",
file = "Fichier",
file_limit = "Limite de fichiers dans un Fil de Discussion",
global = "Global",
index_boards = "Babillards Présent",
janitor = "Concierge",
login = "Connexion",
logout = "Déconnexion",
moderator = "Modérateur",
num_pages = "Pages Actives",
num_threads = "Fils de Discussion par Page",
password = "Mot de Passe",
password_old = "Ancient Mot de Passe",
password_retype = "Confirmer le Mot de Passe",
post_comment_required = "Exiger un commentaire pour pouvoir poster",
post_file_required = "Exiger un Fichier pour pouvoir poster",
regen_thumb = "Régénérer les Miniatures",
reply = "Répondre",
rules = "Les Règles",
name = "Nom",
subtext = "Sous-texte",
success = "Succès",
text_board = "Babillard à Texte",
theme = "Thème",
thread_comment_required = "Exiger un Commentaire ",
thread_file_required = "Exiger un Fil de Discussion",
slug = "Slug",
username = "Nom d'utilisateur",
yes = "Oui",
no = "Non",
-- Announcements
create_ann = "Créer un Annoncement",
modify_ann = "Modifier un Annoncement",
delete_ann = "Supprimer un Annoncement",
created_ann = "Création de l'annoncement: %s réussie.",
modified_ann = "Modification de l'annoncement: %s réussie.",
deleted_ann = "Suppression de l'annoncement: %s réussie.",
-- Boards
create_board = "Créer un Babillard",
modify_board = "Modifier un Babillard",
delete_board = "Supprimer un Babillard",
created_board = "Création du Babillard: /%s/ - %s réussie.",
modified_board = "Modification du Babillard : /%s/ - %s réussie.",
deleted_board = "Suppression du Babillard : /%s/ - %s réussie.",
-- Pages
create_page = "Créer une Page",
modify_page = "Modifier une Page",
delete_page = "Supprimer une Page",
created_page = "Création de la page : /%s/ - %s réussie.",
modified_page = "Modification de la page : /%s/ - %s réussie.",
deleted_page = "Suppression de la page : /%s/ - %s réussie.",
-- Reports
view_report = "Afficher un Rapport",
delete_report = "Supprimer un Rapport",
deleted_report = "Suppression du Rapport: %s réussie.",
-- Users
create_user = "Créer un Utilisateur",
modify_user = "Modifier un Utilisateur",
delete_user = "Supprimer un Utilisateur",
created_user = "Création de l'utilisateur: %s réussie.",
modified_user = "Modification de l'utilisateur: %s réussie.",
deleted_user = "Suppression de l'utilisateur: %s réussie.",
--==[[ Archive ]]==--
arc_display = "Affichage de %{n_thread} %{p_thread} expirés depuis %{n_day} %{p_day} ",
arc_number = "",
arc_name = "Nom",
arc_excerpt = "Extrait",
arc_replies = "Réponses",
arc_view = "Afficher",
--==[[ Ban ]]==--
ban_title = "Banni!",
ban_reason = "Vous avez été banni pour la raison suivante:",
ban_expire = "Votre ban expirera le %{expire}.",
ban_ip = "Selon avec notre serveur, votre IP est: %{ip}.",
--==[[ Catalog ]]==--
cat_stats = "R: %{replies} / F: %{files}",
--==[[ Copyright ]]==--
copy_software = "Réalisé avec %{software} %{version}",
copy_download = "Télécharger à partir de %{github}",
--==[[ Forms ]]==--
form_ban = "Bannir l'Utilisateur",
form_ban_display = "Afficher le Ban",
form_ban_board = "Ban Local",
form_ban_reason = "Raison du ban",
form_ban_time = "Durée (en jours) à bannir l'utilisateur",
form_clear = "Effacer",
form_delete = "Supprimer le Poste",
form_draw = "Dessiner",
form_lock = "Verrouiller le Fil de Discussion",
form_override = "Fichiers Illimités",
form_readme = "Veuillez lire [%{rules}] et la [%{faq}] avant de poster.",
form_remix = "Remixer l'Image",
form_report = "Reporter le Poste",
form_required = "Champ Requis",
form_save = "Épargner le Fil de Discussion",
form_sticky = "Épingler le Fil de Discussion",
form_submit = "Soumettre le Poste",
form_submit_name = "Nom",
form_submit_name_help = "Donnez-vous un nom , un tripcode ou les deux (facultatif)",
form_submit_subject = "Sujet",
form_submit_subject_help = "Définir le sujet de la discussion (facultatif)",
form_submit_options = "Options",
form_submit_options_help = "Sage: poster sans faire monter le fil de discussion (À venir) (facultatif)",
form_submit_comment = "Commentaire",
form_submit_comment_help = "Contribuer à la discussion (ou non)",
form_submit_file = "Fichier",
form_submit_file_help = "Télécharger un fichier",
form_submit_draw = "Dessiner",
form_submit_draw_help = "Dessiner ou remixer une image",
form_submit_spoiler = "Spoiler",
form_submit_spoiler_help = "Replacer la miniature avec une image sans spoiler",
form_submit_mod = "Modérateur",
form_submit_mod_help = "Mettre un drapeau sur ce fil de discussion",
form_width = "Largeur",
form_height = "Hauteur",
--==[[ Posts ]]==--
post_link = "Lier à ce poste",
post_lock = "Le fil de discussion est verrouillé",
post_hidden = "%{n_post} %{p_post} et %{n_file} %{p_file} omis. %{click} pour afficher.",
post_override = "Ce fil de discussion accepte un nombre illimité de fichiers",
post_reply = "Répondre à ce poste",
post_sticky = "Le fil de discussion est épinglé",
post_save = "Le fil de discussion ne sera pas enlevé automatiquement",
--==[[ Plurals ]]==--
days = {
one = "jour",
other = "jours"
},
files = {
one = "fichier",
other = "fichiers"
},
posts = {
one = "poste",
other = "postes"
},
threads = {
one = "fil de discussion",
other = "fils de discussion"
},
}}

@ -1,234 +0,0 @@
return { phpceo = {
--==[[ Navigation ]]==--
archive = "Gallery Of Our Greatness",
bottom = "Best For Last",
catalog = "Products And Services",
index = "Home Page",
refresh = "Experience A Rejuvinating Meeting",
["return"] = "Reconsider",
return_board = "Reconsider that product",
return_index = "Reconsider that home page",
return_thread = "Reconsider that email chain",
top = "I'm The Best",
--==[[ Error Messages ]]==--
-- Controller error messages
err_ban_reason = "Hit 'enter' too early.",
err_board_used = "That product name is in use, and we don't want to spend money on lawyers.",
err_not_admin = "Call HR.",
err_orphaned = "I accidentally lost the email chain about %s, do you know where it went?",
err_slug_used = "SEO CONFLICT, GOOGLE GROWTHHACK ERROR.",
err_user_used = "You can't be Brandon, *I'm* Brandon!",
-- Model error messages
err_contribute = "Your argument needs more pictures or more words. I can't decide which. I'm fickle like that.",
err_locked_thread = "Email chain about %s is locked, I blame Outlook.",
err_no_files = "The cloud is busy, it does not want your attachments.",
err_comment_post = "Your pitch is great, except I don't know what you're talking about. Use your words.",
err_comment_thread = "Did you seriously just try to send an email without a body? At least put in your signature. Have some pride, Intern.",
err_create_ann = "I couldn't stop yelling long enough to say: %s.",
err_create_ban = "These lottery numbers didn't work: %s.",
err_create_board = "I don't know how to make the product: /%s/ - %s.",
err_create_page = "Pages? No, we're paperless now, we don't need: /%s/ - %s.",
err_create_post = "I didn't read your email, let's set up a meeting in a few minutes where you read it to me.",
err_create_report = "I don't see anything wrong with an email about %s and I have no inclination to in my life.",
err_create_thread = "The reply button is broke! This is effecting production.",
err_delete_board = "I tried to delete those emails about: /%s/ - but I just couldn't %s.",
err_delete_post = "I clicked the flag, did that delete %s? No? Well what else am I supposed to do?",
err_create_user = "I don't know who %s is and frankly I don't give a big enough damn to learn.",
err_file_exists = "Johnson! I already have that spreadsheet! I think. Probably.",
err_file_limit = "THE CLOUD IS FULL, IF YOU SUBMIT ANYTHING ELSE TO %s THE INTERNET WILL GO DOWN.",
err_file_post = "If you want my attention you better use something other than words because I sure as hell ain't reading anything that comes across this desk today.",
err_file_thread = "This email chain said 'post your favorite confidential documents' but you forgot to attach any. Try that one again.",
err_invalid_board = "CORRUPT EMAIL: /%s/.",
err_invalid_ext = "Do I look like I know what a %s is?",
err_invalid_image = "It looks like you sent a picture but what I got was a digital clown.",
err_invalid_post = "Whatever %s was about, I've decided to politely disregard it as 'wrong opinion'.",
err_invalid_user = "I don't know who you are or what you're doing. I'm calling the police.",
--==[[ 404 ]]==--
["404"] = "404 - That's 3 better than a 401k!",
--==[[ Administration ]]==--
-- General
admin_panel = "CEO DASHBOARD",
administrator = "CEO",
announcement = "Important things that come out of my mouth",
archive_days = "Days to Archive Threads",
archive_pruned = "Gas these.",
board = "Chain",
board_group = "Chain Group",
board_title = "Email Subject",
bump_limit = "Burp Excusal Tolerance",
content_md = "Markers",
default_name = "What is this?",
draw_board = "MSPAINT but for the Internet",
file = "Datum gap",
file_limit = "Maximum Cloud Precipitation Ratio",
global = "Everyone Has To Deal With",
index_boards = "Product Selection",
janitor = "Unpaid Intern",
login = "Clock In",
logout = "Clock Out",
moderator = "Enforcer",
num_pages = "Reasons this company is great",
num_threads = "How much I can stand of this",
password = "Digital Hash Salt",
password_old = "That old thing.",
password_retype = "Do it again, it'll be funny.",
post_comment_required = "Need Context",
post_file_required = "Insert Meme",
regen_thumb = "Rectify Pixels",
reply = "Interject",
rules = "Things That Don't Apply To Me",
name = "Bob",
subtext = "sub-who?",
success = "Me, The Physical Embodiment of Greatness",
text_board = "Stuff I Won't Read",
theme = "Birthday Party",
thread_comment_required = "CONVERSE BEFORE CLOUD",
thread_file_required = "INSERT FILE FOR CLOUD",
slug = "CLOUD RESOURCE IDENTIFIER",
username = "User Identifcation String",
yes = "I Am Glad To Blindly Accept This",
no = "Not Exactly",
-- Announcements
create_ann = "Open Mouth, Insert Foot",
modify_ann = "Damage Control",
delete_ann = "I never said that, you can't prove it.",
created_ann = "I believe %s is the lifeblood of this company.",
modified_ann = "What I meant to say was actually %s.",
deleted_ann = "What announcement? %s you say? Doesn't ring a bell.",
-- Boards
create_board = "More Email!",
modify_board = "Different Email",
delete_board = "Less Email!",
created_board = "How do you feel about /%s/ - %s? How about agilefall?",
modified_board = "I hope we fixed the issue about /%s/ - %s because I was getting tired of covering for him.",
deleted_board = "I forgot I don't even like /%s/ - %s.",
-- Pages
create_page = "Create new effigy to my greatness.",
modify_page = "Make this less bad.",
delete_page = "I ain't readin' this.",
created_page = "I heard about /%s/ - %s online, let me tell you how we can pivot this.",
modified_page = "I saw some stuff about /%s/ - %s so I fixed it.",
deleted_page = "Good news, I got rid of those books about: /%s/ - %s.",
-- Reports
view_report = "I said *what*?",
delete_report = "Forget about that.",
deleted_report = "I changed my mind about %s and you can too.",
-- Users
create_user = "Hire",
modify_user = "Rectify Person",
delete_user = "Get Rid Of",
created_user = "So %s is this bright young talent we've been hearing about.",
modified_user = "Whatever was wrong with %s we squared away.",
deleted_user = "I got rid of that %s character for you.",
--==[[ Archive ]]==--
arc_display = "Displaying %{n_thread} expired %{p_thread} from the past %{n_day} %{p_day}",
arc_number = "In Britan they say 'pound'.",
arc_name = "Who?",
arc_excerpt = "TL;DR",
arc_replies = "Responses",
arc_view = "Investigate",
--==[[ Ban ]]==--
ban_title = "Not allowed back!",
ban_reason = "I decided I don't like you anymore, because: ",
ban_expire = "I might forget about this on %{expire}.",
ban_ip = "And your stupid raffle ticket was: %{ip}.",
--==[[ Catalog ]]==--
cat_stats = "R: %{replies} / F: %{files}",
--==[[ Copyright ]]==--
copy_software = "You paid HOW MUCH for %{software}? And it's only version %{version}?! We should have used phpBB, it's way more mature.",
copy_download = "Negotiate a license from %{github}",
--==[[ Forms ]]==--
form_ban = "Escort Off Property",
form_ban_display = "Not allowed in public",
form_ban_board = "Not allowed in my hosue",
form_ban_reason = "Why don't I like this person",
form_ban_time = "How long (in digital ages) to pretend I don't know this guy.",
form_clear = "Forget About It",
form_delete = "Hide Evidence",
form_draw = "Sketch Out",
form_lock = "END THIS",
form_override = "CLOUD IS LOOSE AND HUNGRY FOR DATA",
form_readme = "Frankly, I don't read the [%{rules}] and [%{faq}] so I won't ask you to, either. Hell, I don't even know what they say. And I wrote 'em!",
form_remix = "CLAIM AS YOUR OWN",
form_report = "I DON'T LIKE IT",
form_required = "MANDATORY IF YOU WANT TO KEEP WORKING HERE",
form_save = "Fwd to Offshore Account",
form_sticky = "IMMORTIALIZE SHAME",
form_submit = "HEMMORAGE BRILLIANCE",
form_submit_name = "EMPLOYEE ID NUMBER (PRE-ACQUISITION)",
form_submit_name_help = "Who are you again? A trip-what?",
form_submit_subject = "What Am I On About? Oh, Right.",
form_submit_subject_help = "What I've brought you all here today to discuss",
form_submit_options = "Stock Options",
form_submit_options_help = "sage: Don't let anyone know you're desperate for attention.",
form_submit_comment = "What you have to say.",
form_submit_comment_help = "Tell us about your great idea.",
form_submit_file = "DATUMS",
form_submit_file_help = "FOREFIT YOUR DATA TO THE CLOUD",
form_submit_draw = "MSPAINT",
form_submit_draw_help = "MSPAINT OR GIMPIFY",
form_submit_spoiler = "Paywalled Content",
form_submit_spoiler_help = "Increase viewer engagement 10%%",
form_submit_mod = "Enforcer of Marketability",
form_submit_mod_help = "Mark as landmine. Minesweeper humor. It means I think there's a bomb here.",
form_width = "Digital Horizon",
form_height = "Digital Embiggenment",
--==[[ Posts ]]==--
post_link = "FORWARD THIS EMAIL",
post_lock = "EMAIL IS IN ARCHIVE MODE, STOP REPLYING TO THINGS I FORGOT ABOUT.",
post_hidden = "%{n_post} %{p_post} and %{n_file} %{p_file} omitted. %{click} to view.",
post_override = "CLOUD STORAGE ENABLED",
post_reply = "Reply to this email",
post_sticky = "reply-all'd, cc'd the company, 10/10",
post_save = "I refuse to cut down on the fat, on account of all the words I said being fluff meant to distract you.",
--==[[ Plurals ]]==--
days = {
one = "digital age",
other = "digital aegis"
},
files = {
one = "datum",
other = "datums"
},
posts = {
one = "email",
other = "emailadoodles"
},
threads = {
one = "email chain",
other = "clusterfuck"
},
}}

@ -1,240 +0,0 @@
return { pl = {
--==[[ Navigation ]]==--
archive = "Archiwum",
bottom = "Przewiń na dół",
catalog = "Katalog",
index = "Spis treści",
refresh = "Odśwież",
["return"] = "Powróć",
return_board = "Powróć do boardu",
return_index = "Powróć do spisu treści",
return_thread = "Powróć do wątku",
top = "Przewiń na górę",
--==[[ Error Messages ]]==--
-- Controller error messages
err_ban_reason = "Brak powodu.",
err_board_used = "Nazwa boardu jest już w użyciu.",
err_not_admin = "Nie jesteś administratorem.",
err_orphaned = "Wątek %s został osierocony.",
err_slug_used = "Adres slug jest już w użyciu.",
err_user_used = "Nazwa użytkownika jest już w użyciu.",
-- Model error messages
err_contribute = "Musisz załączyć treść posta lub załącznik.",
err_locked_thread = "Wątek %s jest zablokowany.",
err_no_files = "Dodawanie plików jest wyłączone na tym boardzie.",
err_comment_post = "Na tym boardzie, by odpowiedzieć na wątek, musisz zamieścić komentarz.",
err_comment_thread = "Na tym boardzie, by stworzyć wątek, musisz zamieścić komentarz.",
err_create_ann = "Nie można utworzyć ogłoszenia: %s.",
err_create_ban = "Nie można zbananować IP: %s.",
err_create_board = "Nie można utworzyć boarda: /%s/ - %s.",
err_create_page = "Nie można utworzyć strony: /%s/ - %s.",
err_create_post = "Nie można wysłać posta.",
err_create_report = "Nie można zgłosić posta %s.",
err_create_thread = "Nie można utworzyć wątku.",
err_delete_board = "Nie można usunąć boarda: /%s/ - %s.",
err_delete_post = "Nie można usunąć posta %s.",
err_create_user = "Nie można utworzyć użytkownika: %s.",
err_file_exists = "Ten plik już istnieje na tym boardzie.",
err_file_limit = "Wątek %s osiągnął już swój limit plików.",
err_file_post = "Na tym boardzie, by odpowiedzieć na wątek, musisz załączyć plik.",
err_file_thread = "Na tym boardzie, by stworzyć wątek, musisz załączyć plik.",
err_invalid_board = "Nieznany board: /%s/.",
err_invalid_ext = "Nieznany typ pliku: %s.",
err_invalid_image = "Załączony plik nie jest prawidłowym obrazkiem.",
err_invalid_post = "Post %s nie jest poprawnym postem.",
err_invalid_user = "Niepoprawna nazwa użytkownika lub hasło.",
--==[[ 404 ]]==--
["404"] = "404 - Strona nie znaleziona",
--==[[ Administration ]]==--
-- General
admin_panel = "Panel cwela",
administrator = "Cwel",
announcement = "Ogłoszenie",
archive_days = "Dni do archiwizowania wątków",
archive_pruned = "Archiwizuj wątki",
board = "Board",
board_group = "Grupa boarda",
board_title = "Nazwa boarda",
bump_limit = "Limit przyjebek",
content_md = "Opis (Markdown)",
default_name = "Domyślna nazwa",
draw_board = "Board do rysowania",
file = "Plik",
file_limit = "Limit plików w wątku",
global = "Globalnie",
index_boards = "Aktualne boardy",
janitor = "Woźny",
login = "Zaloguj",
logout = "Wyloguj",
moderator = "Moderator",
num_pages = "Aktywne strony",
num_threads = "Fredy na stronę",
password = "Hasło",
password_old = "Powtórz hasło",
password_retype = "Stare hasło",
post_comment_required = "Wymagany komentarz do posta",
post_file_required = "Wymagany plik do posta",
regen_thumb = "Odśwież miniaturki",
reply = "Odpowiedz",
rules = "Zasady",
name = "Nazwa",
subtext = "Podtekst",
success = "Sukces",
text_board = "Board tekstowy",
theme = "Motyw",
thread_comment_required = "Wymagany komentarz do freda",
thread_file_required = "Wymagany plik do freda",
slug = "Slug",
username = "Nazwa użytkownika",
yes = "Tak",
no = "Nie",
-- Announcements
create_ann = "Utwórz ogłoszenie",
modify_ann = "Zmień ogłoszenie",
delete_ann = "Wyjeb ogłoszenie",
created_ann = "Utworzyłeś ogłoszenie %s.",
modified_ann = "Zmieniłeś ogłoszenie %s.",
deleted_ann = "Wyjebałeś ogłoszenie %s.",
-- Boards
create_board = "Utwórz boarda",
modify_board = "Zmień boarda",
delete_board = "Wyjeb boarda",
created_board = "Utworzyłeś boarda /%s/ - %s.",
modified_board = "Zmieniłeś boarda /%s/ - %s.",
deleted_board = "Wyjebałeś boarda /%s/ - %s.",
-- Pages
create_page = "Utwórz stronę",
modify_page = "Zmień stronę",
delete_page = "Wyjeb stronę",
created_page = "Utworzyłeś stronę /%s/ - %s.",
modified_page = "Zmieniłeś stronę /%s/ - %s.",
deleted_page = "Wyjebałeś stronę /%s/ - %s.",
-- Reports
view_report = "Przejrzyj zgłoszenie",
delete_report = "Usuń zgłoszenie",
deleted_report = "Wyjebałeś zgłoszenie %s.",
-- Users
create_user = "Utwórz użytkownika",
modify_user = "Zmień użytkownika",
delete_user = "Wyjeb użytkownika",
created_user = "Utworzyłeś użytkownika %s.",
modified_user = "Zmieniłeś użytkownika %s.",
deleted_user = "Wyjebałeś użytkownika %s.",
--==[[ Archive ]]==--
arc_display = "Wyświetlanie %{n_thread} %{p_thread} (zarchiwizowane) z %{n_day} %{p_day}",
arc_number = "nr ",
arc_name = "Nazwa",
arc_excerpt = "Kawał freda",
arc_replies = "Odpowiedzi",
arc_view = "Wyślij",
--==[[ Ban ]]==--
ban_title = "Zbanowany!",
ban_reason = "Zostałeś zbananowany z powodu o takiego:",
ban_expire = "Twój banan usunie się %{expire}.",
ban_ip = "Według naszych serwerów NASA, twoje IP to %{ip}.",
--==[[ Catalog ]]==--
cat_stats = "Odp.: %{replies} / plików: %{files}",
--==[[ Copyright ]]==--
copy_software = "Fredy napędzane przez %{software} %{version}",
copy_download = "Pobierz z %{github}",
--==[[ Forms ]]==--
form_ban = "Zbanuj użytkownika",
form_ban_display = "Wyświetl bana",
form_ban_board = "Ban lokalny",
form_ban_reason = "Powód bana",
form_ban_time = "Długość bana (w minutach)",
form_clear = "Wyczyść",
form_delete = "Usuń posta",
form_draw = "Rysuj",
form_lock = "Zablokuj wątek",
form_override = "Nielimitowane pliki",
form_readme = "Przeczytaj [%{rules}] oraz [%{faq}] zanim zapostujesz.",
form_remix = "Przerób obrazek",
form_report = "Zgłoś posta",
form_required = "pole wymagane",
form_save = "Zapisz wątek",
form_sticky = "Przyklej freda",
form_submit = "Wyślij posta",
form_submit_name = "Pseudonim",
form_submit_name_help = "Nadaj sobie nazwę lub tripkod (opcjonalne)",
form_submit_subject = "Temat",
form_submit_subject_help = "Temat dyskusji (opcjonalne)",
form_submit_options = "Opcje",
form_submit_options_help = "sage: zapostuj bez podbijania freda (opcjonalne)",
form_submit_comment = "Komentarz",
form_submit_comment_help = "Dodaj coś do dyskusji",
form_submit_file = "Plik",
form_submit_file_help = "Załącz plik",
form_submit_draw = "Rysuj",
form_submit_draw_help = "Narysuj lub przerób obrazek",
form_submit_spoiler = "Spoiler",
form_submit_spoiler_help = "Podmień miniaturkę na spoiler",
form_submit_mod = "Moderator",
form_submit_mod_help = "Oznacz tego freda",
form_width = "Szerokość",
form_height = "Wysokość",
--==[[ Posts ]]==--
post_link = "Link do tego posta",
post_lock = "Wątek jest zablokowany",
post_hidden = "%{n_post} %{p_post} i %{n_file} %{p_file} zostały pominięte. %{click} aby wyświetlić.",
post_override = "Wątek akceptuje nielimitowaną ilość plików",
post_reply = "Odpowiedz na ten post",
post_sticky = "Fred jest przyklejony",
post_save = "Wątek nie będzie archiwizowany",
--==[[ Plurals ]]==--
days = {
one = "dzień",
other = "dni"
},
files = {
one = "plik",
few = "pliki",
many = "plików",
other = "pliki"
},
posts = {
one = "post",
few = "posty",
many = "postów",
other = "posty"
},
threads = {
one = "wątek",
few = "wątki",
many = "wątków",
other = "wątki"
},
}}

@ -1,96 +0,0 @@
local Model = require("lapis.db.model").Model
local Announcements = Model:extend("announcements", {
relations = {
{ "board", belongs_to="Boards" }
}
})
Announcements.valid_record = {
{ "board_id", is_integer=true },
{ "text", exists=true }
}
--- Create an announcement
-- @tparam table params Announcement parameters
-- @treturn boolean success
-- @treturn string error
function Announcements:new(params)
local announcement = self:create(params)
return announcement and announcement or nil, { "err_create_ann", { params.text } }
end
--- Modify an announcement
-- @tparam table params Announcement parameters
-- @treturn boolean success
-- @treturn string error
function Announcements:modify(params)
local announcement = self:get(params.id)
if not announcement then
return nil, { "err_create_ann", { params.text } } -- FIXME: wrong error
end
local success, err = announcement:update(params)
return success and announcement or nil, "FIXME: " .. tostring(err)
end
--- Delete an announcement
-- @tparam number id Announcement ID
-- @treturn boolean success
-- @treturn string error
function Announcements:delete(id)
local announcement = self:get(id)
if not announcement then
return nil, "FIXME"
end
local success = announcement:delete()
return success and announcement or nil, "FIXME"
end
--- Get all announcements
-- @treturn boolean success
-- @treturn string error
function Announcements:get_all()
local announcements = self:select("order by board_id asc")
return announcements
end
--- Get announcements
-- @treturn boolean success
-- @treturn string error
function Announcements:get_global()
local announcements = self:select("where board_id=0")
return announcements
end
--- Get board announcements
-- @tparam number board_id Board ID
-- @treturn boolean success
-- @treturn string error
function Announcements:get_board(board_id)
local announcements = self:select("where board_id=?", board_id)
return announcements
end
--- Get announcement
-- @tparam number id Announcement ID
-- @treturn boolean success
-- @treturn string error
function Announcements:get(id)
local announcement = self:find(id)
return announcement and announcement or nil, "FIXME"
end
function Announcements.format_to_db(_, params)
if not params.board_id then
params.board_id = 0
end
end
function Announcements.format_from_db(_, params)
if params.board_id == 0 then
params.board_id = nil
end
end
return Announcements

@ -1,151 +1,23 @@
local Model = require("lapis.db.model").Model local lapis = require "lapis"
local Bans = Model:extend("bans", { local capture = require("lapis.application").capture_errors_json
relations = { local r2 = require("lapis.application").respond_to
{ "board", belongs_to="Boards" }, local handle = require("utils.error").handle
} local app = lapis.Application()
}) app.__base = app
app.name = "api.bans."
Bans.valid_record = { app.path = "/api/bans"
{ "board_id", is_integer=true },
{ "ip", max_length=255, exists=true }, app:match("bans", "", capture({
{ "time", exists=true } on_error = handle,
} r2(require "apps.api.bans.bans"),
}))
--- Create a ban app:match("ban", "/:uri_ban[%d]", capture({
-- @tparam table params Ban parameters on_error = handle,
-- @treturn boolean success r2(require "apps.api.bans.ban"),
-- @treturn string err }))
function Bans:new(params) app:match("bans_ip", "/ip/:uri_ip", capture({
local ban = self:create(params) on_error = handle,
return ban and ban or nil, { "err_create_ban", { params.ip } } r2(require "apps.api.bans.bans_ip"),
end }))
--- Modify a ban return app
-- @tparam table params Board parameters
-- @treturn boolean success
-- @treturn string error
function Bans:modify(params)
local ban = self:get(params.id)
if not ban then
return nil, { "err_create_board", { params.name, params.title } } -- FIXME: wrong error message
end
local success, err = ban:update(params)
return success and ban or nil, "FIXME: " .. tostring(err)
end
--- Delete a ban
-- @tparam number id Ban's ID
-- @treturn boolean success
-- @treturn string err
function Bans:delete(id)
local ban = self:get(id)
if not ban then
return nil, "FIXME"
end
local success = ban:delete()
return success and ban or nil, "FIXME"
end
--- Get all bans
-- @treturn table users List of bans
function Bans:get_all()
local bans = self:select("order by board_id asc, time + duration desc, ip asc")
for i=#bans, 1, -1 do
local ban = bans[i]
if not self:validate(ban) then
table.remove(bans, i)
end
end
return bans
end
--- Get board bans
-- @treturn table users List of bans
function Bans:get_board(board_id)
local bans = self:select("where board_id=? order by board_id asc, time + duration desc, ip asc", board_id)
for i=#bans, 1, -1 do
local ban = bans[i]
if not self:validate(ban) then
table.remove(bans, i)
end
end
return bans
end
--- Get ban data
-- @tparam number id Ban's ID
-- @treturn table ban
function Bans:get(id)
local ban = self:find(id)
if not ban then
return nil, "FIXME: ALART!"
end
local valid, err = self:validate(ban)
if not valid then
return nil, err
end
return ban
end
--- Get bans for specific ip
-- @tparam string ip IP address
-- @treturn table ban
function Bans:get_ip(ip)
local bans = self:select("where ip=?", ip)
for i=#bans, 1, -1 do
local ban = bans[i]
if not self:validate(ban) then
table.remove(bans, i)
end
end
return bans
end
--- Validate ban
-- @tparam table ban Ban data
-- @treturn boolean valid
function Bans:validate(ban)
local time = os.time()
local finish = ban.time + ban.duration
if time >= finish then
self:delete(ban)
return nil, "FIXME: ban has exired"
end
return true
end
--- Format ban paramaters for DB insertion
-- @tparam table params Ban parameters
function Bans.format_to_db(_, params)
-- Convert duration from days to seconds
params.duration = (tonumber(params.duration) or 0) * 86400
if not params.board_id then
params.board_id = 0
end
end
--- Format ban parameters for User consumption
-- @tparam table params Ban parameters
function Bans.format_from_db(_, params)
-- Convert duration from seconds to days
params.duration = tonumber(params.duration) / 86400
if params.board_id == 0 then
params.board_id = nil
end
end
return Bans

@ -1,226 +0,0 @@
local db = require "lapis.db"
local giflib = require "giflib"
local lfs = require "lfs"
local magick = require "magick"
local Model = require("lapis.db.model").Model
local Boards = Model:extend("boards", {
relations = {
{ "announcements", has_many="Announcements" },
{ "bans", has_many="Bans" },
{ "posts", has_many="Posts" },
{ "reports", has_many="Reports" },
{ "threads", has_many="Threads", where={ archive=false }, order="sticky desc, last_active desc" },
{ "archived", has_many="Threads", where={ archive=true }, order="last_active desc" },
}
})
Boards.valid_record = {
{ "name", max_length=255, exists=true },
{ "title", max_length=255, exists=true },
{ "subtext", max_length=255 },
{ "ban_message", max_length=255 },
{ "anon_name", max_length=255 },
{ "theme", max_length=255 },
{ "pages", exists=true },
{ "threads_per_page", exists=true },
{ "thread_file_limit", exists=true },
{ "post_limit", exists=true },
{ "archive_time", exists=true },
{ "group", exists=true }
}
--- Create a board
-- @tparam table params Board parameters
-- @treturn boolean success
-- @treturn string error
function Boards:new(params)
local board = self:create(params)
if not board then
return false, { "err_create_board", { params.name, params.title } }
end
lfs.mkdir(string.format("./static/%s/", board.name))
return board
end
--- Modify a board
-- @tparam table params Board parameters
-- @tparam old_name Board's current short name
-- @treturn boolean success
-- @treturn string error
function Boards:modify(params, old_name)
local board = self:get(old_name)
if not board then
return false, { "err_create_board", { params.name, params.title } } -- FIXME: wrong error message
end
local success, err = board:update(params)
if not success then
return false, "FIXME: " .. tostring(err)
end
if board.name ~= old_name then
local old = string.format("./static/%s/", old_name)
local new = string.format("./static/%s/", board.name)
os.rename(old, new)
end
return board
end
--- Delete a board
-- @tparam string name Board's short name
-- @treturn boolean success
-- @treturn string error
function Boards:delete(name)
local board = self:get(name)
if not board then
return false, { "err_create_board", { name, name } } -- FIXME: wrong error message
end
local announcements = board:get_announcements()
local bans = board:get_bans()
local posts = board:get_posts()
local reports = board:get_reports()
local threads = board:get_threads()
local dir = string.format("./static/%s/", board.name)
-- Clear data
for _, announcement in ipairs(announcements) do announcement:delete() end
for _, ban in ipairs(bans) do ban:delete() end
for _, post in ipairs(posts) do post:delete() end
for _, report in ipairs(reports) do report:delete() end
for _, thread in ipairs(threads) do thread:delete() end
-- Clear filesystem
if lfs.attributes(dir, "mode") == "directory" then
-- Delete files
for file in lfs.dir(dir) do
os.remove(dir .. file)
end
-- Delete directory
lfs.rmdir(dir)
end
-- Clear board
local success = board:delete()
return success and board or false, { "err_delete_board", { board.name, board.title } }
end
--- Get all boards
-- @treturn table boards
function Boards:get_all()
local boards = self:select("order by boards.group asc, name asc")
return boards and boards or false, "FIXME: ALART!"
end
--- Get board data
-- @tparam string name Board's short name
-- @treturn table board
function Boards:get(name)
local board = self:find { name=name }
return board and board or false, "FIXME: ALART!"
end
--- Format board paramaters for DB insertion
-- @tparam table params Board parameters
function Boards.format_to_db(_, params)
-- Convert archive_time from days to seconds
params.archive_time = (tonumber(params.archive_time) or 0) * 86400
end
--- Format board parameters for User consumption
-- @tparam table params Board parameters
function Boards.format_from_db(_, params)
-- Convert archive_time from seconds to days
params.archive_time = tonumber(params.archive_time) / 86400
end
--- Regenerate thumbnails for all posts
-- @treturn none
function Boards.regen_thumbs(_)
local sql = [[
select
boards.name as board,
posts.thread_id,
posts.file_path,
posts.file_width,
posts.file_height,
posts.file_type
from posts
left join boards on
board_id = boards.id
where
file_path is not null and
file_width is not null and
file_height is not null and
file_type = 'image' and
file_spoiler = false
order by
board_id asc,
thread_id asc,
post_id asc
]]
local dir
local board
local thread = 0
local results = db.query(sql)
for _, result in ipairs(results) do
-- Change board, reset thread counter and image directory
if result.board ~= board then
board = result.board
thread = 0
dir = string.format("./static/%s/", board)
end
-- Filesystem paths
local name, ext = result.file_path:match("^(.+)(%..+)$")
ext = string.lower(ext)
local full_path = dir .. result.file_path
local thumb_path = dir .. "s" .. result.file_path
-- Generate a thumbnail
if ext == ".webm" then
thumb_path = dir .. "s" .. name .. ".png"
-- Create screenshot of first frame
os.execute(string.format("ffmpeg -i %s -ss 00:00:01 -vframes 1 %s -y", full_path, thumb_path))
end
-- Save thumbnail
local w, h
if result.thread_id > thread then
thread = result.thread_id
w = result.file_width < 250 and result.file_width or 250
h = result.file_height < 250 and result.file_height or 250
else
w = result.file_width < 125 and result.file_width or 125
h = result.file_height < 125 and result.file_height or 125
end
-- Grab first frame from video
if ext == ".webm" then
magick.thumb(thumb_path, string.format("%sx%s", w, h), thumb_path)
elseif ext == ".svg" then
thumb_path = dir .. "s" .. name .. ".png"
os.execute(string.format("convert -background none -resize %dx%d %s %s", w, h, full_path, thumb_path))
elseif ext == ".gif" then
-- Grab first frame of a gif instead of the last
local gif, err = giflib.load_gif(full_path)
if err then
magick.thumb(full_path, string.format("%sx%s", w, h), thumb_path)
else
gif:write_first_frame(thumb_path)
magick.thumb(thumb_path, string.format("%sx%s", w, h), thumb_path)
end
else
magick.thumb(full_path, string.format("%sx%s", w, h), thumb_path)
end
end
end
return Boards

@ -1,90 +0,0 @@
local Model = require("lapis.db.model").Model
local Pages = Model:extend("pages")
Pages.valid_record = {
{ "slug", exists=true, type="String" },
{ "title", exists=true, type="String" },
{ "content", exists=true, type="String" }
}
--- Create a new page
-- @tparam table page Page data
-- @treturn boolean success
-- @treturn string error
function Pages:new(params)
local unique, err = self:is_unique(params.slug, params.title)
if not unique then
return nil, err
end
local page = self:create(params)
return page and page or nil, { "err_create_page", { page.slug, page.title } }
end
--- Modify a page
-- @tparam table page Page data
-- @treturn boolean success
-- @treturn string error
function Pages:modify(params, slug)
local page = self:get(slug)
if not page then
return nil, "FIXME"
end
-- Check to see if the page we are modifying is the one that fails validation.
-- If it is, that's fine since we're updating the unique values with themselves.
-- If #pages > 1 then this will always fail since either the new slug or new
-- title is going to belong to some other page.
do
local unique, err, pages = self:is_unique(params.slug, params.title)
if not unique then
for _, p in ipairs(pages) do
if page.id ~= p.id then
return nil, err
end
end
end
end
local success, err = page:update(params)
return success and page or nil, "FIXME: " .. tostring(err)
end
--- Delete page
-- @tparam table page Page data
-- @treturn boolean success
-- @treturn string error
function Pages:delete(slug)
local page = self:get(slug)
if not page then
return nil, "FIXME"
end
local success = page:delete()
return success and page or nil, "FIXME"
end
--- Get all pages
-- @treturn table pages List of pages
function Pages:get_all()
return self:select("order by slug asc")
end
--- Get page
-- @tparam string slug Page slug
-- @treturn table page
function Pages:get(slug)
local page = self:find { slug=slug:lower() }
return page and page or nil, "FIXME"
end
function Pages:is_unique(slug, title)
local pages = self:select("where slug=? or lower(title)=?", slug, title:lower())
return #pages == 0 and true or nil, "FIXME", pages
end
function Pages.format_to_db(_, params)
params.slug = params.slug:lower()
end
return Pages

@ -1,387 +0,0 @@
local encoding = require "lapis.util.encoding"
local Model = require("lapis.db.model").Model
local giflib = require "giflib"
local magick = require "magick"
local md5 = require "md5"
local filetypes = require "utils.file_whitelist"
local generate = require "utils.generate"
local Posts = Model:extend("posts", {
relations = {
{ "board", belongs_to="Boards" },
{ "thread", belongs_to="Threads" },
}
})
local sf = string.format
local function get_duration(path)
local cmd = sf("ffprobe -i %s -show_entries format=duration -v quiet -of csv=\"p=0\" -sexagesimal", path)
local f = io.popen(cmd, "r")
local s = f:read("*a")
f:close()
local hr, mn, sc = string.match(s, "(%d+):(%d+):(%d+).%d+")
local d = mn..":"..sc
if hr ~= "0" then
d = hr..":"..d
end
return d
end
--- Prepare post for insertion
-- @tparam table params Input from the user
-- @tparam table session User session
-- @tparam table board Board data
-- @tparam table thread Thread data
-- @tparam number files Number of files in thread
-- @treturn boolean success
-- @treturn string error
function Posts:prepare_post(params, session, board, thread, files)
-- FIXME: this whole function should be web-side, not api-side
local time = os.time()
-- Prepare session stuff
session.password = session.password or generate.password(time)
-- Save names on individual boards
-- TODO: Put this code elsewhere
if params.name then
session.names[board.name] = params.name
end
-- Files take presidence over drawings, but if there is no file, fill in
-- the file fields with draw data. This should avoid ugly code branching
-- later on in the file.
-- TODO: send file data through api!
params.file = params.file or {
filename = "",
content = ""
}
if #params.file.content == 0 and params.draw then
local pattern = ".-,(.+)"
params.draw = params.draw:match(pattern)
params.file.filename = "tegaki.png"
params.file.content = encoding.decode_base64(params.draw)
end
-- Check board flags
if thread then
if thread.lock and not session.admin and not session.mod then
return false, { "err_locked_thread", { thread.post_id } }
end
if board.post_comment and not params.comment then
return false, { "err_comment_post" }
end
if board.post_file and #params.file.content == 0 then
return false, { "err_file_post" }
end
else
if board.thread_comment and not params.comment then
return false, { "err_comment_thread" }
end
if board.thread_file and #params.file.content == 0 then
return false, { "err_file_thread" }
end
end
-- Parse name
if params.name then
params.name, params.trip = generate.tripcode(params.name)
end
-- Set file
if #params.file.content > 0 then
-- Reject files in text-only boards
if board.text_only then
return false, { "err_no_files" }
end
-- Thread limit is already met.
if thread then
if files >= board.thread_file_limit and not thread.size_override then
return false, { "err_file_limit", { thread.post_id } }
end
end
local name = sf("%s%s", time, generate.random())
local ext = params.file.filename:match("^.+(%..+)$")
ext = string.lower(ext)
-- Figure out how to deal with the file
if filetypes.image[ext] and board.filetype_image then
params.file_type = "image"
if ext ~= ".webm" then
-- Check if valid image
local image = magick.load_image_from_blob(params.file.content)
if not image then
return false, { "err_invalid_image" }
end
params.file_width = image:get_width()
params.file_height = image:get_height()
end
elseif filetypes.audio[ext] and board.filetype_audio then
params.file_type = "audio"
else
return false, { "err_invalid_ext", { ext } }
end
params.file_name = params.file.filename
params.file_path = name .. ext
params.file_md5 = md5.sumhexa(params.file.content)
params.file_size = #params.file.content
if params.file_spoiler then
params.file_spoiler = true
else
params.file_spoiler = false
end
-- Check if file already exists
local file = self:find_file(board.id, params.file_md5)
if file then
return false, { "err_file_exists" }
end
else
params.file_spoiler = false
end
-- Check contributions
if not params.comment and not params.file_name then
return false, { "err_contribute" }
end
return true
end
--- Create a new post
-- @tparam table params Post parameters
-- @tparam table board Board data
-- @tparam boolean op OP flag
-- @treturn boolean success
-- @treturn string error
function Posts:new(params, board, op)
-- Create post
local post = self:create(params)
if not post then
return false, { "err_create_post" }
end
-- Save file
if post.file_path then
local dir = sf("./static/%s/", board.name)
local name, ext = post.file_path:match("^(.+)%.(.+)$")
ext = string.lower(ext)
-- Filesystem paths
local full_path = dir .. post.file_path
local thumb_path = dir .. "s" .. post.file_path
-- Save file
local file = io.open(full_path, "w")
file:write(params.file_content)
file:close()
-- Audio file
if post.file_type == "audio" then
post.file_duration = get_duration(full_path)
post:update("file_duration")
return post
end
-- Image file
if post.file_type == "image" and not post.file_spoiler then
-- Save thumbnail
local w, h
if op then
w = post.file_width < 250 and post.file_width or 250
h = post.file_height < 250 and post.file_height or 250
else
w = post.file_width < 125 and post.file_width or 125
h = post.file_height < 125 and post.file_height or 125
end
-- Generate a thumbnail
if ext == "webm" then
thumb_path = dir .. "s" .. name .. ".png"
-- Create screenshot of first frame
os.execute(sf("ffmpeg -i %s -ss 00:00:01 -vframes 1 %s -y", full_path, thumb_path))
-- Update post info
local image = magick.load_image(thumb_path)
post.file_width = image:get_width()
post.file_height = image:get_height()
post.file_duration = get_duration(full_path)
post:update("file_width", "file_height", "file_duration")
-- Resize thumbnail
magick.thumb(thumb_path, sf("%sx%s", w, h), thumb_path)
elseif ext == "svg" then
thumb_path = dir .. "s" .. name .. ".png"
os.execute(sf("convert -background none -resize %dx%d %s %s", w, h, full_path, thumb_path))
elseif ext == "gif" then
local gif, err = giflib.load_gif(full_path)
if err then
-- Not animated I presume? TODO: check what err represents
magick.thumb(full_path, sf("%sx%s", w, h), thumb_path)
else
-- Grab first frame of a gif instead of the last
gif:write_first_frame(thumb_path)
-- Update post info
local width, height = gif:dimensions()
post.file_width = width
post.file_height = height
post:update("file_width", "file_height")
-- Resize thumbnail
magick.thumb(thumb_path, sf("%sx%s", w, h), thumb_path)
end
else
magick.thumb(full_path, sf("%sx%s", w, h), thumb_path)
end
end
end
-- Update board
board:update("total_posts")
return post
end
--- Delete post data
-- @tparam number id Post ID
-- @treturn boolean success
-- @treturn string error
function Posts:delete(id)
-- Get post
local post, err = self:get_post_by_id(id)
if not post then
return false, err
end
-- Delete post
local success, err = post:delete()
if not success then
return false, err--{ "err_delete_post", { post.post_id } }
end
-- Delete files
if post.file_path then
local board = post:get_board()
local dir = sf("./static/%s/", board.name)
local name, ext = post.file_path:match("^(.+)%.(.+)$")
ext = string.lower(ext)
os.remove(dir .. post.file_path)
-- Change thumbnail path to png
if ext == "webm" or ext == "svg" then
post.file_path = name .. ".png"
end
os.remove(dir .. "s" .. post.file_path)
end
return post
end
--- Get op and last 5 posts of a thread to display on board index
-- @tparam number thread_id Thread ID
-- @treturn table posts
function Posts:get_index_posts(thread_id)
local sql = "where thread_id=? order by post_id desc limit 5"
local posts = self:select(sql, thread_id)
if self:count_posts(thread_id) > 5 then
local thread = posts[1]:get_thread()
local op = thread:get_op()
table.insert(posts, op)
end
return posts
end
--- Get post data
-- @tparam number board_id Board ID
-- @tparam number post_id Local Post ID
-- @treturn table post
function Posts:get(board_id, post_id)
local post = self:find {
board_id = board_id,
post_id = post_id
}
return post and post or false, "FIXME"
end
--- Get post data
-- @tparam number id Post ID
-- @treturn table post
function Posts:get_post_by_id(id)
local post = self:find(id)
return post and post or false, "FIXME"
end
--- Find file in active posts
-- @tparam number board Board ID
-- @tparam string file_md5 Unique hash of file
-- @treturn boolean success
-- @treturn string error
function Posts:find_file(board_id, file_md5)
local sql = "where board_id=? and file_md5=? limit 1"
return unpack(self:select(sql, board_id, file_md5))
end
--- Count hidden posts in a thread
-- @tparam number thread_id Thread ID
-- @treturn table hidden
function Posts:count_hidden_posts(thread_id)
local posts = self:get_index_posts(thread_id)
local num_posts = self:count_posts(thread_id)
local num_files = self:count_files(thread_id)
for _, post in ipairs(posts) do
-- Reduce number of posts hidden
num_posts = num_posts - 1
-- Reduce number of files hidden
if post.file_name then
num_files = num_files - 1
end
end
return { posts=num_posts, files=num_files }
end
--- Count posts in a thread
-- @tparam number thread_id Thread ID
-- @treturn number posts
function Posts:count_posts(thread_id)
local sql = "thread_id=?"
return self:count(sql, thread_id)
end
--- Count posts with images in a thread
-- @tparam number thread_id Thread ID
-- @treturn number files
function Posts:count_files(thread_id)
local sql = "thread_id=? and file_name is not null"
return self:count(sql, thread_id)
end
return Posts

@ -1,76 +0,0 @@
local trim = require("lapis.util").trim_filter
local Model = require("lapis.db.model").Model
local Reports = Model:extend("reports")
--- Create a new report
-- @tparam table report Report data
-- @treturn boolean success
-- @treturn string error
function Reports:create_report(report)
-- Trim white space
trim(report, {
"board_id", "thread_id", "post_id",
"timestamp", "num_reports"
}, nil)
local r = self:create {
board_id = report.board_id,
thread_id = report.thread_id,
post_id = report.post_id,
timestamp = report.timestamp,
num_reports = report.num_reports
}
if r then
return r
end
return false, { "err_create_report" }
end
--- Modify a report
-- @tparam table report Report data
-- @treturn boolean success
-- @treturn string error
function Reports:modify_report(report)
local columns = {}
for col in pairs(report) do
table.insert(columns, col)
end
return report:update(unpack(columns))
end
--- Delete report
-- @tparam table report Report data
-- @treturn boolean success
-- @treturn string error
function Reports:delete_report(report)
return report:delete()
end
--- Get all reports
-- @treturn table reports List of reports
function Reports:get_reports()
return self:select("order by timestamp asc")
end
--- Get report
-- @tparam string board_id Board ID
-- @tparam string post_id Post ID
-- @treturn table report
function Reports:get_report(board_id, post_id)
return unpack(self:select(
"where board_id=? and post_id=? limit 1",
board_id, post_id
))
end
--- Get report
-- @tparam string id Report ID
-- @treturn table report
function Reports:get_report_by_id(id)
return unpack(self:select("where id=? limit 1", id))
end
return Reports

@ -1,106 +0,0 @@
local Model = require("lapis.db.model").Model
local Threads = Model:extend("threads", {
relations = {
{ "board", belongs_to="Boards" },
{ "posts", has_many="Posts" },
{ "op", has_one="Posts", order="post_id asc" },
}
})
Threads.valid_record = {
{ "board_id", exists=true }
}
--- Create thread
-- @tparam table params Thread parameters
-- @treturn boolean success
-- @treturn string error
function Threads:new(params)
local thread = self:create(params)
return thread and thread or false, { "err_create_thread" }
end
--- Modify a thread
-- @tparam table params Thread parameters
-- @treturn boolean success
-- @treturn string error
function Threads:modify(params)
local thread = self:get(params.id)
if not thread then
return false, { "err_create_board" } -- FIXME: wrong error message
end
local success, err = thread:update(params)
return success and thread or false, "FIXME: " .. tostring(err)
end
--- Delete entire thread
-- @tparam number id Thread ID
-- @treturn boolean success
-- @treturn string error
function Threads:delete(id)
-- FIXME: API needs to create a user object for better auth checking
local thread, err = self:get(id)
if not thread then
return false, err
end
local op = thread:get_op()
local success = thread:delete()
return success and thread or false, { "err_delete_thread", { op.post_id } }
end
--- Get thread data
-- @tparam number id Thread ID
-- @treturn table thread
function Threads:get(id)
local thread = self:find(id)
return thread and thread or false, "FIXME"
end
--- Get archived threads
-- @tparam number board_id Board ID
-- @treturn table threads
function Threads:get_archived(board_id)
local sql = "where board_id=? and archive=true order by last_active desc"
return self:select(sql, board_id)
end
--- Bump threads to archive
-- @tparam number board_id Board ID
-- @tparam number max_threads Maximum number of threads on this board
-- @treturn boolean success
-- @treturn string error
function Threads:archive_threads(board_id, max_threads)
local threads = self:get_threads(board_id)
if #threads > max_threads then
for i=max_threads+1, #threads do
local _, err = self:archive_thread(threads[i])
if err then
return false, err
end
end
end
end
--- Archive a thread
-- @tparam table thread Thread data
-- @treturn boolean success
-- @treturn string error
function Threads:archive_thread(thread)
thread.sticky = false
thread.lock = true
thread.archive = true
thread.last_active = os.time()
return thread:update("sticky", "lock", "archive", "last_active")
end
--- Find threads with no posts
-- @treturn table threads
function Threads:find_orphans()
return self:select("where id not in (select distinct thread_id from posts)")
end
return Threads

@ -1,30 +1,34 @@
local bcrypt = require "bcrypt" local bcrypt = require "bcrypt"
local uuid = require "resty.jit-uuid" local uuid = require "resty.jit-uuid"
local config = require("lapis.config").get() local config = require("lapis.config").get()
local Model = require("lapis.db.model").Model local Model = require("lapis.db.model").Model
local Users = Model:extend("users") local Users = Model:extend("users")
local token = config.secret local token = config.secret
Users.role = { Users.role = {
[-1] = "INVALID", [-1] = "INVALID",
[1] = "USER", [1] = "USER",
[6] = "JANITOR", [6] = "JANITOR",
[7] = "MOD", [7] = "MOD",
[8] = "ADMIN", [8] = "ADMIN",
[9] = "OWNER", [9] = "OWNER",
INVALID = -1, INVALID = -1,
USER = 1, USER = 1,
JANITOR = 6, JANITOR = 6,
MOD = 7, MOD = 7,
ADMIN = 8, ADMIN = 8,
OWNER = 9 OWNER = 9,
} }
Users.valid_record = { Users.valid_record = {{
{ "username", exists=true }, "username",
{ "role", exists=true, is_integer=true } exists = true,
} }, {
"role",
exists = true,
is_integer = true,
}}
Users.default_key = "00000000-0000-0000-0000-000000000000" Users.default_key = "00000000-0000-0000-0000-000000000000"
@ -34,31 +38,37 @@ Users.default_key = "00000000-0000-0000-0000-000000000000"
-- @treturn string error -- @treturn string error
function Users:new(params, raw_password) function Users:new(params, raw_password)
-- Check if username is unique -- Check if username is unique
do do
local unique, err = self:is_unique(params.username) local unique, err = self:is_unique(params.username)
if not unique then return nil, err end if not unique then
end return nil, err
end
-- Verify password end
do
local valid, err = self:validate_password(params.password, params.confirm, raw_password) -- Verify password
if not valid then return nil, err end do
local valid, err = self:validate_password(params.password, params.confirm, raw_password)
params.confirm = nil if not valid then
params.password = bcrypt.digest(params.username:lower() .. params.password .. token, 12) return nil, err
end end
-- Generate unique API key params.confirm = nil
do params.password = bcrypt.digest(params.username:lower() .. params.password .. token, 12)
local api_key, err = self:generate_api_key() end
if not api_key then return nil, err end
-- Generate unique API key
params.api_key = api_key do
end local api_key, err = self:generate_api_key()
if not api_key then
local user = self:create(params) return nil, err
return user and user or nil, { "err_create_user", { params.username } } end
params.api_key = api_key
end
local user = self:create(params)
return user and user or nil, {"err_create_user", {params.username}}
end end
--- Modify a user --- Modify a user
@ -66,34 +76,42 @@ end
-- @treturn boolean success -- @treturn boolean success
-- @treturn string error -- @treturn string error
function Users:modify(params, raw_username, raw_password) function Users:modify(params, raw_username, raw_password)
local user = self:get(raw_username) local user = self:get(raw_username)
if not user then return nil, "FIXME" end if not user then
return nil, "FIXME"
-- Check if username is unique end
do
local unique, err, u = self:is_unique(params.username) -- Check if username is unique
if not unique and user.id ~= u.id then return nil, err end do
end local unique, err, u = self:is_unique(params.username)
if not unique and user.id ~= u.id then
-- Verify password return nil, err
if params.password then end
local valid, err = self:validate_password(params.password, params.confirm, raw_password) end
if not valid then return nil, err end
-- Verify password
params.confirm = nil if params.password then
params.password = bcrypt.digest(params.username:lower() .. params.password .. token, 12) local valid, err = self:validate_password(params.password, params.confirm, raw_password)
end if not valid then
return nil, err
-- Generate unique API key end
if params.api_key then
local api_key, err = self:generate_api_key() params.confirm = nil
if not api_key then return nil, err end params.password = bcrypt.digest(params.username:lower() .. params.password .. token, 12)
end
params.api_key = api_key
end -- Generate unique API key
if params.api_key then
local success, err = user:update(params) local api_key, err = self:generate_api_key()
return success and user or nil, "FIXME: " .. tostring(err) if not api_key then
return nil, err
end
params.api_key = api_key
end
local success, err = user:update(params)
return success and user or nil, "FIXME: " .. tostring(err)
end end
--- Delete user --- Delete user
@ -101,13 +119,13 @@ end
-- @treturn boolean success -- @treturn boolean success
-- @treturn string error -- @treturn string error
function Users:delete(username) function Users:delete(username)
local user = self:get(username) local user = self:get(username)
if not user then if not user then
return nil, "FIXME" return nil, "FIXME"
end end
local success = user:delete() local success = user:delete()
return success and user or nil, "FIXME" return success and user or nil, "FIXME"
end end
--- Verify user --- Verify user
@ -115,67 +133,73 @@ end
-- @treturn boolean success -- @treturn boolean success
-- @treturn string error -- @treturn string error
function Users:login(params) function Users:login(params)
local user = self:get(params.username) local user = self:get(params.username)
if not user then return nil, { "err_invalid_user" } end if not user then
return nil, {"err_invalid_user"}
end
local password = user.username .. params.password .. token local password = user.username .. params.password .. token
local verified = bcrypt.verify(password, user.password) local verified = bcrypt.verify(password, user.password)
return verified and user or nil, { "err_invalid_user" } return verified and user or nil, {"err_invalid_user"}
end end
--- Get all users --- Get all users
-- @treturn table users List of users -- @treturn table users List of users
function Users:get_all() function Users:get_all()
local users = self:select("order by username asc") local users = self:select("order by username asc")
return users return users
end end
--- Get user --- Get user
-- @tparam string username Username -- @tparam string username Username
-- @treturn table user -- @treturn table user
function Users:get(username) function Users:get(username)
local users = self:select("where lower(username)=? limit 1", username:lower()) local users = self:select("where lower(username)=? limit 1", username:lower())
return #users == 1 and users[1] or nil, "FIXME" return #users == 1 and users[1] or nil, "FIXME"
end end
function Users:get_api(params) function Users:get_api(params)
local user = self:find(params) local user = self:find(params)
return user and user or nil, "FIXME" return user and user or nil, "FIXME"
end end
function Users:format_to_db(params) function Users:format_to_db(params)
if not params.role then if not params.role then
params.role = self.role.INVALID params.role = self.role.INVALID
end end
end end
function Users.format_from_db(_, params) function Users.format_from_db(_, params)
params.password = nil params.password = nil
params.api_key = nil params.api_key = nil
end end
function Users:is_unique(username) function Users:is_unique(username)
local user = self:get(username) local user = self:get(username)
return not user and true or nil, "FIXME", user return not user and true or nil, "FIXME", user
end end
function Users.validate_password(_, password, confirm, old_password) function Users.validate_password(_, password, confirm, old_password)
if password ~= confirm or password ~= old_password then if password ~= confirm or password ~= old_password then
return nil, "FIXME" return nil, "FIXME"
end end
return true return true
end end
function Users:generate_api_key() function Users:generate_api_key()
for _ = 1, 10 do for _ = 1, 10 do
local api_key = uuid() local api_key = uuid()
local user = self:find { api_key=api_key } local user = self:find{
if not user then return api_key end api_key = api_key,
end }
if not user then
return nil, "FIXME" return api_key
end
end
return nil, "FIXME"
end end
return Users return Users

@ -1,109 +0,0 @@
$spacing: 10px;
.thread_container, .post_container {
margin: $spacing 0 0 0;
padding: $spacing;
}
.thread_container {
overflow: visible;
.op, .post_container {
position: relative;
}
.op {
.post_admin {
margin-left: $spacing * 35;
}
}
.post_container {
display: table;
min-width: 300px;
max-width: 100%;
.post_admin {
margin-left: $spacing * 2;
}
}
.post_file {
margin: 0 0 3px 0;
a {
text-decoration: underline;
}
}
.post_image {
float: left;
margin: 0 $spacing * 2 0 $spacing;
img {
max-width: 1000px;
max-height: 1000px;
}
}
.post_subject, .post_name {
font-weight: bold;
}
.post_comment {
padding: $spacing $spacing * 1.5;
a {
text-decoration: underline;
}
.broken_link {
text-decoration: line-through;
}
.spoiler {
background-color: #000000;
color: #000000;
&:hover {
color: #ffffff;
}
}
.post_banned {
color: #ff0000;
font-weight: bold;
margin-top: $spacing * 2;
}
}
.post_flag, .post_menu {
cursor: pointer;
}
.post_menu {
position: relative;
display: inline-block;
.menu {
display: none;
position: absolute;
z-index: 9;
div {
padding: 3px;
form {
margin: 0;
padding: 0;
input[type="submit"], button {
background: none;
border: none;
padding: 0;
}
}
}
}
}
}

@ -1,141 +0,0 @@
/*
HTML5 Reset :: style.css
----------------------------------------------------------
We have learned much from/been inspired by/taken code where offered from:
Eric Meyer :: http://meyerweb.com
HTML5 Doctor :: http://html5doctor.com
and the HTML5 Boilerplate :: http://html5boilerplate.com
-------------------------------------------------------------------------------*/
/* Let's default this puppy out
-------------------------------------------------------------------------------*/
html, body, body div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, figure, footer, header, menu, nav, section, time, mark, audio, video, details, summary {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font-weight: normal;
vertical-align: baseline;
background: transparent;
}
article, aside, figure, footer, header, nav, section, details, summary {display: block;}
/* Handle box-sizing while better addressing child elements:
http://css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice/ */
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
/* consider resetting the default cursor: https://gist.github.com/murtaugh/5247154 */
/* Responsive images and other embedded objects */
/* if you don't have full control over `img` tags (if you have to overcome attributes), consider adding height: auto */
img,
object,
embed {max-width: 100%;}
/*
Note: keeping IMG here will cause problems if you're using foreground images as sprites.
In fact, it *will* cause problems with Google Maps' controls at small size.
If this is the case for you, try uncommenting the following:
#map img {
max-width: none;
}
*/
/* force a vertical scrollbar to prevent a jumpy page */
html {overflow-y: scroll;}
/* we use a lot of ULs that aren't bulleted.
you'll have to restore the bullets within content,
which is fine because they're probably customized anyway */
ul {list-style: none;}
blockquote, q {quotes: none;}
blockquote:before,
blockquote:after,
q:before,
q:after {content: ''; content: none;}
a {margin: 0; padding: 0; font-size: 100%; vertical-align: baseline; background: transparent;}
del {text-decoration: line-through;}
abbr[title], dfn[title] {border-bottom: 1px dotted #000; cursor: help;}
/* tables still need cellspacing="0" in the markup */
table {border-collapse: collapse; border-spacing: 0;}
th {font-weight: bold; vertical-align: bottom;}
td {font-weight: normal; vertical-align: top;}
hr {display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0;}
input, select {vertical-align: middle;}
pre {
white-space: pre; /* CSS2 */
white-space: pre-wrap; /* CSS 2.1 */
white-space: pre-line; /* CSS 3 (and 2.1 as well, actually) */
word-wrap: break-word; /* IE */
}
input[type="radio"] {vertical-align: text-bottom;}
.ie7 input[type="checkbox"] {vertical-align: baseline;}
.ie6 input {vertical-align: text-bottom;}
select, input, textarea {font: 99% sans-serif;}
table {font-size: inherit; font: 100%;}
small {font-size: 85%;}
strong {font-weight: bold;}
td, td img {vertical-align: top;}
/* Make sure sup and sub don't mess with your line-heights http://gist.github.com/413930 */
sub, sup {font-size: 75%; line-height: 0; position: relative;}
sup {top: -0.5em;}
sub {bottom: -0.25em;}
/* standardize any monospaced elements */
pre, code, kbd, samp {font-family: monospace, sans-serif;}
/* hand cursor on clickable elements */
.clickable,
input[type=button],
input[type=submit],
input[type=file],
button {cursor: pointer;}
/* Webkit browsers add a 2px margin outside the chrome of form elements */
button, input, select, textarea {margin: 0;}
/* make buttons play nice in IE */
button,
input[type=button] {width: auto; overflow: visible;}
/* scale images in IE7 more attractively */
.ie7 img {-ms-interpolation-mode: bicubic;}
/* prevent BG image flicker upon hover
(commented out as usage is rare, and the filter syntax messes with some pre-processors)
.ie6 html {filter: expression(document.execCommand("BackgroundImageCache", false, true));}
*/
/* let's clear some floats */
.clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; }
.clearfix:after { clear: both; }
.clearfix { zoom: 1; }

@ -1,322 +0,0 @@
@import "reset";
@import "posts";
@mixin display-flex {
display: -webkit-box-flex;
display: -webkit-flexbox;
display: -ms-flexbox;
display: -webkit-box;
display: flex;
}
@mixin flex($args) {
-webkit-flex: $args;
-ms-flex: $args;
flex: $args;
}
@mixin flex-direction($dir) {
$alt: vertical !default;
@if $dir == column {
$alt: vertical;
} @else {
$alt: horizontal;
}
-webkit-flex-direction: column;
-webkit-box-orient: $alt;
-ms-flex-direction: column;
flex-direction: column;
}
$spacing: 10px;
body {
margin: $spacing;
font-size: 10pt;
font-family: Helvetica, "Noto Sans", "Roboto Sans", Arial, sans-serif;
}
a {
text-decoration: none;
}
input[type="text"],
input[type="password"],
textarea,
select,
.admin_main {
padding: 4px;
width: 280px;
}
input[type="checkbox"] {
margin: 4px 0;
}
input[type="file"] {
padding: 4px 0;
}
h1 {
font-size: 240%;
font-weight: bolder;
margin-top: 10px;
text-align: center;
}
h2 {
margin: $spacing 0;
text-align: center;
}
hr {
clear: left;
}
form {
display: inline-block;
margin: $spacing / 2 auto;
padding: $spacing;
}
table {
border-collapse: separate;
border-spacing: 1px;
margin: 0 auto;
max-width: 80%;
thead {
vertical-align: middle;
td {
font-weight: bolder;
padding: $spacing / 2 $spacing;
}
}
td {
padding: 3px $spacing / 2;
text-align: center;
}
.rules {
text-align: left;
}
}
ul {
list-style: disc;
li {
margin-left: $spacing * 2;
}
}
ol {
list-style: decimal;
li {
margin-left: $spacing * 2;
}
}
#error {
background-color: #ff0000;
color: #ffffff;
margin-bottom: $spacing;
padding: $spacing;
text-align: center;
h3 {
font-size: 220%;
font-weight: bolder;
}
p {
font-weight: bold;
}
}
#post_form, .admin_form {
text-align: center;
form {
div {
@include display-flex;
.title {
@include flex(0);
font-weight: bold;
text-align: right;
min-width: 120px;
margin: 1px;
padding: 4px;
span {
cursor: pointer;
}
}
.fields {
margin: 1px;
textarea {
min-height: 100px !important;
}
}
}
button {
margin: $spacing 0 0 0;
}
}
}
.admin_form {
form {
margin: 0;
padding: 0;
button {
margin: 0;
}
}
}
.required {
color: #ff0000;
}
.boards, .return, .copyright {
text-align: center;
}
.archive_stats {
text-align: center;
margin-bottom: $spacing;
}
.announcement {
text-align: center;
font-weight: bold;
}
.banned {
border: 7px dashed #000000;
display: table;
font-size: 180%;
min-width: 200px;
max-width: 600px;
margin: $spacing * 2 auto;
padding: $spacing;
p {
margin: $spacing;
}
div {
text-align: center;
}
}
.medium {
width: 120px !important;
}
.short {
width: 40px !important;
}
.catalog_container {
display: inline-block;
overflow: hidden;
text-align: center;
vertical-align: top;
width: 135px;
.catalog_stats {
font-size: 70%;
}
img {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);
max-width: 125px;
max-height: 125px;
}
}
.logout {
top: $spacing;
right: $spacing;
position: absolute;
}
.copyright {
margin-top: $spacing;
}
.front_page {
h2 {
font-size: 180%;
}
h3 {
font-size: 140%;
margin-top: $spacing;
}
.board {
display: inline-block;
max-width: 300px;
margin: $spacing * 2;
vertical-align: top;
a {
font-size: 160%;
font-weight: bold;
}
.subtext {
display: block;
font-size: 120%;
}
}
}
.table_of_contents {
display: table;
margin: 0 auto;
max-width: 400px;
}
.answers,{
max-width: 600px;
margin: 0 auto;
h2 {
font-size: 160%;
}
}
.install {
max-width: 300px;
margin: 0 auto;
h2 {
font-size: 160%;
margin-top: $spacing * 2;
}
label {
font-weight: bold;
margin-top: $spacing * 2;
}
button {
font-size: 120%;
font-weight: bold;
margin-top: $spacing * 2;
padding: $spacing / 2;
}
}

@ -1,113 +0,0 @@
$background: #ffffee;
$bg_light: #ffffff;
$border: #d9bfb7;
$subject: #cc1105;
$header: #800000;
$link: #0000ee;
$hover: #ff0000;
$quote: #000080;
$post: #f0e0d6;
$name: #117743;
$black: #000000;
$green: #789922;
$blue: #292299;
$active: #f0c0b0;
$active_border: #d99f91;
body {
background-color: $background;
}
body, h1, h2 {
color: $header;
}
a, a:visited, .broken_link {
color: $link;
}
a:hover, .quote_link:hover, .broken_link:hover {
color: $hover;
}
.quote_link {
color: $quote;
}
.quote_green {
color: $green;
}
.quote_blue {
color: $blue;
}
.announcement {
color: $header;
}
#post_form, .admin_form {
form {
.title {
background-color: $active;
border: 1px solid $active_border;
}
}
}
table {
thead {
td {
background-color: $post;
border: 1px solid $border;
}
}
tr:nth-of-type(odd) {
background-color: $background;
}
tr:nth-of-type(even) {
background-color: $bg_light;
}
}
.post_container {
background-color: $post;
border-right: 1px solid $border;
border-bottom: 1px solid $border;
&:target {
background-color: $active;
border-right: 1px solid $active_border;
border-bottom: 1px solid $active_border;
}
}
.post_subject {
color: $subject;
}
.post_name, .post_trip {
color: $name;
}
.post_menu .menu {
background-color: $post;
border:1px solid $border;
div {
border-right: 1px solid $border;
border-bottom: 1px solid $border;
form {
button {
color: $header;
&:hover {
color: $hover;
}
}
}
}
}

@ -1,109 +0,0 @@
$background: #eef2ff;
$bg_light: #f7f9ff;
$border: #b7c5d9;
$subject: #0f0c5d;
$header: #af0a0f;
$link: #34345c;
$hover: #dd0000;
$post: #d6daf0;
$name: #117743;
$black: #000000;
$green: #789922;
$blue: #292299;
$active: #d6bad0;
$active_border: #ba9dbf;
body {
background-color: $background;
color: $black;
}
h1, h2 {
color: $header;
}
a, a:visited, .broken_link {
color: $link;
}
a:hover, .quote_link, .quote_link:visited, .broken_link:hover {
color: $hover;
}
.quote_green {
color: $green;
}
.quote_blue {
color: $blue;
}
.announcement {
color: $header;
}
#post_form, .admin_form {
form {
.title {
background-color: $active;
border: 1px solid $active_border;
}
}
}
table {
thead {
td {
background-color: $post;
border: 1px solid $border;
}
}
tr:nth-of-type(odd) {
background-color: $background;
}
tr:nth-of-type(even) {
background-color: $bg_light;
}
}
.post_container {
background-color: $post;
border-right: 1px solid $border;
border-bottom: 1px solid $border;
&:target {
background-color: $active;
border-right: 1px solid $active_border;
border-bottom: 1px solid $active_border;
}
}
.post_subject {
color: $subject;
}
.post_name, .post_trip {
color: $name;
}
.post_menu .menu {
background-color: $post;
border:1px solid $border;
div {
border-right: 1px solid $border;
border-bottom: 1px solid $border;
form {
button {
color: $link;
&:hover {
color: $hover;
}
}
}
}
}

@ -1,35 +1,37 @@
local ngx = _G.ngx local ngx = _G.ngx
local json = require "cjson" local json = require"utils.json"
local function capture(method, uri, body) local function capture(method, uri, body)
local response = ngx.location.capture(uri, { local response = ngx.location.capture(uri, {
method = method, method = method,
body = json.encode(body) body = json.encode(body),
}) })
if response.truncated then return end if response.truncated then
return
end
if response.status ~= ngx.HTTP_OK then if response.status ~= ngx.HTTP_OK then
return nil, json.decode(response.body) return nil, json.decode(response.body)
end end
return json.decode(response.body) return json.decode(response.body)
end end
return { return {
get = function(...) get = function(...)
return capture(ngx.HTTP_GET, ...) return capture(ngx.HTTP_GET, ...)
end, end,
post = function(...) post = function(...)
return capture(ngx.HTTP_POST, ...) return capture(ngx.HTTP_POST, ...)
end, end,
put = function(...) put = function(...)
return capture(ngx.HTTP_PUT, ...) return capture(ngx.HTTP_PUT, ...)
end, end,
delete = function(...) delete = function(...)
return capture(ngx.HTTP_DELETE, ...) return capture(ngx.HTTP_DELETE, ...)
end, end,
} }

@ -1,64 +1,94 @@
local ngx = _G.ngx local ngx = _G.ngx
local get_error = {} local get_error = {}
local status = {} local status = {}
--[[ API Error Codes ]]-- --[[ API Error Codes ]] --
-- Authorization -- Authorization
-- email:api_key format in Authorization HTTP header is invalid -- email:api_key format in Authorization HTTP header is invalid
function get_error.malformed_authorization() function get_error.malformed_authorization()
return { code=100 } return {
code = 100,
}
end end
-- email:api_key in Authorization HTTP header does not match any user -- email:api_key in Authorization HTTP header does not match any user
-- login credentials do not match any user -- login credentials do not match any user
function get_error.invalid_authorization() function get_error.invalid_authorization()
return { code=101 } return {
code = 101,
}
end end
-- Attempting to access endpoint that requires higher priviliges -- Attempting to access endpoint that requires higher priviliges
function get_error.unauthorized_access() function get_error.unauthorized_access()
return { code=102 } return {
code = 102,
}
end end
-- Data Validation -- Data Validation
function get_error.field_not_found(field) function get_error.field_not_found(field)
return { code=200, field=field } return {
code = 200,
field = field,
}
end end
function get_error.field_invalid(field) function get_error.field_invalid(field)
return { code=201, field=field } return {
code = 201,
field = field,
}
end end
function get_error.field_not_unique(field) function get_error.field_not_unique(field)
return { code=202, field=field } return {
code = 202,
field = field,
}
end end
function get_error.token_expired(field) function get_error.token_expired(field)
return { code=203, field=field } return {
code = 203,
field = field,
}
end end
function get_error.password_not_match() function get_error.password_not_match()
return { code=204 } return {
code = 204,
}
end end
-- Database I/O -- Database I/O
function get_error.database_unresponsive() function get_error.database_unresponsive()
return { code=300 } return {
code = 300,
}
end end
function get_error.database_create() function get_error.database_create()
return { code=301 } return {
code = 301,
}
end end
function get_error.database_modify() function get_error.database_modify()
return { code=302 } return {
code = 302,
}
end end
function get_error.database_delete() function get_error.database_delete()
return { code=303 } return {
code = 303,
}
end end
function get_error.database_select() function get_error.database_select()
return { code=304 } return {
code = 304,
}
end end
--[[ API -> HTTP Code Map ]]-- --[[ API -> HTTP Code Map ]] --
-- Authorization -- Authorization
status[100] = ngx.HTTP_BAD_REQUEST status[100] = ngx.HTTP_BAD_REQUEST
@ -79,26 +109,26 @@ status[303] = ngx.HTTP_INTERNAL_SERVER_ERROR
status[304] = ngx.HTTP_INTERNAL_SERVER_ERROR status[304] = ngx.HTTP_INTERNAL_SERVER_ERROR
return { return {
get_error = get_error, get_error = get_error,
handle = function(self) handle = function(self)
-- Inject localized error messages -- Inject localized error messages
for _, err in ipairs(self.errors) do for _, err in ipairs(self.errors) do
--err.message = self.i18n(err.code) -- err.message = self.i18n(err.code)
if type(err) == "table" then if type(err) == "table" then
for k, v in pairs(err) do for k, v in pairs(err) do
print(k, ": ", v) print(k, ": ", v)
end end
else else
print(err) print(err)
end end
end end
print(#self.errors) print(#self.errors)
return self:write { return self:write{
status = 401,--status[self.errors[1].code], status = 401, -- status[self.errors[1].code],
json = self.errors json = self.errors,
} }
end end,
} }

@ -1,22 +1,22 @@
-- A whitelist of filetypes -- A whitelist of filetypes
return { return {
-- Image formats -- Image formats
image = { image = {
[".bmp"] = true, [".bmp"] = true,
[".png"] = true, [".png"] = true,
[".gif"] = true, [".gif"] = true,
[".jpg"] = true, [".jpg"] = true,
[".jpeg"] = true, [".jpeg"] = true,
[".webp"] = true, [".webp"] = true,
[".webm"] = true, [".webm"] = true,
[".svg"] = true [".svg"] = true,
}, },
-- Audio formats -- Audio formats
audio = { audio = {
[".wav"] = true, [".wav"] = true,
[".flac"] = true, [".flac"] = true,
[".mp3"] = true, [".mp3"] = true,
[".ogg"] = true [".ogg"] = true,
} },
} }

@ -1,29 +1,28 @@
local config = require("lapis.config").get()
local config = require("lapis.config").get()
local encoding = require "lapis.util.encoding" local encoding = require "lapis.util.encoding"
local sha256 = require "resty.sha256" local sha256 = require "resty.sha256"
local ffi = require "ffi" local ffi = require "ffi"
local posix = require "posix" local posix = require "posix"
local salt = loadfile("../data/secrets/salt.lua")() local salt = loadfile("../data/secrets/salt.lua")()
local token = config.secret local token = config.secret
local sf = string.format local sf = string.format
local ss = string.sub local ss = string.sub
local function get_chunks(str) local function get_chunks(str)
-- Secure trip -- Secure trip
local name, tripcode = str:match("(.-)(##.+)") local name, tripcode = str:match("(.-)(##.+)")
-- Insecure trip -- Insecure trip
if not name then if not name then
name, tripcode = str:match("(.-)(#.+)") name, tripcode = str:match("(.-)(#.+)")
-- Just a name -- Just a name
if not name then if not name then
return str:match("(.+)") return str:match("(.+)")
end end
end end
return name, tripcode return name, tripcode
end end
local generate = {} local generate = {}
@ -31,23 +30,23 @@ local generate = {}
-- math.random isn't reliable for this use case, so instead we're gonna snag -- math.random isn't reliable for this use case, so instead we're gonna snag
-- some bytes from /dev/urandom, create a uint32, and grab the last 3 digits. -- some bytes from /dev/urandom, create a uint32, and grab the last 3 digits.
function generate.random() function generate.random()
-- Read uint32_t from /dev/urandom -- Read uint32_t from /dev/urandom
local r = io.open("/dev/urandom", "rb") local r = io.open("/dev/urandom", "rb")
local bytes = r:read(4) local bytes = r:read(4)
r:close() r:close()
-- Build number -- Build number
local num = ffi.new("unsigned int[1]") local num = ffi.new("unsigned int[1]")
ffi.copy(num, bytes, 4) ffi.copy(num, bytes, 4)
return sf("%03d", num[0] % 1000) return sf("%03d", num[0] % 1000)
end end
-- Generate an insecure password -- Generate an insecure password
function generate.password(time) function generate.password(time)
local hasher = sha256:new() local hasher = sha256:new()
hasher:update(sf("%s%s", time, generate.random())) hasher:update(sf("%s%s", time, generate.random()))
return encoding.encode_base64(hasher:final()) return encoding.encode_base64(hasher:final())
end end
-- Generate a secure or insecure tripcode based off the name a user supplies -- Generate a secure or insecure tripcode based off the name a user supplies
@ -55,40 +54,40 @@ end
-- Secure tripcodes use sha256 + the app's secret token. -- Secure tripcodes use sha256 + the app's secret token.
-- Insecure tripcodes use standard posix crypt + the app's secret salt. -- Insecure tripcodes use standard posix crypt + the app's secret salt.
function generate.tripcode(raw_name) function generate.tripcode(raw_name)
local name, tripcode = get_chunks(raw_name) local name, tripcode = get_chunks(raw_name)
if tripcode then if tripcode then
local pattern = "^([^=]*)" local pattern = "^([^=]*)"
tripcode = tripcode:sub(2) -- remove leading '#' tripcode = tripcode:sub(2) -- remove leading '#'
-- Secure tripcode -- Secure tripcode
if tripcode:sub(1, 1) == "#" then if tripcode:sub(1, 1) == "#" then
local hasher = sha256:new() local hasher = sha256:new()
tripcode = token .. tripcode:sub(2) -- remove leading '#' tripcode = token .. tripcode:sub(2) -- remove leading '#'
hasher:update(tripcode) hasher:update(tripcode)
local hash = encoding.encode_base64(hasher:final()) local hash = encoding.encode_base64(hasher:final())
tripcode = "!!" .. ss(hash:match(pattern), -10) tripcode = "!!" .. ss(hash:match(pattern), -10)
-- Insecure tripcode -- Insecure tripcode
else else
local hash = posix.crypt(tripcode, salt) local hash = posix.crypt(tripcode, salt)
tripcode = "!" .. ss(hash, -10) tripcode = "!" .. ss(hash, -10)
end end
end end
return name, tripcode return name, tripcode
end end
function generate.errors(i18n, errors) function generate.errors(i18n, errors)
local err = {} local err = {}
if #errors > 0 then if #errors > 0 then
for _, error in ipairs(errors) do for _, error in ipairs(errors) do
local e = i18n(unpack(error)) local e = i18n(unpack(error))
table.insert(err, e) table.insert(err, e)
end end
end end
return err return err
end end
return generate return generate

@ -0,0 +1,42 @@
local cjson = require("cjson")
local xpcall = xpcall
-- 设置 处理稀疏数组
-- https://www.kyne.com.au/~mark/software/lua-cjson-manual.html#encode_sparse_array
cjson.encode_sparse_array(true, 1, 1)
-- https://github.com/cloudwu/lua-cjson/pull/8
cjson.encode_empty_table_as_array("on")
-- https://github.com/cloudwu/lua-cjson/pull/10
cjson.encode_number_precision(16)
local json = {}
function json.encode(var)
local function _logExce(err)
print("json encode", err)
end
local status, result = xpcall(cjson.encode, _logExce, var)
if status then
return result
end
end
function json.decode(text)
if not text then
return
end
local function _logExce(err)
print("json decode", text, err)
end
local status, result = xpcall(cjson.decode, _logExce, text)
if status then
return result
end
end
return json

@ -1,298 +0,0 @@
local Bans = require "models.bans"
local Threads = require "models.threads"
local Posts = require "models.posts"
local Reports = require "models.reports"
local process = {}
function process.create_thread(params, session, board)
-- Prepare data for entry
local _, err = Posts:prepare_post(params, session, board)
if err then
return false, err
end
-- Archive old threads
local max_threads = board.threads_per_page * board.pages
Threads:archive_threads(board.id, max_threads)
-- Delete old archived threads
local time = os.time()
local threads = Threads:get_archived_threads(board.id)
for _, t in ipairs(threads) do
if time - t.last_active > board.archive_time and not t.save then
local posts = Posts:get_thread_posts(t.id)
Threads:delete_thread("override", t, posts[1])
-- Delete all associated posts
for _, post in ipairs(posts) do
Posts:delete_post("override", board, post)
-- Delete associated report
local report = Reports:get_report(post.id)
if report then
Reports:delete_report(report)
end
end
end
end
-- Insert post data into database
local post, err = Posts:create_post(
params,
session,
board,
thread,
true
)
if err then
return false, err
end
return post
end
function process.create_post(params, session, board, thread)
local posts = Posts:count_posts(thread.id)
local files = Posts:count_files(thread.id)
-- Prepare data for entry
local _, err = Posts:prepare_post(
params, session, board, thread, files
)
if err then
return false, err
end
-- Insert post data into database
local post, err = Posts:create_post(
params,
session,
board,
thread,
false
)
if err then
return false, err
end
posts = posts + 1
-- Check for [auto]sage
if params.options ~= "sage" and
posts <= board.post_limit then
-- Update thread
thread.last_active = os.time()
thread:update("last_active")
end
return post
end
function process.delete_thread(params, session, board)
-- Validate post
local post = Posts:get_post(board.id, params.thread_id)
if not post then
return false, { "err_invalid_post", { params.thread_id } }
end
-- Validate thread
local thread = Threads:get_thread(post.thread_id)
if not thread then
return false, { "err_invalid_thread" }
end
local posts = Posts:get_posts_by_thread(thread.id)
-- Delete thread
local _, err = Threads:delete_thread(session, thread, posts[1])
if err then
return false, err
end
-- Delete all associated posts
for _, post in ipairs(posts) do
Posts:delete_post("override", board, post)
-- Delete associated report
local report = Reports:get_report(board.id, post.id)
if report then
Reports:delete_report(report)
end
end
return true
end
function process.delete_post(params, session, board)
-- Validate post
local post = Posts:get_post(board.id, params.post_id)
if not post then
return false, { "err_invalid_post", { params.post_id } }
end
-- Validate thread
local thread = Threads:get_thread(post.thread_id)
if not thread then
return false, { "err_invalid_thread" }
end
-- Delete post
local _, err = Posts:delete_post(session, board, post)
if err then
return false, err
end
-- Update thread
local posts = Posts:get_posts_by_thread(thread.id)
thread.last_active = posts[#posts].timestamp
thread:update("last_active")
return true
end
function process.report_post(params, board)
-- Validate post
local post = Posts:get_post(board.id, params.post_id)
if not post then
return false, { "err_invalid_post", { params.thread } }
end
local report = Reports:get_report(board.id, post.post_id)
-- If report exists, update it
if report then
report.num_reports = report.num_reports + 1 -- FIXME: race condition
local _, err = Reports:modify_report(report)
if err then
return false, err
end
-- If report is new, create it
else
local _, err = Reports:create_report {
board_id = board.id,
thread_id = post.thread_id,
post_id = post.post_id,
timestamp = os.time(),
num_reports = 1
}
if err then
return false, err
end
end
return post
end
-- Sticky thread
function process.sticky_thread(params, board)
-- Validate post
local post = Posts:get_post(board.id, params.post_id)
if not post then
return false, { "err_invalid_post", { params.post_id } }
end
-- Validate thread
local thread = Threads:get_thread(post.thread_id)
if not thread then
return false, { "err_invalid_thread" }
end
thread.sticky = not thread.sticky
thread:update("sticky")
return true
end
-- Lock thread
function process.lock_thread(params, board)
-- Validate post
local post = Posts:get_post(board.id, params.post_id)
if not post then
return false, { "err_invalid_post", { params.post_id } }
end
-- Validate thread
local thread = Threads:get_thread(post.thread_id)
if not thread then
return false, { "err_invalid_thread" }
end
thread.lock = not thread.lock
thread:update("lock")
return true
end
-- Save thread
function process.save_thread(params, board)
-- Validate post
local post = Posts:get_post(board.id, params.post_id)
if not post then
return false, { "err_invalid_post", { params.post_id } }
end
-- Validate thread
local thread = Threads:get_thread(post.thread_id)
if not thread then
return false, { "err_invalid_thread" }
end
thread.save = not thread.save
thread:update("save")
return true
end
-- Override thread
function process.override_thread(params, board)
-- Validate post
local post = Posts:get_post(board.id, params.post_id)
if not post then
return false, { "err_invalid_post", { params.post_id } }
end
-- Validate thread
local thread = Threads:get_thread(post.thread_id)
if not thread then
return false, { "err_invalid_thread" }
end
thread.size_override = not thread.size_override
thread:update("size_override")
return true
end
-- Ban user
function process.ban_user(params, board)
-- Validate post
local post = Posts:get_post(board.id, params.post_id)
if not post then
return false, { "err_invalid_post", { params.post_id } }
end
params.ip = post.ip
-- Convert board name to id if checkbox is set
if params.board_id then
params.board_id = board.id
end
-- Ban user
local _, err = Bans:create_ban(params)
if err then
return false, err
end
-- Flag post
if params.banned then
post.banned = true
post:update("banned")
end
return true
end
return process

@ -1,31 +1,31 @@
local models = require "models" local models = require "models"
local get_error = require "utils.error".get_error local get_error = require"utils.error".get_error
local Users = models.users local Users = models.users
local role = {} local role = {}
-- User must be the Owner -- User must be the Owner
function role.owner(user) function role.owner(user)
return user.role == Users.role.OWNER and true or nil, get_error.unauthorized_access() return user.role == Users.role.OWNER and true or nil, get_error.unauthorized_access()
end end
-- User must be an Admin or higher -- User must be an Admin or higher
function role.admin(user) function role.admin(user)
return user.role >= Users.role.ADMIN and true or nil, get_error.unauthorized_access() return user.role >= Users.role.ADMIN and true or nil, get_error.unauthorized_access()
end end
-- User must be a Mod or higher -- User must be a Mod or higher
function role.mod(user) function role.mod(user)
return user.role >= Users.role.MOD and true or nil, get_error.unauthorized_access() return user.role >= Users.role.MOD and true or nil, get_error.unauthorized_access()
end end
-- User must be a Janitor or higher -- User must be a Janitor or higher
function role.janitor(user) function role.janitor(user)
return user.role >= Users.role.JANITOR and true or nil, get_error.unauthorized_access() return user.role >= Users.role.JANITOR and true or nil, get_error.unauthorized_access()
end end
-- User must be signed in -- User must be signed in
function role.user(user) function role.user(user)
return user.role >= Users.role.USER and true or nil, get_error.unauthorized_access() return user.role >= Users.role.USER and true or nil, get_error.unauthorized_access()
end end
return role return role

@ -1,20 +1,20 @@
local Posts = require "models.posts" local Posts = require "models.posts"
local escape = require("lapis.html").escape local escape = require("lapis.html").escape
local sf = string.format local sf = string.format
local formatter = {} local formatter = {}
--- Sanitize text for HTML safety --- Sanitize text for HTML safety
-- @tparam string text Raw text -- @tparam string text Raw text
-- @treturn string formatted -- @treturn string formatted
function formatter.sanitize(text) function formatter.sanitize(text)
return escape(text) return escape(text)
end end
--- Format new lines to 'br' tags --- Format new lines to 'br' tags
-- @tparam string text Raw text -- @tparam string text Raw text
-- @treturn string formatted -- @treturn string formatted
function formatter.new_lines(text) function formatter.new_lines(text)
return text:gsub("\n", "<br />\n") return text:gsub("\n", "<br />\n")
end end
--- Format words that begin with '>>' --- Format words that begin with '>>'
@ -24,137 +24,150 @@ end
-- @tparam table post Post data -- @tparam table post Post data
-- @treturn string formatted -- @treturn string formatted
function formatter.quote(text, request, board, post) function formatter.quote(text, request, board, post)
local function get_url(board, post_id) local function get_url(board, post_id)
if tonumber(post_id) then if tonumber(post_id) then
local p = Posts:get(board.id, post_id) local p = Posts:get(board.id, post_id)
if not p then return false end if not p then
return false
local thread = p:get_thread() end
if not thread then return false end
local thread = p:get_thread()
local op = thread:get_op() if not thread then
return return false
request:url_for("web.boards.thread", { board=board.name, thread=op.post_id }), end
op
else local op = thread:get_op()
return request:url_for("web.boards.board", { board=board.name }) return request:url_for("web.boards.thread", {
end board = board.name,
end thread = op.post_id,
}), op
-- >>1234 ur a fag else
-- >>(%d+) return request:url_for("web.boards.board", {
local match_pattern = "&gt;&gt;(%d+)" board = board.name,
local sub_pattern = "&gt;&gt;%s" })
end
-- Get all the matches and store them in an ordered list end
local posts = {}
for post_id in text:gmatch(match_pattern) do -- >>1234 ur a fag
table.insert(posts, { board=board, id=post_id }) -- >>(%d+)
end local match_pattern = "&gt;&gt;(%d+)"
local sub_pattern = "&gt;&gt;%s"
-- Format each match
for i, p in ipairs(posts) do -- Get all the matches and store them in an ordered list
local text = sf(sub_pattern, p.id) local posts = {}
local url, op = get_url(p.board, p.id) for post_id in text:gmatch(match_pattern) do
if url then table.insert(posts, {
if op.thread_id == post.thread_id then board = board,
posts[i] = sf("<a href='%s#p%s' class='quote_link'>%s</a>", url, p.id, text) id = post_id,
else })
posts[i] = sf("<a href='%s#p%s' class='quote_link'>%s→</a>", url, p.id, text) end
end
else -- Format each match
posts[i] = sf("<span class='broken_link'>%s</span>", text) for i, p in ipairs(posts) do
end local text = sf(sub_pattern, p.id)
end local url, op = get_url(p.board, p.id)
if url then
-- Substitute each match with the formatted match if op.thread_id == post.thread_id then
local i = 0 posts[i] = sf("<a href='%s#p%s' class='quote_link'>%s</a>", url, p.id, text)
text = text:gsub(match_pattern, function() else
i = i + 1 posts[i] = sf("<a href='%s#p%s' class='quote_link'>%s→</a>", url, p.id, text)
return posts[i] end
end) else
posts[i] = sf("<span class='broken_link'>%s</span>", text)
-- >>>/a/1234 check over here end
-- >>>/(%w+)/(%d*) end
match_pattern = "&gt;&gt;&gt;/(%w+)/(%d*)"
sub_pattern = "&gt;&gt;&gt;/%s/%s" -- Substitute each match with the formatted match
local i = 0
-- Get all the matches and store them in an ordered list text = text:gsub(match_pattern, function()
posts = {} i = i + 1
for b, post_id in text:gmatch(match_pattern) do return posts[i]
local response = request.api.boards.GET(request) end)
b = response.json or b
table.insert(posts, { board=b, id=post_id }) -- >>>/a/1234 check over here
end -- >>>/(%w+)/(%d*)
match_pattern = "&gt;&gt;&gt;/(%w+)/(%d*)"
-- Format each match sub_pattern = "&gt;&gt;&gt;/%s/%s"
for i, p in ipairs(posts) do
if type(p.board) == "table" then -- Get all the matches and store them in an ordered list
local text = sf(sub_pattern, p.board.name, p.id) posts = {}
local url, op = get_url(p.board, p.id) for b, post_id in text:gmatch(match_pattern) do
if op then local response = request.api.boards.GET(request)
posts[i] = sf("<a href='%s#p%s' class='quote_link'>%s</a>", url, p.id, text) b = response.json or b
else table.insert(posts, {
posts[i] = sf("<a href='%s' class='quote_link'>%s</a>", url, text) board = b,
end id = post_id,
else })
local text = sf(sub_pattern, p.board, p.id) end
posts[i] = sf("<span class='broken_link'>%s</span>", text)
end -- Format each match
end for i, p in ipairs(posts) do
if type(p.board) == "table" then
-- Substitute each match with the formatted match local text = sf(sub_pattern, p.board.name, p.id)
i = 0 local url, op = get_url(p.board, p.id)
text = text:gsub(match_pattern, function() if op then
i = i + 1 posts[i] = sf("<a href='%s#p%s' class='quote_link'>%s</a>", url, p.id, text)
return posts[i] else
end) posts[i] = sf("<a href='%s' class='quote_link'>%s</a>", url, text)
end
return text else
local text = sf(sub_pattern, p.board, p.id)
posts[i] = sf("<span class='broken_link'>%s</span>", text)
end
end
-- Substitute each match with the formatted match
i = 0
text = text:gsub(match_pattern, function()
i = i + 1
return posts[i]
end)
return text
end end
--- Format lines that begin with '>' --- Format lines that begin with '>'
-- @tparam string text Raw text -- @tparam string text Raw text
-- @treturn string formatted -- @treturn string formatted
function formatter.green_text(text) function formatter.green_text(text)
local formatted = "" local formatted = ""
for line in text:gmatch("[^\n]+") do for line in text:gmatch("[^\n]+") do
local first = line:sub(1, 4) local first = line:sub(1, 4)
-- >implying -- >implying
if first == "&gt;" then if first == "&gt;" then
line = sf("%s%s%s", "<span class='quote_green'>", line, "</span>") line = sf("%s%s%s", "<span class='quote_green'>", line, "</span>")
end end
formatted = sf("%s%s%s", formatted, line, "\n") formatted = sf("%s%s%s", formatted, line, "\n")
end end
return formatted return formatted
end end
--- Format lines that begin with '<' --- Format lines that begin with '<'
-- @tparam string text Raw text -- @tparam string text Raw text
-- @treturn string formatted -- @treturn string formatted
function formatter.blue_text(text) function formatter.blue_text(text)
local formatted = "" local formatted = ""
for line in text:gmatch("[^\n]+") do for line in text:gmatch("[^\n]+") do
local first = line:sub(1, 4) local first = line:sub(1, 4)
-- <implying -- <implying
if first == "&lt;" then if first == "&lt;" then
line = sf("%s%s%s", "<span class='quote_blue'>", line, "</span>") line = sf("%s%s%s", "<span class='quote_blue'>", line, "</span>")
end end
formatted = sf("%s%s%s", formatted, line, "\n") formatted = sf("%s%s%s", formatted, line, "\n")
end end
return formatted return formatted
end end
function formatter.spoiler(text) function formatter.spoiler(text)
return text:gsub("(%[spoiler%])(.-)(%[/spoiler%])", "<span class='spoiler'>%2</span>") return text:gsub("(%[spoiler%])(.-)(%[/spoiler%])", "<span class='spoiler'>%2</span>")
end end
return formatter return formatter

@ -1,247 +0,0 @@
<h1><%= page_title %></h1>
<hr />
<!-- Report Junk -->
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="view_report" type="hidden" />
<div>
<label class="title"><%= i18n("view_report") %></label>
<span class="fields">
<select name="report" class="change_submit">
<option></option>
<% for _, report in ipairs(reports) do %>
<option value="<%= report.id %>"><%= report.id %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="delete_report" type="hidden" />
<div>
<label class="title"><%= i18n("delete_report") %></label>
<span class="fields">
<select name="report" class="change_delete">
<option></option>
<% for _, report in ipairs(reports) do %>
<option value="<%= report.id %>"><%= report.id %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<!-- User Junk -->
<div class="admin_form">
<form action="<%= url_for('web.admin.users', { action='create' }) %>" method="get">
<div>
<label class="title"><%= i18n("create_user") %></label>
<span class="fields">
<button class="admin_main"><%= i18n("create_user") %></button>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="modify_user" type="hidden" />
<div>
<label class="title"><%= i18n("modify_user") %></label>
<span class="fields">
<select name="user" class="change_submit">
<option></option>
<% for _, user in ipairs(users) do %>
<option value="<%= user.id %>"><%= user.username %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="delete_user" type="hidden" />
<div>
<label class="title"><%= i18n("delete_user") %></label>
<span class="fields">
<select name="user" class="change_delete">
<option></option>
<% for _, user in ipairs(users) do %>
<option value="<%= user.id %>"><%= user.username %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<!-- Board Junk -->
<div class="admin_form">
<form action="<%= url_for('web.admin.boards', { action='create' }) %>" method="get">
<div>
<label class="title"><%= i18n("create_board") %></label>
<span class="fields">
<button class="admin_main"><%= i18n("create_board") %></button>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="modify_board" type="hidden" />
<div>
<label class="title"><%= i18n("modify_board") %></label>
<span class="fields">
<select name="board" class="change_submit">
<option></option>
<% for _, board in ipairs(boards) do %>
<option value="<%= board.name %>"><%= string.format("/%s/ - %s", board.name, board.title) %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="delete_board" type="hidden" />
<div>
<label class="title"><%= i18n("delete_board") %></label>
<span class="fields">
<select name="board" class="change_delete">
<option></option>
<% for _, board in ipairs(boards) do %>
<option value="<%= board.name %>"><%= string.format("/%s/ - %s", board.name, board.title) %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<!-- Announcement Junk -->
<div class="admin_form">
<form action="<%= url_for('web.admin.announcements', { action='create' }) %>" method="get">
<div>
<label class="title"><%= i18n("create_ann") %></label>
<span class="fields">
<button class="admin_main"><%= i18n("create_ann") %></button>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="modify_announcement" type="hidden" />
<div>
<label class="title"><%= i18n("modify_ann") %></label>
<span class="fields">
<select name="ann" class="change_submit">
<option></option>
<% for _, ann in ipairs(announcements) do %>
<option value="<%= ann.id %>"><%= ann.text %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="delete_announcement" type="hidden" />
<div>
<label class="title"><%= i18n("delete_ann") %></label>
<span class="fields">
<select name="ann" class="change_delete">
<option></option>
<% for _, ann in ipairs(announcements) do %>
<option value="<%= ann.id %>"><%= ann.text %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<!-- Page Junk -->
<div class="admin_form">
<form action="<%= url_for('web.admin.pages', { action='create' }) %>" method="get">
<div>
<label class="title"><%= i18n("create_page") %></label>
<span class="fields">
<button class="admin_main"><%= i18n("create_page") %></button>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="modify_page" type="hidden" />
<div>
<label class="title"><%= i18n("modify_page") %></label>
<span class="fields">
<select name="page" class="change_submit">
<option></option>
<% for _, page in ipairs(pages) do %>
<option value="<%= page.slug %>"><%= page.slug %> - <%= page.title %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input name="delete_page" type="hidden" />
<div>
<label class="title"><%= i18n("delete_page") %></label>
<span class="fields">
<select name="page" class="change_delete">
<option></option>
<% for _, page in ipairs(pages) do %>
<option value="<%= page.slug %>"><%= page.slug %> - <%= page.title %></option>
<% end %>
</select>
</span>
</div>
</form>
</div>
<!-- Regen Thumbs -->
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<div>
<label class="title"><%= i18n("regen_thumb") %></label>
<span class="fields">
<button class="admin_main" name="regen_thumbs"><%= i18n("regen_thumb") %></button>
</span>
</div>
</form>
</div>
<hr />

@ -1,30 +0,0 @@
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<div>
<label class="title"><%= i18n("board") %></label>
<span class="fields">
<select name="board_id">
<option value="0" <%= announcement.board_id == 0 and 'selected' or '' %>><%= i18n("global") %></option>
<% for _, board in ipairs(boards) do %>
<option value="<%= board.id %>" <%= announcement.board_id == board.id and 'selected' or '' %>>/<%= board.name %>/ - <%= board.title %></option>
<% end %>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("announcement") %></label>
<span class="fields">
<input type="text" name="text" value="<%= announcement.text or '' %>" />
</span>
</div>
<% if params.action == "create" then %>
<button name="create_announcement"><%= i18n("create_ann") %></button>
<% elseif params.action == "modify" then %>
<button name="modify_announcement"><%= i18n("modify_ann") %></button>
<% end %>
</form>
</div>

@ -1,189 +0,0 @@
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<div>
<label class="title"><%= i18n("board_title") %></label>
<span class="fields">
<input type="text" name="title" placeholder="Random" value="<%= board.title or '' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("name") %></label>
<span class="fields">
<input type="text" name="name" placeholder="b" value="<%= board.name or '' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("subtext") %></label>
<span class="fields">
<input type="text" name="subtext" placeholder="Post at your own peril." value="<%= board.subtext or '' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("rules") %></label>
<span class="fields">
<textarea name="rules"><%= board.rules or '' %></textarea>
</span>
</div>
<div>
<label class="title"><%= i18n("default_name") %></label>
<span class="fields">
<input type="text" name="anon_name" placeholder="Anonymous" value="<%= board.anon_name or '' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("theme") %></label>
<span class="fields">
<select name="theme">
<% for _, theme in ipairs(themes) do %>
<option value="<%= theme %>" <%= board.theme == theme and 'selected' or '' %>><%= theme %></option>
<% end %>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("num_pages") %></label>
<span class="fields">
<input type="text" name="pages" placeholder="10" value="<%= board.pages or '10' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("num_threads") %></label>
<span class="fields">
<input type="text" name="threads_per_page" placeholder="10" value="<%= board.threads_per_page or '10' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("text_board") %></label>
<span class="fields">
<select name="text_only">
<option value="t" <%= board.text_only == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.text_only == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("filetype_image") %></label>
<span class="fields">
<select name="filetype_image">
<option value="t" <%= board.filetype_image == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.filetype_image == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("filetype_audio") %></label>
<span class="fields">
<select name="filetype_audio">
<option value="t" <%= board.filetype_audio == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.filetype_audio == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("draw_board") %></label>
<span class="fields">
<select name="draw">
<option value="t" <%= board.draw == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.draw == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("thread_file_required") %></label>
<span class="fields">
<select name="thread_file">
<option value="t" <%= board.thread_file == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.thread_file == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("thread_comment_required") %></label>
<span class="fields">
<select name="thread_comment">
<option value="t" <%= board.thread_comment == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.thread_comment == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("file_limit") %></label>
<span class="fields">
<input type="text" name="thread_file_limit" placeholder="100" value="<%= board.thread_file_limit or '100' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("post_file_required") %></label>
<span class="fields">
<select name="post_file">
<option value="t" <%= board.post_file == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.post_file == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("post_comment_required") %></label>
<span class="fields">
<select name="post_comment">
<option value="t" <%= board.post_comment == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.post_comment == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("bump_limit") %></label>
<span class="fields">
<input type="text" name="post_limit" placeholder="250" value="<%= board.post_limit or '250' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("archive_pruned") %></label>
<span class="fields">
<select name="archive">
<option value="t" <%= board.archive == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= board.archive == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("archive_days") %></label>
<span class="fields">
<input type="text" name="archive_time" placeholder="30" value="<%= board.archive_time or '30' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("board_group") %></label>
<span class="fields">
<input type="text" name="group" placeholder="1" value="<%= board.group or '1' %>" />
</span>
</div>
<% if params.action == "create" then %>
<button name="create_board"><%= i18n("create_board") %></button>
<% elseif params.action == "modify" then %>
<button name="modify_board"><%= i18n("modify_board") %></button>
<% end %>
</form>
</div>

@ -1,21 +0,0 @@
<div class="admin_form">
<form action="<%= url_for('web.admin.index') %>" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<div>
<label class="title"><%= i18n("username") %></label>
<span class="fields">
<input type="text" name="username" value="<%= params.username or '' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("password") %></label>
<span class="fields">
<input type="password" name="password" />
</span>
</div>
<button name="login"><%= i18n("login") %></button>
</form>
</div>

@ -1,33 +0,0 @@
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<input type="hidden" name="old" value="<%= page.old or page.slug or '' %>" />
<div>
<label class="title"><%= i18n("name") %></label>
<span class="fields">
<input type="text" name="title" placeholder="Frequently Asked Questions" value="<%= page.title or '' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("slug") %></label>
<span class="fields">
<input type="text" name="slug" placeholder="faq" value="<%= page.slug or '' %>" />
</span>
</div>
<div>
<label class="title"><%= i18n("content_md") %></label>
<span class="fields">
<textarea name="content"><%= page.content or '' %></textarea>
</span>
</div>
<% if params.action == "create" then %>
<button name="create_page"><%= i18n("create_page") %></button>
<% elseif params.action == "modify" then %>
<button name="modify_page"><%= i18n("modify_page") %></button>
<% end %>
</form>
</div>

@ -1,3 +0,0 @@
<h1>Success</h1>
<p><%= action %></p>
[<a href="<%= url_for('web.admin.index') %>"><%= i18n("return") %></a>]

@ -1,71 +0,0 @@
<div class="admin_form">
<form action="" method="post">
<input name="csrf_token" type="hidden" value="<%= csrf_token %>" />
<div>
<label class="title"><%= i18n("username") %></label>
<span class="fields">
<input type="text" name="username" placeholder="moot" value="<%= user.username or '' %>" />
</span>
</div>
<% if params.action == "modify" then %>
<div>
<label class="title"><%= i18n("password_old") %></label>
<span class="fields">
<input type="password" name="old_password" />
</span>
</div>
<% end %>
<div>
<label class="title"><%= i18n("password") %></label>
<span class="fields">
<input type="password" name="new_password" />
</span>
</div>
<div>
<label class="title"><%= i18n("password_retype") %></label>
<span class="fields">
<input type="password" name="retype_password" />
</span>
</div>
<div>
<label class="title"><%= i18n("administrator") %></label>
<span class="fields">
<select name="admin">
<option value="t" <%= user.admin == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= user.admin == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("moderator") %></label>
<span class="fields">
<select name="mod">
<option value="t" <%= user.mod == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= user.mod == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<div>
<label class="title"><%= i18n("janitor") %></label>
<span class="fields">
<select name="janitor">
<option value="t" <%= user.janitor == true and 'selected' or '' %>><%= i18n("yes") %></option>
<option value="f" <%= user.janitor == false and 'selected' or '' %>><%= i18n("no") %></option>
</select>
</span>
</div>
<% if params.action == "create" then %>
<button name="create_user"><%= i18n("create_user") %></button>
<% elseif params.action == "modify" then %>
<button name="modify_user"><%= i18n("modify_user") %></button>
<% end %>
</form>
</div>

@ -1,49 +0,0 @@
<% render('views.fragments.board_title') %>
<% render('views.fragments.announcements') %>
<hr />
[<a href="<%= url_for('web.boards.board', { uri_name=board.name }) %>"><%= i18n('return') %></a>]
[<a href="<%= url_for('web.boards.catalog', { uri_name=board.name }) %>"><%= i18n('catalog') %></a>]
[<a href="#bottom"><%= i18n('bottom') %></a>]
<hr />
<p class="archive_stats">
<strong>
<%= i18n('arc_display', {
n_thread = #threads,
p_thread = i18n('threads', { count=#threads }),
n_day = days,
p_day = i18n('days', { count=days })
}) %>
</strong>
</p>
<table id="archive_table">
<thead>
<tr>
<td><%= i18n('arc_number') %></td>
<td><%= i18n('arc_name') %></td>
<td><%= i18n('arc_excerpt') %></td>
<td><%= i18n('arc_replies') %></td>
<td></td>
</tr>
</thead>
<tbody>
<% for _, thread in ipairs(threads) do %>
<tr>
<td><%= thread.op.post_id %></td>
<td><strong><%= thread.op.name %><strong> <%= thread.op.trip %></td>
<td>
<% if thread.op.subject then %>
<strong><%= thread.op.subject %>:</strong>
<% end %>
<%- thread.op.comment %>
</td>
<td><%= thread.replies %></td>
<td>[<a href="<%= thread.url %>"><%= i18n('arc_view') %></a>]</td>
</tr>
<% end %>
</tbody>
</table>
<hr />
[<a href="<%= url_for('web.boards.board', { uri_name=board.name }) %>"><%= i18n('return') %></a>]
[<a href="<%= url_for('web.boards.catalog', { uri_name=board.name }) %>"><%= i18n('catalog') %></a>]
[<a href="#top"><%= i18n('top') %></a>]
<hr />

@ -1,8 +0,0 @@
<h1><%= page_title %></h1>
<div class="banned">
<p><%= i18n('ban_reason') %></p>
<p><strong><%= reason %></strong></p>
<p><%- i18n('ban_expire', { expire='<strong>'..expire..'</strong>' }) %></p>
<p><%- i18n('ban_ip', { ip='<strong>'..ip..'</strong>' }) %></p>
<div><img src="/static/banned.jpg" alt="Catfish." /></div>
</div>

@ -1,36 +0,0 @@
<% render('views.fragments.board_title') %>
<% render('views.fragments.form_submit') %>
<% render('views.fragments.announcements') %>
<hr />
[<a href="<%= url_for('web.boards.catalog', { uri_name=board.name }) %>"><%= i18n('catalog') %></a>]
[<a href="<%= url_for('web.boards.archive', { uri_name=board.name }) %>"><%= i18n('archive') %></a>]
[<a href="<%= url_for('web.boards.board', { uri_name=board.name }) %>"><%= i18n('refresh') %></a>]
<hr />
<% for _, thread in ipairs(threads) do %>
<div class="thread_container">
<%
local posts = thread.posts or {}
for i=#posts, 1, -1 do
local post = posts[i]
local op = posts[#posts]
if post.post_id == op.post_id then
render('views.fragments.op_content', { thread=thread, post=post, posts=posts, is_board=true })
else
render('views.fragments.post_content', { thread=thread, post=post, posts=posts, is_board=true, op=op })
end
end
%>
</div>
<hr />
<% end %>
<div class="pages">
<% for i=1, pages do %>
<% if i == params.page then %>
[<a href="<%= url_for('web.boards.board', { uri_name=board.name, page=i }) %>"><strong><%= i %></strong></a>]
<% else %>
[<a href="<%= url_for('web.boards.board', { uri_name=board.name, page=i }) %>"><%= i %></a>]
<% end %>
<% end %>
[<a href="<%= url_for('web.boards.catalog', { uri_name=board.name }) %>"><%= i18n('catalog') %></a>]
[<a href="<%= url_for('web.boards.archive', { uri_name=board.name }) %>"><%= i18n('archive') %></a>]
</div>

@ -1,34 +0,0 @@
<% render('views.fragments.board_title') %>
<% render('views.fragments.form_submit') %>
<% render('views.fragments.announcements') %>
<hr />
[<a href="<%= url_for('web.boards.board', { uri_name=board.name }) %>"><%= i18n('return') %></a>]
[<a href="<%= url_for('web.boards.archive', { uri_name=board.name }) %>"><%= i18n('archive') %></a>]
[<a href="#bottom"><%= i18n('bottom') %></a>]
[<a href="<%= url_for('web.boards.catalog', { uri_name=board.name }) %>"><%= i18n('refresh') %></a>]
<hr />
<% for t, thread in ipairs(threads) do %>
<div class="catalog_container">
<a href="<%= thread.url %>">
<% if thread.op.file_name then %>
<img src="<%= thread.op.thumb %>" alt="" /><br />
<% end %>
<span class="catalog_stats">
<%- i18n('cat_stats', {
replies = '<strong>'..thread.replies..'</strong>',
files = '<strong>'..thread.files..'</strong>'
}) %>
</span><br />
<% if thread.op.subject then %>
<strong><%= thread.op.subject %>:</strong>
<% end %>
<%- thread.op.comment %>
</a>
</div>
<% end %>
<hr />
[<a href="<%= url_for('web.boards.board', { uri_name=board.name }) %>"><%= i18n('return') %></a>]
[<a href="<%= url_for('web.boards.archive', { uri_name=board.name }) %>"><%= i18n('archive') %></a>]
[<a href="#top"><%= i18n('top') %></a>]
[<a href="<%= url_for('web.boards.catalog', { uri_name=board.name }) %>"><%= i18n('refresh') %></a>]
<hr />

@ -1,2 +0,0 @@
<h1><%= page_title %></h1>
<p><a href="<%= url_for('web.pages.index') %>"><%= i18n("return_index") %></a></p>

@ -1,6 +0,0 @@
<% if #announcements > 0 then %>
<hr />
<% for _, announcement in ipairs(announcements) do %>
<p class="announcement"><%= announcement.text %></p>
<% end %>
<% end %>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save