Compare commits
1 Commits
e7260090ac
...
with-docke
Author | SHA1 | Date | |
---|---|---|---|
ca23415288 |
@ -1,8 +0,0 @@
|
|||||||
logs/
|
|
||||||
nginx.conf.compiled
|
|
||||||
.vscode/
|
|
||||||
.local/
|
|
||||||
data/db/*
|
|
||||||
secrets
|
|
||||||
secrets/.touched*
|
|
||||||
sass
|
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,9 +1,10 @@
|
|||||||
logs/
|
logs/
|
||||||
nginx.conf.compiled
|
nginx.conf.compiled
|
||||||
|
db.*.sqlite
|
||||||
.vscode/
|
.vscode/
|
||||||
.local/
|
.local/
|
||||||
data/db/*
|
static/avatars/*
|
||||||
secrets/secrets.lua
|
!static/avatars/default.webp
|
||||||
secrets/.touched*
|
secrets.lua
|
||||||
data/static/avatars/*
|
|
||||||
!data/static/avatars/default.webp
|
.first_launch.*
|
||||||
|
16
Dockerfile
16
Dockerfile
@ -1,16 +0,0 @@
|
|||||||
# HOW TO:
|
|
||||||
#
|
|
||||||
# docker compose up
|
|
||||||
#
|
|
||||||
# it exposes the data/ and secrets/ volumes in app root
|
|
||||||
#
|
|
||||||
FROM openresty/openresty:alpine-fat
|
|
||||||
|
|
||||||
RUN apk add --no-cache git make gcc g++ musl-dev libffi-dev openssl-dev sqlite-dev libsodium libsodium-dev imagemagick-dev openssl
|
|
||||||
WORKDIR /app
|
|
||||||
COPY . .
|
|
||||||
RUN eval "$(luarocks --lua-version=5.1 path)"
|
|
||||||
RUN luarocks --lua-version=5.1 build --only-deps
|
|
||||||
EXPOSE 8080
|
|
||||||
RUN chmod +x /app/start.sh
|
|
||||||
ENTRYPOINT ["/app/start.sh", "production"]
|
|
39
README.md
39
README.md
@ -6,43 +6,34 @@ Released under [CNPLv7+](https://thufie.lain.haus/NPL.html).
|
|||||||
Please read the [full terms](./LICENSE.md) for proper wording.
|
Please read the [full terms](./LICENSE.md) for proper wording.
|
||||||
|
|
||||||
# installing & first time setup
|
# installing & first time setup
|
||||||
## docker
|
1. first, install OpenResty. instructions for linux can be found [here](https://openresty.org/en/linux-packages.html).
|
||||||
```bash
|
2. then, install LuaJIT and Lua 5.1 (usually called `lua5.1` in package managers)
|
||||||
$ docker compose up
|
3. then, install [LuaRocks](https://luarocks.org) (prefer your package manager instead of a local install recommended by the guide)
|
||||||
```
|
4. add luarocks search dirs to path:
|
||||||
|
|
||||||
- opens port 8080
|
|
||||||
- exposes `data/db` and `data/avatars` as volumes for data backup and persistence
|
|
||||||
- exposes `secrets/` as a volume so that the script won't try to perform first time setup again
|
|
||||||
|
|
||||||
## manual
|
|
||||||
1. install:
|
|
||||||
- OpenResty. instructions for linux can be found [here](https://openresty.org/en/linux-packages.html)
|
|
||||||
- LuaJIT and Lua 5.1 (usually called `lua5.1` in package managers)
|
|
||||||
- openssl (-dev)
|
|
||||||
- sqlite (-dev)
|
|
||||||
- libsodium (-dev)
|
|
||||||
- imagemagick (-dev)
|
|
||||||
- [LuaRocks](https://luarocks.org) (either through the guide's instructions or your package manager, whichever is newer)
|
|
||||||
2. add luarocks search dirs to path:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# in .bashrc (or other shell equivalent)
|
# in .bashrc (or other shell equivalent)
|
||||||
eval "$(luarocks --lua-version 5.1 path)"
|
eval "$(luarocks --lua-version 5.1 path)"
|
||||||
```
|
```
|
||||||
3. clone repo
|
5. clone repo
|
||||||
4. install the lua dependencies:
|
6. install the dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ luarocks --local --lua-version 5.1 build --only-deps
|
$ luarocks --local --lua-version 5.1 build --only-deps
|
||||||
```
|
```
|
||||||
5. run:
|
7. create a file named `secrets.lua` in the project directory.
|
||||||
|
use the `secrets.lua.example` file as reference, and generate a cryptographically secure random key, for example, with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ start.sh production # or 'development' or empty string
|
$ openssl rand -hex 32
|
||||||
|
```
|
||||||
|
8. run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ start.sh production
|
||||||
```
|
```
|
||||||
the script will perform some necessary first time setup (and create a hidden file in the folder to ensure it won't do so again). it will create an administrator account and print the credentials to the console; **this will only happen once**. make sure you save them somewhere. the administrator account is the only one that can promote other users to moderator.
|
the script will perform some necessary first time setup (and create a hidden file in the folder to ensure it won't do so again). it will create an administrator account and print the credentials to the console; **this will only happen once**. make sure you save them somewhere. the administrator account is the only one that can promote other users to moderator.
|
||||||
(note the `production` argument. if called with no arguments, `start.sh` will run in a development environment, which uses a separate database and shows more debug information.)
|
(note the `production` argument. if called with no arguments, `start.sh` will run in a development environment, which uses a separate database.)
|
||||||
|
|
||||||
this app is made with the assumption that it is being reverse-proxied. as such, you may want to change the port to something other than the default `8080`. you can do that in [`config.lua`]([./config.lua]).
|
this app is made with the assumption that it is being reverse-proxied. as such, you may want to change the port to something other than the default `8080`. you can do that in [`config.lua`]([./config.lua]).
|
||||||
|
|
||||||
|
4
app.lua
4
app.lua
@ -21,9 +21,6 @@ local function inject_methods(req)
|
|||||||
return util.ntob(v)
|
return util.ntob(v)
|
||||||
end
|
end
|
||||||
req.PermissionLevelString = constants.PermissionLevelString
|
req.PermissionLevelString = constants.PermissionLevelString
|
||||||
req.infobox_message = function (_, s)
|
|
||||||
return util.infobox_message(s)
|
|
||||||
end
|
|
||||||
|
|
||||||
util.pop_infobox(req)
|
util.pop_infobox(req)
|
||||||
end
|
end
|
||||||
@ -35,7 +32,6 @@ app:include("apps.users", {path = "/user"})
|
|||||||
app:include("apps.topics", {path = "/topics"})
|
app:include("apps.topics", {path = "/topics"})
|
||||||
app:include("apps.threads", {path = "/threads"})
|
app:include("apps.threads", {path = "/threads"})
|
||||||
app:include("apps.mod", {path = "/mod"})
|
app:include("apps.mod", {path = "/mod"})
|
||||||
app:include("apps.post", {path = "/post"})
|
|
||||||
|
|
||||||
app:get("/", function(self)
|
app:get("/", function(self)
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
return {redirect_to = self:url_for("all_topics")}
|
||||||
|
35
apps/mod.lua
35
apps/mod.lua
@ -1,46 +1,23 @@
|
|||||||
local app = require("lapis").Application()
|
local app = require("lapis").Application()
|
||||||
|
|
||||||
local db = require("lapis.db")
|
|
||||||
|
|
||||||
local util = require("util")
|
local util = require("util")
|
||||||
|
|
||||||
local models = require("models")
|
local models = require("models")
|
||||||
local Users = models.Users
|
local Users = models.Users
|
||||||
|
|
||||||
-- everything here requires a logged in moderator
|
app:get("user_list", "/list", function(self)
|
||||||
app:before_filter(function(self)
|
|
||||||
self.me = util.get_logged_in_user(self)
|
self.me = util.get_logged_in_user(self)
|
||||||
if not self.me then
|
if not self.me then
|
||||||
self:write{redirect_to = self:url_for("all_topics")}
|
return {redirect_to = self:url_for("all_topics")}
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if not self.me:is_mod() then
|
if not self.me:is_mod() then
|
||||||
self:write{redirect_to = self:url_for("all_topics")}
|
return {redirect_to = self:url_for("all_topics")}
|
||||||
return
|
|
||||||
end
|
end
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("user_list", "/list", function(self)
|
|
||||||
self.users = Users:select("")
|
self.users = Users:select("")
|
||||||
|
|
||||||
return {render = "mod.user-list"}
|
return {render = "mod.user-list"}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
app:get("sort_topics", "/sort-topics", function(self)
|
|
||||||
self.topics = db.query("SELECT * FROM topics ORDER BY sort_order ASC")
|
|
||||||
self.page_title = "sorting topics"
|
|
||||||
return {render = "mod.sort-topics"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("sort_topics", "/sort-topics", function(self)
|
|
||||||
local updates = self.params
|
|
||||||
db.query("BEGIN")
|
|
||||||
for topic_id, new_order in pairs(updates) do
|
|
||||||
db.update("topics", {sort_order = new_order}, {id = topic_id})
|
|
||||||
end
|
|
||||||
db.query("COMMIT")
|
|
||||||
return {redirect_to = self:url_for("sort_topics")}
|
|
||||||
end)
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
local app = require("lapis").Application()
|
|
||||||
|
|
||||||
local db = require("lapis.db")
|
|
||||||
local constants = require("constants")
|
|
||||||
|
|
||||||
local util = require("util")
|
|
||||||
|
|
||||||
local models = require("models")
|
|
||||||
local Posts = models.Posts
|
|
||||||
local Threads = models.Threads
|
|
||||||
|
|
||||||
|
|
||||||
app:get("single_post", "/:post_id", function(self)
|
|
||||||
local query = constants.FULL_POSTS_QUERY .. "WHERE posts.id = ?"
|
|
||||||
local p = db.query(query, self.params.post_id)
|
|
||||||
if p then
|
|
||||||
self.post = p[1]
|
|
||||||
self.thread = Threads:find({id = self.post.thread_id})
|
|
||||||
self.page_title = self.post.username .. "'s post in " .. self.thread.title
|
|
||||||
end
|
|
||||||
|
|
||||||
return {render = "post.single-post"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:get("edit_post", "/:post_id/edit", function(self)
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if not user then
|
|
||||||
return {redirect_to = self:url_for"all_topics"}
|
|
||||||
end
|
|
||||||
|
|
||||||
local editing_query = constants.FULL_POSTS_QUERY .. "WHERE posts.id = ?"
|
|
||||||
local p = db.query(editing_query, self.params.post_id)
|
|
||||||
if not p then
|
|
||||||
return {redirect_to = self:url_for"all_topics"}
|
|
||||||
end
|
|
||||||
if p[1].user_id ~= user.id then
|
|
||||||
return {redirect_to = self:url_for"all_topics"}
|
|
||||||
end
|
|
||||||
self.me = user
|
|
||||||
self.editing_post = p[1]
|
|
||||||
self.thread = Threads:find({id = self.editing_post.thread_id})
|
|
||||||
|
|
||||||
local thread_predicate = constants.FULL_POSTS_QUERY .. "WHERE posts.thread_id = ?\n"
|
|
||||||
|
|
||||||
local context_prev_query = thread_predicate .. "AND posts.created_at < ? ORDER BY posts.created_at DESC LIMIT 2"
|
|
||||||
local context_next_query = thread_predicate .. "AND posts.created_at > ? ORDER BY posts.created_at ASC LIMIT 2"
|
|
||||||
|
|
||||||
self.prev_context = db.query(context_prev_query, self.thread.id, self.editing_post.created_at)
|
|
||||||
self.next_context = db.query(context_next_query, self.thread.id, self.editing_post.created_at)
|
|
||||||
|
|
||||||
return {render = "post.edit-post"}
|
|
||||||
end)
|
|
||||||
|
|
||||||
app:post("edit_post", "/:post_id/edit", function(self)
|
|
||||||
local user = util.get_logged_in_user(self)
|
|
||||||
if not user then
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end
|
|
||||||
|
|
||||||
local post = Posts:find({id = self.params.post_id})
|
|
||||||
if not post then
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end
|
|
||||||
|
|
||||||
if post.user_id ~= user.id then
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end
|
|
||||||
|
|
||||||
util.update_post(post, self.params.new_content)
|
|
||||||
local thread = Threads:find({id = post.thread_id})
|
|
||||||
local link = self:url_for("thread", {slug = thread.slug}, {after = post.id}) .. "#post-" .. post.id
|
|
||||||
return {redirect_to = link}
|
|
||||||
end)
|
|
||||||
|
|
||||||
return app
|
|
@ -1,6 +1,5 @@
|
|||||||
local app = require("lapis").Application()
|
local app = require("lapis").Application()
|
||||||
local lapis_util = require("lapis.util")
|
local lapis_util = require("lapis.util")
|
||||||
local constants = require("constants")
|
|
||||||
|
|
||||||
local db = require("lapis.db")
|
local db = require("lapis.db")
|
||||||
local util = require("util")
|
local util = require("util")
|
||||||
@ -36,10 +35,7 @@ app:post("thread_create", "/create", function(self)
|
|||||||
end
|
end
|
||||||
local topic = Topics:find(self.params.topic_id)
|
local topic = Topics:find(self.params.topic_id)
|
||||||
if not topic then
|
if not topic then
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
return {redirect_to = self:url_for("topics")}
|
||||||
end
|
|
||||||
if util.is_topic_locked(topic) and not user:is_mod() then
|
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local title = lapis_util.trim(self.params.title)
|
local title = lapis_util.trim(self.params.title)
|
||||||
@ -58,7 +54,7 @@ app:post("thread_create", "/create", function(self)
|
|||||||
|
|
||||||
local post = util.create_post(thread.id, user.id, post_content)
|
local post = util.create_post(thread.id, user.id, post_content)
|
||||||
if not post then
|
if not post then
|
||||||
return {redirect_to = self:url_for("all_topics")}
|
return {redirect_to = self:url_for("topics")}
|
||||||
end
|
end
|
||||||
|
|
||||||
return {redirect_to = self:url_for("thread", {slug = slug})}
|
return {redirect_to = self:url_for("thread", {slug = slug})}
|
||||||
@ -90,9 +86,23 @@ app:get("thread", "/:slug", function(self)
|
|||||||
end
|
end
|
||||||
|
|
||||||
-- self.page = math.max(1, math.min(self.page, self.pages))
|
-- self.page = math.max(1, math.min(self.page, self.pages))
|
||||||
local query = (constants.FULL_POSTS_QUERY ..
|
local posts = db.query([[
|
||||||
"WHERE posts.thread_id = ? ORDER BY posts.created_at ASC LIMIT ? OFFSET ?")
|
SELECT
|
||||||
local posts = db.query(query, thread.id, POSTS_PER_PAGE, (self.page - 1) * POSTS_PER_PAGE)
|
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path
|
||||||
|
FROM
|
||||||
|
posts
|
||||||
|
JOIN
|
||||||
|
post_history ON posts.current_revision_id = post_history.id
|
||||||
|
JOIN
|
||||||
|
users ON posts.user_id = users.id
|
||||||
|
LEFT JOIN
|
||||||
|
avatars ON users.avatar_id = avatars.id
|
||||||
|
WHERE
|
||||||
|
posts.thread_id = ?
|
||||||
|
ORDER BY
|
||||||
|
posts.created_at ASC
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
]], thread.id, POSTS_PER_PAGE, (self.page - 1) * POSTS_PER_PAGE)
|
||||||
self.topic = Topics:find(thread.topic_id)
|
self.topic = Topics:find(thread.topic_id)
|
||||||
self.me = util.get_logged_in_user_or_transient(self)
|
self.me = util.get_logged_in_user_or_transient(self)
|
||||||
self.posts = posts
|
self.posts = posts
|
||||||
|
@ -5,7 +5,7 @@ local constants = require("constants")
|
|||||||
|
|
||||||
local util = require("util")
|
local util = require("util")
|
||||||
|
|
||||||
local auth = require("lib.auth")
|
local bcrypt = require("bcrypt")
|
||||||
local rand = require("openssl.rand")
|
local rand = require("openssl.rand")
|
||||||
|
|
||||||
local models = require("models")
|
local models = require("models")
|
||||||
@ -14,7 +14,7 @@ local Sessions = models.Sessions
|
|||||||
local Avatars = models.Avatars
|
local Avatars = models.Avatars
|
||||||
|
|
||||||
local function authenticate_user(user, password)
|
local function authenticate_user(user, password)
|
||||||
return auth.verify(password, user.password_hash)
|
return bcrypt.verify(password, user.password_hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function create_session_key()
|
local function create_session_key()
|
||||||
@ -321,7 +321,7 @@ app:post("user_signup", "/signup", function(self)
|
|||||||
|
|
||||||
local new_user = Users:create({
|
local new_user = Users:create({
|
||||||
username = username,
|
username = username,
|
||||||
password_hash = auth.digest(password),
|
password_hash = bcrypt.digest(password, constants.BCRYPT_ROUNDS),
|
||||||
permission = constants.PermissionLevel.GUEST,
|
permission = constants.PermissionLevel.GUEST,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
local config = require("lapis.config")
|
local config = require("lapis.config")
|
||||||
local secrets = require("secrets.secrets")
|
local secrets = require("secrets")
|
||||||
|
|
||||||
config({"development", "production"}, {
|
config({"development", "production"}, {
|
||||||
port = 8080,
|
port = 8080,
|
||||||
@ -7,7 +7,7 @@ config({"development", "production"}, {
|
|||||||
code_cache = "off",
|
code_cache = "off",
|
||||||
num_workers = "1",
|
num_workers = "1",
|
||||||
sqlite = {
|
sqlite = {
|
||||||
database = "data/db/db.dev.sqlite"
|
database = "db.dev.sqlite"
|
||||||
},
|
},
|
||||||
secret = "SUPER SECRET",
|
secret = "SUPER SECRET",
|
||||||
session_name = "porom_session",
|
session_name = "porom_session",
|
||||||
@ -20,7 +20,7 @@ config("production", {
|
|||||||
},
|
},
|
||||||
secret = secrets.key,
|
secret = secrets.key,
|
||||||
sqlite = {
|
sqlite = {
|
||||||
database = "data/db/db.prod.sqlite"
|
database = "db.prod.sqlite"
|
||||||
},
|
},
|
||||||
session_name = "porom_session_s"
|
session_name = "porom_session_s"
|
||||||
})
|
})
|
||||||
|
@ -8,19 +8,6 @@ Constants.PermissionLevel = {
|
|||||||
ADMIN = 4,
|
ADMIN = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
Constants.FULL_POSTS_QUERY = [[
|
|
||||||
SELECT
|
|
||||||
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup
|
|
||||||
FROM
|
|
||||||
posts
|
|
||||||
JOIN
|
|
||||||
post_history ON posts.current_revision_id = post_history.id
|
|
||||||
JOIN
|
|
||||||
users ON posts.user_id = users.id
|
|
||||||
LEFT JOIN
|
|
||||||
avatars ON users.avatar_id = avatars.id
|
|
||||||
]]
|
|
||||||
|
|
||||||
Constants.PermissionLevelString = {
|
Constants.PermissionLevelString = {
|
||||||
[Constants.PermissionLevel.GUEST] = "Guest",
|
[Constants.PermissionLevel.GUEST] = "Guest",
|
||||||
[Constants.PermissionLevel.USER] = "User",
|
[Constants.PermissionLevel.USER] = "User",
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
local auth = require("lib.auth")
|
local bcrypt = require("bcrypt")
|
||||||
local models = require("models")
|
local models = require("models")
|
||||||
local constants = require("constants")
|
local constants = require("constants")
|
||||||
|
|
||||||
@ -23,14 +23,13 @@ local function create_admin()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
math.randomseed(os.time())
|
|
||||||
local password = ""
|
local password = ""
|
||||||
for _ = 1, 16 do
|
for _ = 1, 16 do
|
||||||
local randi = math.random(#alphabet)
|
local randi = math.random(#alphabet)
|
||||||
password = password .. alphabet:sub(randi, randi)
|
password = password .. alphabet:sub(randi, randi)
|
||||||
end
|
end
|
||||||
|
|
||||||
local hash = auth.digest(password)
|
local hash = bcrypt.digest(password, constants.BCRYPT_ROUNDS)
|
||||||
|
|
||||||
models.Users:create({
|
models.Users:create({
|
||||||
username = username,
|
username = username,
|
||||||
|
13
docker-compose.yaml
Normal file
13
docker-compose.yaml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Generate a random secret key
|
||||||
|
# export PROD_SECRET_KEY=$(openssl rand -hex 32)
|
||||||
|
# Start the container
|
||||||
|
# docker-compose up
|
||||||
|
version: "3"
|
||||||
|
services:
|
||||||
|
porom:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- PROD_SECRET_KEY=${PROD_SECRET_KEY}
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
@ -1,10 +0,0 @@
|
|||||||
services:
|
|
||||||
porom:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- ./data/static:/app/data/static
|
|
||||||
- ./data/db:/app/data/db
|
|
||||||
- ./secrets:/app/secrets
|
|
36
dockerfile
Normal file
36
dockerfile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# HOW TO:
|
||||||
|
#
|
||||||
|
# Generate a random secret key & build the Docker image
|
||||||
|
# ```sh
|
||||||
|
# SECRET_KEY=$(openssl rand -hex 32) docker build --build-arg PROD_SECRET_KEY="$SECRET_KEY" -t porom:latest .
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# Then run the container
|
||||||
|
# ```sh
|
||||||
|
# docker run -d -p 8080:8080 --name porom porom:latest
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
FROM openresty/openresty:alpine-fat
|
||||||
|
COPY ./nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
|
||||||
|
COPY . /usr/local/openresty/nginx/html
|
||||||
|
WORKDIR /usr/local/openresty/nginx/html
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
make \
|
||||||
|
git \
|
||||||
|
make \
|
||||||
|
gcc \
|
||||||
|
g++ \
|
||||||
|
musl-dev \
|
||||||
|
libffi-dev \
|
||||||
|
openssl-dev \
|
||||||
|
sqlite-dev \
|
||||||
|
imagemagick-dev \
|
||||||
|
lua5.1 \
|
||||||
|
lua5.1-dev
|
||||||
|
RUN eval "$(luarocks --lua-version 5.1 path)"
|
||||||
|
RUN luarocks --lua-version 5.1 build --only-deps
|
||||||
|
ARG PROD_SECRET_KEY
|
||||||
|
RUN echo "return { key = \"${PROD_SECRET_KEY}\",}" > /usr/local/openresty/nginx/html/secrets.lua
|
||||||
|
EXPOSE 8080
|
||||||
|
RUN chmod +x /usr/local/openresty/nginx/html/start.sh
|
||||||
|
ENTRYPOINT ["/usr/local/openresty/nginx/html/start.sh", "production"]
|
@ -1,45 +0,0 @@
|
|||||||
// https://codepen.io/crouchingtigerhiddenadam/pen/qKXgap
|
|
||||||
let selected = null;
|
|
||||||
let container = document.getElementById("topics-container")
|
|
||||||
|
|
||||||
function isBefore(el1, el2) {
|
|
||||||
let cur
|
|
||||||
if (el2.parentNode === el1.parentNode) {
|
|
||||||
for (cur = el1.previousSibling; cur; cur = cur.previousSibling) {
|
|
||||||
if (cur === el2) return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragOver(e) {
|
|
||||||
let target = e.target.closest(".draggable-topic")
|
|
||||||
|
|
||||||
if (!target || target === selected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBefore(selected, target)) {
|
|
||||||
container.insertBefore(selected, target)
|
|
||||||
} else {
|
|
||||||
container.insertBefore(selected, target.nextSibling)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragEnd() {
|
|
||||||
if (!selected) return;
|
|
||||||
|
|
||||||
selected.classList.remove("dragged")
|
|
||||||
selected = null;
|
|
||||||
for (let i = 0; i < container.childElementCount - 1; i++) {
|
|
||||||
let input = container.children[i].querySelector(".topic-input");
|
|
||||||
input.value = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function dragStart(e) {
|
|
||||||
e.dataTransfer.effectAllowed = 'move'
|
|
||||||
e.dataTransfer.setData('text/plain', null)
|
|
||||||
selected = e.target
|
|
||||||
selected.classList.add("dragged")
|
|
||||||
}
|
|
16
lib/auth.lua
16
lib/auth.lua
@ -1,16 +0,0 @@
|
|||||||
local auth = {}
|
|
||||||
|
|
||||||
local ls = require "luasodium"
|
|
||||||
|
|
||||||
function auth.digest(password)
|
|
||||||
return ls.crypto_pwhash_str(
|
|
||||||
password,
|
|
||||||
ls.crypto_pwhash_OPSLIMIT_INTERACTIVE,
|
|
||||||
ls.crypto_pwhash_MEMLIMIT_INTERACTIVE)
|
|
||||||
end
|
|
||||||
|
|
||||||
function auth.verify(password, hash)
|
|
||||||
return ls.crypto_pwhash_str_verify(hash, password)
|
|
||||||
end
|
|
||||||
|
|
||||||
return auth
|
|
@ -62,10 +62,5 @@ return {
|
|||||||
[8] = function ()
|
[8] = function ()
|
||||||
schema.add_column("topics", "sort_order", types.integer{default = 0})
|
schema.add_column("topics", "sort_order", types.integer{default = 0})
|
||||||
db.query("UPDATE topics SET sort_order = (SELECT COUNT(*) FROM topics t2 WHERE t2.ROWID <= topics.ROWID)")
|
db.query("UPDATE topics SET sort_order = (SELECT COUNT(*) FROM topics t2 WHERE t2.ROWID <= topics.ROWID)")
|
||||||
end,
|
end
|
||||||
|
|
||||||
[9] = function ()
|
|
||||||
schema.add_column("post_history", "original_markup", types.text{null = false})
|
|
||||||
schema.add_column("post_history", "markup_language", types.text{default = "babycode"})
|
|
||||||
end,
|
|
||||||
}
|
}
|
||||||
|
10
nginx.conf
10
nginx.conf
@ -26,20 +26,16 @@ http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias data/static/;
|
alias static/;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /favicon.ico {
|
location /favicon.ico {
|
||||||
alias data/static/favicon.ico;
|
alias static/favicon.ico;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /avatars {
|
location /avatars {
|
||||||
alias data/static/avatars;
|
alias static/avatars;
|
||||||
expires 1y;
|
expires 1y;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /static/js/ {
|
|
||||||
alias js/;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ dependencies = {
|
|||||||
"lapis == 1.16.0",
|
"lapis == 1.16.0",
|
||||||
"lsqlite3",
|
"lsqlite3",
|
||||||
"magick",
|
"magick",
|
||||||
"luasodium",
|
"bcrypt",
|
||||||
"luaossl",
|
"luaossl",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -366,39 +366,3 @@ input[type="text"], input[type="password"], textarea, select {
|
|||||||
grid-area: topic-locked-container;
|
grid-area: topic-locked-container;
|
||||||
border: 2px outset $light;
|
border: 2px outset $light;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.draggable-topic {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: $accent_color;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 12px 0;
|
|
||||||
border-top: 6px outset $light;
|
|
||||||
border-bottom: 6px outset $dark2;
|
|
||||||
|
|
||||||
&.dragged {
|
|
||||||
background-color: $button_color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing {
|
|
||||||
background-color: $light;
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-explain {
|
|
||||||
margin: 20px 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-edit-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: baseline;
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
&>textarea{
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
17
start.sh
17
start.sh
@ -1,24 +1,15 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
lapis migrate
|
|
||||||
lapis serve
|
lapis serve
|
||||||
}
|
}
|
||||||
|
|
||||||
first_launch() {
|
first_launch() {
|
||||||
echo "Setting up for the first time"
|
echo "Setting up for the first time"
|
||||||
mkdir -p secrets
|
touch ".first_launch.$LAPIS_ENVIRONMENT"
|
||||||
local SECRET
|
lua5.1 schema.lua
|
||||||
SECRET="$(openssl rand -hex 32)"
|
|
||||||
echo "return { key = \"${SECRET}\",}" > secrets/secrets.lua
|
|
||||||
touch "secrets/.touched.$LAPIS_ENVIRONMENT"
|
|
||||||
mkdir -p data/db
|
|
||||||
luajit schema.lua
|
|
||||||
chmod -R a+rw data
|
|
||||||
lapis migrate
|
lapis migrate
|
||||||
luajit create_default_accounts.lua
|
lua5.1 create_default_accounts.lua
|
||||||
}
|
}
|
||||||
|
|
||||||
if [[ $# -ne 1 ]]; then
|
if [[ $# -ne 1 ]]; then
|
||||||
@ -30,7 +21,7 @@ fi
|
|||||||
|
|
||||||
echo "Starting in $LAPIS_ENVIRONMENT"
|
echo "Starting in $LAPIS_ENVIRONMENT"
|
||||||
|
|
||||||
if ! [ -f "secrets/.touched.$LAPIS_ENVIRONMENT" ]; then
|
if ! [ -f ".first_launch.$LAPIS_ENVIRONMENT" ]; then
|
||||||
first_launch
|
first_launch
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 8.3 KiB After Width: | Height: | Size: 8.3 KiB |
@ -364,36 +364,3 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus, select:focus
|
|||||||
grid-area: topic-locked-container;
|
grid-area: topic-locked-container;
|
||||||
border: 2px outset rgb(217.26, 220.38, 213.42);
|
border: 2px outset rgb(217.26, 220.38, 213.42);
|
||||||
}
|
}
|
||||||
|
|
||||||
.draggable-topic {
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
background-color: #c1ceb1;
|
|
||||||
padding: 20px;
|
|
||||||
margin: 12px 0;
|
|
||||||
border-top: 6px outset rgb(217.26, 220.38, 213.42);
|
|
||||||
border-bottom: 6px outset rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
|
||||||
}
|
|
||||||
.draggable-topic.dragged {
|
|
||||||
background-color: rgb(177, 206, 204.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.editing {
|
|
||||||
background-color: rgb(217.26, 220.38, 213.42);
|
|
||||||
}
|
|
||||||
|
|
||||||
.context-explain {
|
|
||||||
margin: 20px 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-edit-form {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: baseline;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
.post-edit-form > textarea {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
197
util.lua
197
util.lua
@ -3,7 +3,6 @@ local magick = require("magick")
|
|||||||
local db = require("lapis.db")
|
local db = require("lapis.db")
|
||||||
local html_escape = require("lapis.html").escape
|
local html_escape = require("lapis.html").escape
|
||||||
local constants = require("constants")
|
local constants = require("constants")
|
||||||
local string_trim = require("lapis.util").trim
|
|
||||||
|
|
||||||
local Avatars = require("models").Avatars
|
local Avatars = require("models").Avatars
|
||||||
local Users = require("models").Users
|
local Users = require("models").Users
|
||||||
@ -34,131 +33,10 @@ util.TransientUser = {
|
|||||||
username = "Deleted User",
|
username = "Deleted User",
|
||||||
}
|
}
|
||||||
|
|
||||||
-- PURE API
|
|
||||||
|
|
||||||
function util.get_user_avatar_url(req, user)
|
function util.get_user_avatar_url(req, user)
|
||||||
return Avatars:find(user.avatar_id).file_path
|
return Avatars:find(user.avatar_id).file_path
|
||||||
end
|
end
|
||||||
|
|
||||||
---split a string
|
|
||||||
---@param s string subject
|
|
||||||
---@param delimiter string? string to split by, can be empty to split by character
|
|
||||||
---@param max_matches integer? the maximum number of returned elements
|
|
||||||
---@param trim boolean? whether to trim whitespace off matches
|
|
||||||
---@param allow_empty boolean? should empty matches be in the resulting table
|
|
||||||
---@return string[]
|
|
||||||
function util.s_split(s, delimiter, max_matches, trim, allow_empty)
|
|
||||||
local result = {}
|
|
||||||
if s == "" then
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
trim = trim == nil and true or trim
|
|
||||||
local tr = function(subj)
|
|
||||||
if trim then return string_trim(subj) else return subj end
|
|
||||||
end
|
|
||||||
max_matches = max_matches or -1
|
|
||||||
allow_empty = allow_empty == nil and true or allow_empty
|
|
||||||
|
|
||||||
if delimiter == "" then
|
|
||||||
for i=1, #s do
|
|
||||||
local c = s:sub(i, 1)
|
|
||||||
if allow_empty or c ~= "" then
|
|
||||||
table.insert(result, c)
|
|
||||||
if max_matches > 0 and #result == max_matches then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
local current_pos = 1
|
|
||||||
local delim_len = #delimiter
|
|
||||||
|
|
||||||
while true do
|
|
||||||
if max_matches > 0 and #result >= max_matches then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
---@diagnostic disable-next-line: param-type-mismatch
|
|
||||||
local start_pos, end_pos = s:find(delimiter, current_pos, true)
|
|
||||||
if not start_pos then
|
|
||||||
break
|
|
||||||
end
|
|
||||||
local substr = s:sub(current_pos, start_pos - 1)
|
|
||||||
if allow_empty or substr ~= "" then
|
|
||||||
table.insert(result, tr(substr))
|
|
||||||
end
|
|
||||||
current_pos = end_pos + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
local substr = s:sub(current_pos)
|
|
||||||
if allow_empty or substr ~= "" then
|
|
||||||
table.insert(result, tr(substr))
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.split_sentences(sentences, max_sentences)
|
|
||||||
return util.s_split(sentences, ".", max_sentences or 2, true, false)
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.infobox_message(msg)
|
|
||||||
local sentences = util.split_sentences(msg)
|
|
||||||
if #sentences == 1 then
|
|
||||||
return "<b>" .. sentences[1] .. ". " .. "</b>"
|
|
||||||
end
|
|
||||||
return "<span><b>" .. sentences[1] .. ". " .. "</b> " .. sentences[2] .. ".</span>"
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.get_logged_in_user(req)
|
|
||||||
if req.session.session_key == nil then
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
local session = db.select('* FROM "sessions" WHERE "key" = ? AND "expires_at" > "?" LIMIT 1', req.session.session_key, os.time())
|
|
||||||
if #session > 0 then
|
|
||||||
return Users:find({id = session[1].user_id})
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.get_logged_in_user_or_transient(req)
|
|
||||||
return util.get_logged_in_user(req) or util.TransientUser
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.ntob(v)
|
|
||||||
return v ~= 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.bton(b)
|
|
||||||
return 1 and b or 0
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.stob(s)
|
|
||||||
if s == "true" then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
if s == "false" then
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.form_bool_to_sqlite(s)
|
|
||||||
return util.bton(util.stob(s))
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.is_thread_locked(thread)
|
|
||||||
return util.ntob(thread.is_locked)
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.is_topic_locked(topic)
|
|
||||||
return util.ntob(topic.is_locked)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- OTHER API
|
|
||||||
|
|
||||||
function util.validate_and_create_image(input_image, filename)
|
function util.validate_and_create_image(input_image, filename)
|
||||||
local img = magick.load_image_from_blob(input_image)
|
local img = magick.load_image_from_blob(input_image)
|
||||||
|
|
||||||
@ -214,8 +92,49 @@ function util.destroy_avatar(avatar_id)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function util.create_post(thread_id, user_id, content, markup_language)
|
function util.get_logged_in_user(req)
|
||||||
markup_language = markup_language or "babycode"
|
if req.session.session_key == nil then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local session = db.select('* FROM "sessions" WHERE "key" = ? AND "expires_at" > "?" LIMIT 1', req.session.session_key, os.time())
|
||||||
|
if #session > 0 then
|
||||||
|
return Users:find({id = session[1].user_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function util.get_logged_in_user_or_transient(req)
|
||||||
|
return util.get_logged_in_user(req) or util.TransientUser
|
||||||
|
end
|
||||||
|
|
||||||
|
function util.ntob(v)
|
||||||
|
return v ~= 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function util.bton(b)
|
||||||
|
return 1 and b or 0
|
||||||
|
end
|
||||||
|
|
||||||
|
function util.stob(s)
|
||||||
|
if s == "true" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if s == "false" then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function util.form_bool_to_sqlite(s)
|
||||||
|
return util.bton(util.stob(s))
|
||||||
|
end
|
||||||
|
|
||||||
|
function util.is_thread_locked(thread)
|
||||||
|
return util.ntob(thread.is_locked)
|
||||||
|
end
|
||||||
|
|
||||||
|
function util.create_post(thread_id, user_id, content)
|
||||||
db.query("BEGIN")
|
db.query("BEGIN")
|
||||||
local post = Posts:create({
|
local post = Posts:create({
|
||||||
thread_id = thread_id,
|
thread_id = thread_id,
|
||||||
@ -223,17 +142,12 @@ function util.create_post(thread_id, user_id, content, markup_language)
|
|||||||
current_revision_id = db.NULL,
|
current_revision_id = db.NULL,
|
||||||
})
|
})
|
||||||
|
|
||||||
local parsed_content = ""
|
local bb_content = babycode.to_html(content, html_escape)
|
||||||
if markup_language == "babycode" then
|
|
||||||
parsed_content = babycode.to_html(content, html_escape)
|
|
||||||
end
|
|
||||||
|
|
||||||
local revision = PostHistory:create({
|
local revision = PostHistory:create({
|
||||||
post_id = post.id,
|
post_id = post.id,
|
||||||
content = parsed_content,
|
content = bb_content,
|
||||||
is_initial_revision = true,
|
is_initial_revision = true,
|
||||||
original_markup = content,
|
|
||||||
markup_language = "babycode",
|
|
||||||
})
|
})
|
||||||
|
|
||||||
post:update({current_revision_id = revision.id})
|
post:update({current_revision_id = revision.id})
|
||||||
@ -242,27 +156,6 @@ function util.create_post(thread_id, user_id, content, markup_language)
|
|||||||
return post
|
return post
|
||||||
end
|
end
|
||||||
|
|
||||||
function util.update_post(post, new_content, markup_language)
|
|
||||||
markup_language = markup_language or "babycode"
|
|
||||||
db.query("BEGIN")
|
|
||||||
|
|
||||||
local parsed_content = ""
|
|
||||||
if markup_language == "babycode" then
|
|
||||||
parsed_content = babycode.to_html(new_content, html_escape)
|
|
||||||
end
|
|
||||||
|
|
||||||
local revision = PostHistory:create({
|
|
||||||
post_id = post.id,
|
|
||||||
content = parsed_content,
|
|
||||||
is_initial_revision = false,
|
|
||||||
original_markup = new_content,
|
|
||||||
markup_language = markup_language
|
|
||||||
})
|
|
||||||
|
|
||||||
post:update({current_revision_id = revision.id})
|
|
||||||
db.query("COMMIT")
|
|
||||||
end
|
|
||||||
|
|
||||||
function util.transfer_and_delete_user(user)
|
function util.transfer_and_delete_user(user)
|
||||||
local deleted_user = Users:find({
|
local deleted_user = Users:find({
|
||||||
username = "DeletedUser",
|
username = "DeletedUser",
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<title>Porom</title>
|
<title>Porom</title>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% math.randomseed(os.time()) %>
|
<% math.randomseed(os.time()) %>
|
||||||
<link rel="stylesheet" href="<%= "/static/style.css?v=" .. math.random(1, 100) %>">
|
<link rel="stylesheet" href="<%= "/static/style.css?" .. math.random(1, 100) %>">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<% render("views.common.topnav") -%>
|
<% render("views.common.topnav") -%>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<%
|
<%
|
||||||
local class = "infobox " .. constants.InfoboxHTMLClass[kind]
|
local class = "infobox " .. constants.InfoboxHTMLClass[kind]
|
||||||
local icon = constants.InfoboxIcons[kind]
|
local icon = constants.InfoboxIcons[kind]
|
||||||
local sentences = infobox_message(msg)
|
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<div class="<%= class %>">
|
<div class="<%= class %>">
|
||||||
@ -9,6 +8,6 @@
|
|||||||
<div class="infobox-icon-container">
|
<div class="infobox-icon-container">
|
||||||
<% render(icon) %>
|
<% render(icon) %>
|
||||||
</div>
|
</div>
|
||||||
<%- sentences %>
|
<%= msg %>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
<div class="darkbg settings-container">
|
|
||||||
<% if infobox then %>
|
|
||||||
<% render("views.common.infobox", infobox) %>
|
|
||||||
<% end %>
|
|
||||||
<h1>Change topics order</h1>
|
|
||||||
<p>Drag topic titles to reoder them. Press submit when done. The topics will appear to users in the order set here.</p>
|
|
||||||
<form method="post" id=topics-container>
|
|
||||||
<% for _, topic in ipairs(topics) do %>
|
|
||||||
<div draggable="true" class="draggable-topic" ondragover="dragOver(event)" ondragstart="dragStart(event)" ondragend="dragEnd()">
|
|
||||||
<div class="thread-title"><%= topic.name %></div>
|
|
||||||
<div><%= topic.description %></div>
|
|
||||||
<input type="hidden" name="<%= topic.id %>" value="<%= topic.sort_order %>" class="topic-input">
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<input type=submit value="Save order">
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/js/sort-topics.js"></script>
|
|
@ -1,16 +0,0 @@
|
|||||||
<% for _, post in ipairs(prev_context) do %>
|
|
||||||
<% render("views.threads.post", {post = post, edit = false, is_latest = false, no_reply = true}) %>
|
|
||||||
<% end %>
|
|
||||||
<span class="context-explain">
|
|
||||||
<span>↑↑↑</span><i>Context</i><span>↑↑↑</span>
|
|
||||||
</span>
|
|
||||||
<% if infobox then %>
|
|
||||||
<% render("views.common.infobox", infobox) %>
|
|
||||||
<% end %>
|
|
||||||
<% render("views.threads.post", {post = editing_post, edit = true, is_latest = false, no_reply = true}) %>
|
|
||||||
<span class="context-explain">
|
|
||||||
<span>↓↓↓</span><i>Context</i><span>↓↓↓</span>
|
|
||||||
</span>
|
|
||||||
<% for _, post in ipairs(next_context) do %>
|
|
||||||
<% render("views.threads.post", {post = post, edit = false, is_latest = false, no_reply = true}) %>
|
|
||||||
<% end %>
|
|
@ -1,9 +0,0 @@
|
|||||||
<% if not post then %>
|
|
||||||
<% render("views.common.infobox", {kind = constants.InfoboxKind.ERROR, msg = "Post not found"}) %>
|
|
||||||
<% else %>
|
|
||||||
<div class=darkbg>
|
|
||||||
<h1 class=thread-title><%= post.username .. "'s post in " .. thread.title %></h1>
|
|
||||||
</div>
|
|
||||||
<% render("views.threads.post", {post = post, edit = false, is_latest = false, no_reply = true}) %>
|
|
||||||
<a class=linkbutton href="<%= url_for("thread", {slug = thread.slug}, {after = post.id}) .. "#post-" .. post.id %>">View in context</a>
|
|
||||||
<% end %>
|
|
@ -1,10 +1,4 @@
|
|||||||
<%
|
<div class="post" id="post-<%= post.id %>">
|
||||||
local pc = "post"
|
|
||||||
if edit then
|
|
||||||
pc = pc .. " editing"
|
|
||||||
end
|
|
||||||
%>
|
|
||||||
<div class="<%= pc %>" id="post-<%= post.id %>">
|
|
||||||
<div class="usercard">
|
<div class="usercard">
|
||||||
<a href="<%= url_for("user", {username = post.username}) %>" style="display: contents;">
|
<a href="<%= url_for("user", {username = post.username}) %>" style="display: contents;">
|
||||||
<img src="<%= post.avatar_path %>" class="avatar">
|
<img src="<%= post.avatar_path %>" class="avatar">
|
||||||
@ -14,52 +8,19 @@
|
|||||||
<em class="user-status"><%= post.status %></em>
|
<em class="user-status"><%= post.status %></em>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="post-content-container"<%= is_latest and 'id=latest-post' or "" %>>
|
<div class="post-content-container"<%= is_latest and 'id=latest-post' or "" %>>
|
||||||
<div class="post-info">
|
<div class="post-info">
|
||||||
<a href="<%= url_for("thread", {slug = thread.slug}, {page = page}) .. "#post-" .. post.id %>" title="Permalink"><i>
|
<div><a href="<%= "#post-" .. post.id %>" title="Permalink"><i>
|
||||||
<% if tonumber(post.edited_at) > tonumber(post.created_at) then -%>
|
<% if tonumber(post.edited_at) > tonumber(post.created_at) then -%>
|
||||||
Edited at <%= os.date("%c", post.edited_at) %>
|
Edited at <%= os.date("%c", post.edited_at) %>
|
||||||
<% else -%>
|
<% else -%>
|
||||||
Posted at <%= os.date("%c", post.created_at) %>
|
Posted at <%= os.date("%c", post.created_at) %>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
</i></a>
|
</i></a></div>
|
||||||
<span>
|
<div><button>Reply</button></div>
|
||||||
<%
|
|
||||||
local show_edit = me.id == post.user_id and not me:is_guest() and not ntob(thread.is_locked) and not no_reply
|
|
||||||
if show_edit then
|
|
||||||
%>
|
|
||||||
<a class="linkbutton" href="<%= url_for("edit_post", {post_id = post.id}) %>">Edit</a>
|
|
||||||
<% end %>
|
|
||||||
<%
|
|
||||||
local show_reply = true
|
|
||||||
if ntob(thread.is_locked) and not me:is_mod() then
|
|
||||||
show_reply = false
|
|
||||||
elseif me:is_guest() then
|
|
||||||
show_reply = false
|
|
||||||
elseif edit then
|
|
||||||
show_reply = false
|
|
||||||
elseif no_reply then
|
|
||||||
show_reply = false
|
|
||||||
end
|
|
||||||
if show_reply then
|
|
||||||
%>
|
|
||||||
<button>Reply</button>
|
|
||||||
<% end %>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="post-content">
|
<div class="post-content">
|
||||||
<% if not edit then %>
|
<%- post.content %>
|
||||||
<%- post.content %>
|
|
||||||
<% else %>
|
|
||||||
<form class="post-edit-form" method="post">
|
|
||||||
<textarea name="new_content" required><%- post.original_markup %></textarea>
|
|
||||||
<span>
|
|
||||||
<input type=submit value="Save">
|
|
||||||
<a class="linkbutton warn" href="<%= url_for("thread", {slug = thread.slug}, {page = page}) .. "#post-" .. post.id %>">Cancel</a>
|
|
||||||
</span>
|
|
||||||
</form>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
<% render("views.common.infobox", infobox) %>
|
<% render("views.common.infobox", infobox) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<% local is_locked = ntob(topic.is_locked) %>
|
|
||||||
|
|
||||||
<nav class="darkbg">
|
<nav class="darkbg">
|
||||||
<h1 class="thread-title">All threads in "<%= topic.name %>"</h1>
|
<h1 class="thread-title">All threads in "<%= topic.name %>"</h1>
|
||||||
<span><%= topic.description %></span>
|
<span><%= topic.description %></span>
|
||||||
@ -14,27 +12,25 @@
|
|||||||
<p>Your account is still pending confirmation by a moderator. You are not able to create a new thread or post at this time.</p>
|
<p>Your account is still pending confirmation by a moderator. You are not able to create a new thread or post at this time.</p>
|
||||||
<% elseif thread_create_error == ThreadCreateError.LOGGED_OUT then %>
|
<% elseif thread_create_error == ThreadCreateError.LOGGED_OUT then %>
|
||||||
<p>Only logged in users can create threads. <a href="<%= url_for("user_signup") %>">Sign up</a> or <a href="<%= url_for("user_login")%>">log in</a> to create a thread.</p>
|
<p>Only logged in users can create threads. <a href="<%= url_for("user_signup") %>">Sign up</a> or <a href="<%= url_for("user_login")%>">log in</a> to create a thread.</p>
|
||||||
|
<% else %>
|
||||||
|
<p>This topic is locked.</p>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% if me:is_mod() then %>
|
<% if me:is_mod() then %>
|
||||||
<a class="linkbutton" href="<%= url_for("topic_edit", {slug = topic.slug}) %>">Edit topic</a>
|
<a class="linkbutton" href="<%= url_for("topic_edit", {slug = topic.slug}) %>">Edit topic</a>
|
||||||
<form class="modform" method="post" action="<%= url_for("topic_edit", {slug = topic.slug}) %>">
|
<form class="modform" method="post" action="<%= url_for("topic_edit", {slug = topic.slug}) %>">
|
||||||
<input type="hidden" name="is_locked" value="<%= not is_locked %>">
|
<input type="hidden" name="is_locked" value="<%= not ntob(topic.is_locked) %>">
|
||||||
<input class="warn" type="submit" id="lock" value="<%= is_locked and "Unlock topic" or "Lock topic" %>">
|
<input class="warn" type="submit" id="lock" value="<%= ntob(topic.is_locked) and "Unlock topic" or "Lock topic" %>">
|
||||||
</form>
|
</form>
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<% if is_locked then -%>
|
|
||||||
<% render("views.common.infobox", {kind = constants.InfoboxKind.LOCK, msg = "This topic is locked. Only moderators can create new threads."}) %>
|
|
||||||
<% end -%>
|
|
||||||
|
|
||||||
<% if #threads_list == 0 then %>
|
<% if #threads_list == 0 then %>
|
||||||
<p>There are no threads in this topic.</p>
|
<p>There are no threads in this topic.</p>
|
||||||
<% else %>
|
<% else %>
|
||||||
<% for _, thread in ipairs(threads_list) do %>
|
<% for _, thread in ipairs(threads_list) do %>
|
||||||
<% local is_stickied = ntob(thread.is_stickied) %>
|
<% local is_stickied = ntob(thread.is_stickied) %>
|
||||||
<% local thread_is_locked = ntob(thread.is_locked) %>
|
<% local is_locked = ntob(thread.is_locked) %>
|
||||||
<div class="thread">
|
<div class="thread">
|
||||||
<div class="thread-sticky-container contain-svg">
|
<div class="thread-sticky-container contain-svg">
|
||||||
<% if is_stickied then -%>
|
<% if is_stickied then -%>
|
||||||
@ -58,7 +54,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="thread-locked-container contain-svg">
|
<div class="thread-locked-container contain-svg">
|
||||||
<% if thread_is_locked then -%>
|
<% if is_locked then -%>
|
||||||
<% render("svg-icons.lock") %>
|
<% render("svg-icons.lock") %>
|
||||||
<i>Locked</i>
|
<i>Locked</i>
|
||||||
<% end -%>
|
<% end -%>
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
<h1 class="thread-title">All topics</h1>
|
<h1 class="thread-title">All topics</h1>
|
||||||
<% if me:is_mod() then %>
|
<% if me:is_mod() then %>
|
||||||
<a class="linkbutton" href="<%= url_for("topic_create") %>">Create new topic</a>
|
<a class="linkbutton" href="<%= url_for("topic_create") %>">Create new topic</a>
|
||||||
<a class="linkbutton" href="<%= url_for("sort_topics") %>">Sort topics</a>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -13,7 +12,7 @@
|
|||||||
<% local is_locked = ntob(topic.is_locked) %>
|
<% local is_locked = ntob(topic.is_locked) %>
|
||||||
<div class="topic">
|
<div class="topic">
|
||||||
<div class="topic-info-container">
|
<div class="topic-info-container">
|
||||||
<a class="thread-title" href=<%= url_for("topic", {slug = topic.slug}) %>><%= topic.name %></a>
|
<a href=<%= url_for("topic", {slug = topic.slug}) %>><%= topic.name %></a>
|
||||||
<%= topic.description %>
|
<%= topic.description %>
|
||||||
<% if topic.latest_thread_username then %>
|
<% if topic.latest_thread_username then %>
|
||||||
<span>
|
<span>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<% if infobox then %>
|
<% if infobox then %>
|
||||||
<% render("views.common.infobox", infobox) %>
|
<% render("views.common.infobox", pop_infobox) %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="darkbg">
|
<div class="darkbg">
|
||||||
<h1 class="thread-title">Latest posts by <i><%= user.username %></i></h1>
|
<h1 class="thread-title">Latest posts by <i><%= user.username %></i></h1>
|
||||||
|
Reference in New Issue
Block a user