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 }}
+
+
+
+
+
+
+
+
+
+

+
+
+
+ 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) }}
+
+ {{ 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
+}