From 836ad725219b08bb057aa5d40edeb4ea5c9c4359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Sun, 18 May 2025 11:39:12 +0300 Subject: [PATCH] add avatars --- .gitignore | 1 + app.lua | 8 +++ apps/users.lua | 108 ++++++++++++++++++++++++++++++++++++++ migrations.lua | 12 ++++- models.lua | 5 ++ nginx.conf | 5 ++ util.lua | 45 ++++++++++++++++ views/user/settings.etlua | 18 +++++++ views/user/user.etlua | 1 + 9 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 util.lua create mode 100644 views/user/settings.etlua diff --git a/.gitignore b/.gitignore index b2c2066..10cc71a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ nginx.conf.compiled db.*.sqlite .vscode/ .local/ +static/ diff --git a/app.lua b/app.lua index 6c66b96..c89cb0b 100644 --- a/app.lua +++ b/app.lua @@ -1,9 +1,17 @@ local lapis = require("lapis") local app = lapis.Application() +local util = require("util") + app:enable("etlua") app.layout = require "views.base" +local function inject_methods(req) + req.avatar_url = util.get_user_avatar_url +end + +app:before_filter(inject_methods) + app:include("apps.users", {path = "/user"}) app:get("/", function() diff --git a/apps/users.lua b/apps/users.lua index fde4af7..9fd379d 100644 --- a/apps/users.lua +++ b/apps/users.lua @@ -3,12 +3,15 @@ local app = require("lapis").Application() local db = require("lapis.db") local constants = require("constants") +local util = require("util") + local bcrypt = require("bcrypt") local rand = require("openssl.rand") local models = require("models") local Users = models.Users local Sessions = models.Sessions +local Avatars = models.Avatars local TransientUser = { is_admin = function (self) @@ -76,6 +79,10 @@ local function validate_username(username) return username:match("^[%w_-]+$") and true end +local function validate_url(url) + return url:match('^https?://.+$') and true +end + app:get("user", "/:username", function(self) local user = Users:find({username = self.params.username}) if not user then @@ -101,6 +108,107 @@ app:get("user", "/:username", function(self) return {render = "user.user"} end) +app:post("user_clear_avatar", "/:username/clear_avatar", function(self) + local me = validate_session(self.session.session_key) + if me == nil then + self.session.flash = {error = "You must be logged in to perform this action."} + return {redirect_to = self:url_for("user_login")} + end + local target_user = Users:find({username = self.params.username}) + if me.id ~= target_user.id then + return {redirect_to = self:url_for("user", {username = self.params.username})} + end + target_user:update({ + avatar_id = db.NULL, + }) + self.session.flash = {success = true, msg = "Avatar cleared."} + return {redirect_to = self:url_for("user_settings", {username = self.params.username})} +end) + +app:post("user_set_avatar", "/:username/set_avatar", function(self) + local me = validate_session(self.session.session_key) + if me == nil then + self.session.flash = {error = "You must be logged in to perform this action."} + return {redirect_to = self:url_for("user_login")} + end + local target_user = Users:find({username = self.params.username}) + if me.id ~= target_user.id then + return {redirect_to = self:url_for("user", {username = self.params.username})} + end + local file = self.params.avatar + if not file then + self.session.flash = {error = "Something went wrong. Try again later."} + return {redirect_to = self:url_for("user_settings", {username = self.params.username})} + end + local time = os.time() + local filename = "u" .. target_user.id .. "d" .. time .. ".webp" + local proxied_filename = "/avatars/" .. filename + local save_path = "static" .. proxied_filename + local res = util.validate_and_create_image(file.content, save_path) + if not res then + self.session.flash = {error = "Something went wrong. Try again later."} + return {redirect_to = self:url_for("user_settings", {username = self.params.username})} + end + + self.session.flash = {success = true, msg = "Avatar updated."} + local avatar = Avatars:create({ + file_path = proxied_filename, + uploaded_at = time, + }) + + target_user:update({ + avatar_id = avatar.id + }) + + return {redirect_to = self:url_for("user_settings", {username = self.params.username})} +end) + +app:get("user_settings", "/:username/settings", function(self) + local me = validate_session(self.session.session_key) + if me == nil then + self.session.flash = {error = "You must be logged in to perform this action."} + return {redirect_to = self:url_for("user_login")} + end + local target_user = Users:find({username = self.params.username}) + if me.id ~= target_user.id then + return {redirect_to = self:url_for("user", {username = self.params.username})} + end + if self.session.flash then + local flash = self.session.flash + self.session.flash = nil + if flash.success then + self.flash_msg = flash.msg + elseif flash.error then + self.flash_msg = flash.error + end + end + self.user = target_user + return {render = "user.settings"} +end) + +app:post("user_settings", "/:username/settings", function(self) + local me = validate_session(self.session.session_key) + if me == nil then + self.session.flash = {error = "You must be logged in to perform this action."} + return {redirect_to = self:url_for("user_login")} + end + local target_user = Users:find({username = self.params.username}) + if me.id ~= target_user.id then + return {redirect_to = self:url_for("user", {username = self.params.username})} + end + + local status = self.params.status:sub(1, 100) + + target_user:update({ + status = status, + }) + self.session.flash = { + success = true, + msg = "Settings updated." + } + return {redirect_to = self:url_for("user_settings", {username = self.params.username})} +end) + app:get("user_login", "/login", function(self) if self.session.session_key then local user = validate_session(self.session.session_key) diff --git a/migrations.lua b/migrations.lua index 29fd504..807272f 100644 --- a/migrations.lua +++ b/migrations.lua @@ -18,5 +18,15 @@ return { [2] = function () schema.add_column("users", "confirmed_on", types.integer{null = true}) - end + end, + + [3] = function () + schema.add_column("users", "status", types.text{null = true, default=""}) + schema.create_table("avatars", { + {"id", types.integer{primary_key = true}}, + {"file_path", types.text{unique = true}}, + {"uploaded_at", "INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP))"}, + }) + schema.add_column("users", "avatar_id", "REFERENCES avatars(id) ON DELETE SET NULL") + end, } diff --git a/models.lua b/models.lua index 21b7927..9da8b62 100644 --- a/models.lua +++ b/models.lua @@ -12,6 +12,10 @@ function Users_mt:is_admin() return self.permission == constants.PermissionLevel.ADMIN end +function Users_mt:is_default_avatar() + return self.avatar_id == nil +end + local ret = { Users = Users, Topics = Model:extend("topics"), @@ -19,6 +23,7 @@ local ret = { Posts = Model:extend("posts"), PostHistory = Model:extend("post_history"), Sessions = Model:extend("sessions"), + Avatars = Model:extend("avatars"), } return ret diff --git a/nginx.conf b/nginx.conf index e9bcd06..ddb6441 100644 --- a/nginx.conf +++ b/nginx.conf @@ -32,5 +32,10 @@ http { location /favicon.ico { alias static/favicon.ico; } + + location /avatars { + alias static/avatars; + expires 1y; + } } } diff --git a/util.lua b/util.lua new file mode 100644 index 0000000..8c35435 --- /dev/null +++ b/util.lua @@ -0,0 +1,45 @@ +local util = {} +local magick = require("magick") + +local Avatars = require("models").Avatars + +function util.get_user_avatar_url(req, user) + if not user.avatar_id then + return "/avatars/default.webp" + end + return Avatars:find(user.avatar_id).file_path +end + +function util.validate_and_create_image(input_image, filename) + local img = magick.load_image_from_blob(input_image) + + if not img then + return false + end + + img:strip() + img:set_gravity("CenterGravity") + + local width, height = img:get_width(), img:get_height() + local min_dim = math.min(width, height) + if min_dim > 256 then + local ratio = 256.0 / min_dim + local new_w, new_h = width * ratio, height * ratio + img:resize(new_w, new_h) + end + + width, height = img:get_width(), img:get_height() + local crop_size = math.min(width, height) + local x_offset = (width - crop_size) / 2 + local y_offset = (height - crop_size) / 2 + img:crop(crop_size, crop_size, x_offset, y_offset) + + img:set_format("webp") + img:set_quality(85) + + img:write(filename) + img:destroy() + return true +end + +return util \ No newline at end of file diff --git a/views/user/settings.etlua b/views/user/settings.etlua new file mode 100644 index 0000000..ea02bfe --- /dev/null +++ b/views/user/settings.etlua @@ -0,0 +1,18 @@ +

User settings

+<% if flash_msg then %> +

<%= flash_msg %>

+<% end %> +
" enctype="multipart/form-data"> +
+
+ +<% if not user:is_default_avatar() then %> +"> +<% end %> +
+
+
+ +
+ +
diff --git a/views/user/user.etlua b/views/user/user.etlua index 3aa315e..755dcb9 100644 --- a/views/user/user.etlua +++ b/views/user/user.etlua @@ -1,6 +1,7 @@ <% if just_logged_in then %>

Logged in successfully.

<% end %> +

<%= user.username %>

<% if user:is_guest() and user_is_me then %>

You are a guest. An administrator needs to approve your account before you will be able to post.