diff --git a/database/user.go b/database/user.go index 421bf90..48b0497 100644 --- a/database/user.go +++ b/database/user.go @@ -22,7 +22,10 @@ import ( "git.erbosoft.com/amy/amsterdam/util" lru "github.com/hashicorp/golang-lru" + "github.com/klauspost/lctime" log "github.com/sirupsen/logrus" + "golang.org/x/text/language" + "golang.org/x/text/message" ) // UserPrefs represents the user's preferences in a table (one row per user). @@ -60,6 +63,30 @@ func (p *UserPrefs) Save(u *User) error { return err } +// Localizer returns a localizer for this locale. +func (p *UserPrefs) Localizer() lctime.Localizer { + lc, err := lctime.NewLocalizer(p.LocaleID) + if err != nil { + log.Fatalf("BOGUS LANGUAGE TAG %s in user prefs for uid %d", p.LocaleID, p.Uid) + } + return lc +} + +// LanguageTag returns the user's language tag. +func (p *UserPrefs) LanguageTag() *language.Tag { + lt, err := language.Parse(p.ReadLocale()) + if err != nil { + log.Fatalf("BOGUS LANGUAGE TAG %s in user prefs for uid %d", p.LocaleID, p.Uid) + return nil + } + return < +} + +// MessagePrinter returns a message printer for the user's selected locale. +func (p *UserPrefs) MessagePrinter() *message.Printer { + return message.NewPrinter(*p.LanguageTag()) +} + // User represents a user in the Amsterdam database. type User struct { Mutex sync.RWMutex diff --git a/go.mod b/go.mod index 9476a4f..ffcdb4a 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/gorilla/sessions v1.4.0 github.com/hashicorp/golang-lru v1.0.2 github.com/jmoiron/sqlx v1.4.0 + github.com/klauspost/lctime v0.1.0 github.com/labstack/echo-contrib v0.17.4 github.com/labstack/echo/v4 v4.13.4 github.com/labstack/gommon v0.4.2 diff --git a/go.sum b/go.sum index 7e8783d..c475121 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,8 @@ github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iP github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/klauspost/lctime v0.1.0 h1:nINsuFc860M9cyYhT6vfg6U1USh7kiVBj/s/2b04U70= +github.com/klauspost/lctime v0.1.0/go.mod h1:OwdMhr8tbQvusAsnilqkkgDQqivWlqyg0w5cfXkLiDk= github.com/labstack/echo-contrib v0.17.4 h1:g5mfsrJfJTKv+F5uNKCyrjLK7js+ZW6HTjg4FnDxxgk= github.com/labstack/echo-contrib v0.17.4/go.mod h1:9O7ZPAHUeMGTOAfg80YqQduHzt0CzLak36PZRldYrZ0= github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= diff --git a/main.go b/main.go index 31ea613..3d340e7 100644 --- a/main.go +++ b/main.go @@ -56,6 +56,8 @@ func setupEcho() *echo.Echo { e.POST("/profile", ui.AmWrap(EditProfile)) e.GET("/profile_photo", ui.AmWrap(ProfilePhotoForm)) e.POST("/profile_photo", ui.AmWrap(ProfilePhoto)) + e.GET("/user/:uname", ui.AmWrap(ShowProfile)) + e.POST("/quick_email", ui.AmWrap(QuickEMail)) return e } diff --git a/top.go b/top.go index 8fd9666..a6afc69 100644 --- a/top.go +++ b/top.go @@ -121,7 +121,7 @@ func buildUsersOnline(uid int32, out *RenderedSidebox, in *database.Sidebox) err } for i, n := range users { out.Items[b+i].Text = n - lk := fmt.Sprintf("/TODO/user/%s", n) + lk := fmt.Sprintf("/user/%s", n) out.Items[b+i].Link = &lk out.Items[b+i].Flags = make(map[string]bool) out.Items[b+i].Flags["bold"] = true diff --git a/ui/views/profile.jet b/ui/views/profile.jet new file mode 100644 index 0000000..f933e67 --- /dev/null +++ b/ui/views/profile.jet @@ -0,0 +1,124 @@ +{* + * Amsterdam Web Communities System + * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + *} +
+ +
+

User Profile:

+ {{ username }} +
+
+ + +
+
+ +
+
+ {{ username }}'s photo +
+
+
+ Account created:
{{ dateCreated }} +
+ {{ if isset(dateLastLogin) }} +
+ Last login:
{{ dateLastLogin }} +
+ {{ end }} + {{ if isset(dateLastUpdate) }} +
+ Profile last updated:
{{ dateLastUpdate }} +
+ {{ end }} +
+
+ + +
+
+
{{ fullname }}
+ {{ if isset(description) }}
{{ description }}
{{ end }} + {{ if isset(email) }}
E-mail: {{ email }}
{{ end }} + {{ if isset(url) }} +
+ URL: + {{ url }} +
+ {{ end }} + +
+ {{ if isset(company) }}
{{ company }}
{{ end }} + {{ if isset(addr1) }}
{{ addr1 }}
{{ end }} + {{ if isset(addr2) }}
{{ addr2 }}
{{ end }} +
{{ addrLast }}
+ {{ if isset(country) }}
{{ country }}
{{ end }} +
+ + {{ if isset(phone) || isset(fax) || isset(mobile) }} +
+ {{ if isset(phone) }}
Phone: {{ phone }}
{{ end }} + {{ if isset(fax) }}
Fax: {{ fax }}
{{ end }} + {{ if isset(mobile) }}
Mobile: {{ mobile }}
{{ end }} +
+ {{ end }} +
+
+
+
+ + {{ if !.CurrentUser().IsAnon }} + +
+
+

Send Quick E-Mail To {{ username }}:

+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ + +
+ +
+
+
+
+ {{ end }} +
\ No newline at end of file diff --git a/userdata.go b/userdata.go index 8f893db..36d4573 100644 --- a/userdata.go +++ b/userdata.go @@ -12,13 +12,16 @@ package main import ( "errors" "fmt" + "net/http" "net/url" "strconv" "strings" "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/email" "git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/util" + "github.com/biter777/countries" log "github.com/sirupsen/logrus" ) @@ -226,7 +229,7 @@ func ProfilePhotoForm(ctxt ui.AmContext) (string, any, error) { return ui.ErrorPage(ctxt, err) } -/* ProfilePhoto handles processing the uploaded user photo.. +/* ProfilePhoto handles processing the uploaded user photo. * Parameters: * ctxt - The AmContext for the request. * Returns: @@ -310,3 +313,144 @@ func ProfilePhoto(ctxt ui.AmContext) (string, any, error) { } return ui.ErrorPage(ctxt, errors.New("invalid button detected in photo upload")) } + +/* ShowProfile displays a user's profile. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ +func ShowProfile(ctxt ui.AmContext) (string, any, error) { + me := ctxt.CurrentUser() + prefs, err := me.Prefs() + if err != nil { + return ui.ErrorPage(ctxt, err) + } + + // Gather the info on the current user. + user, err := database.AmGetUserByName(ctxt.URLParam("uname")) + if err != nil { + ctxt.SetRC(http.StatusNotFound) + return ui.ErrorPage(ctxt, err) + } + ci, err := user.ContactInfo() + if err != nil { + return ui.ErrorPage(ctxt, err) + } + + // Fill all the page variables for display. + ctxt.VarMap().Set("uid", user.Uid) + ctxt.VarMap().Set("username", user.Username) + ctxt.VarMap().Set("photoURL", userPhotoURL(ci)) + loc := prefs.Localizer() + ctxt.VarMap().Set("dateCreated", loc.Strftime("%x %X", user.Created)) + if user.LastAccess != nil { + ctxt.VarMap().Set("dateLastLogin", loc.Strftime("%x %X", *user.LastAccess)) + } + if ci.LastUpdate != nil { + ctxt.VarMap().Set("dateLastUpdate", loc.Strftime("%x %X", *ci.LastUpdate)) + } + var b strings.Builder + if ci.Prefix != nil && *ci.Prefix != "" { + b.WriteString(*ci.Prefix + " ") + } + b.WriteString(ci.GivenName) + if ci.MiddleInit != nil && *ci.MiddleInit != "" && *ci.MiddleInit != " " { + b.WriteString(" " + *ci.MiddleInit + ".") + } + b.WriteString(" " + ci.FamilyName) + if ci.Suffix != nil && *ci.Suffix != "" { + b.WriteString(" " + *ci.Suffix) + } + ctxt.VarMap().Set("fullname", b.String()) + if user.Description != nil { + ctxt.VarMap().Set("description", *user.Description) + } + if !ci.PrivateEmail && ci.Email != nil { + ctxt.VarMap().Set("email", *ci.Email) + } + if ci.URL != nil { + ctxt.VarMap().Set("url", *ci.URL) + } + if ci.Company != nil { + ctxt.VarMap().Set("company", *ci.Company) + } + if !ci.PrivateAddr && ci.Addr1 != nil { + ctxt.VarMap().Set("addr1", *ci.Addr1) + } + if !ci.PrivateAddr && ci.Addr2 != nil { + ctxt.VarMap().Set("addr2", *ci.Addr2) + } + b.Reset() + if ci.Locality != nil { + b.WriteString(*ci.Locality) + if ci.Region != nil { + b.WriteString(", ") + } + } + if ci.Region != nil { + b.WriteString(*ci.Region) + } + if ci.PostalCode != nil { + b.WriteString(" " + *ci.PostalCode) + } + ctxt.VarMap().Set("addrLast", b.String()) + if ci.Country != nil { + country := countries.ByName(*ci.Country) + ctxt.VarMap().Set("country", country.String()) + } + if !ci.PrivatePhone && ci.Phone != nil { + ctxt.VarMap().Set("phone", *ci.Phone) + } + if !ci.PrivateFax && ci.Fax != nil { + ctxt.VarMap().Set("fax", *ci.Fax) + } + if !ci.PrivatePhone && ci.Mobile != nil { + ctxt.VarMap().Set("mobile", *ci.Mobile) + } + ctxt.VarMap().Set("amsterdam_pageTitle", fmt.Sprintf("User Profile - %s", user.Username)) + return "framed_template", "profile.jet", nil +} + +/* QuickEMail sends quick E-mail to a user. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ +func QuickEMail(ctxt ui.AmContext) (string, any, error) { + me := ctxt.CurrentUser() + if me.IsAnon { + return ui.ErrorPage(ctxt, errors.New("you are not logged in")) + } + myCI, err := me.ContactInfo() + if err != nil { + return ui.ErrorPage(ctxt, err) + } + toUid, err := ctxt.FormFieldInt("to_uid") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + user, err := database.AmGetUser(int32(toUid)) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + if user.IsAnon { + return ui.ErrorPage(ctxt, errors.New("cannot send quick E-mail to anonymous user")) + } + ci, err := user.ContactInfo() + if err != nil { + return ui.ErrorPage(ctxt, err) + } + msg := email.AmNewEmailMessage(me.Uid, ctxt.RemoteIP()) + msg.AddTo(*ci.Email, user.Username) + msg.AddHeader("X-Originally-From", fmt.Sprintf("%s <%s>", me.Username, *myCI.Email)) + msg.SetSubject(ctxt.FormField("subj")) + msg.SetText(ctxt.FormField("pb")) + msg.Send() + return "redirect", "/user/" + user.Username, nil +}