loading user agreement from a "resource" now

This commit is contained in:
2026-03-04 16:34:19 -07:00
parent 573707587d
commit e158af99f1
10 changed files with 124 additions and 42 deletions
+2
View File
@@ -7,6 +7,8 @@ After the point where it reaches feature parity with Venice circa 2006.
* Policy page support.
* User agreement in a separate file rather than directly in settings.
* Support all customizations that were done with the EMinds instance of Venice.
* Gitea-like status page showing Go-specific internals.
* Build static Tailwind CSS file rather than using remote-loaded version. (Gate on debug/prod flag)
## Immediate Cleanups Required
+26 -2
View File
@@ -15,6 +15,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"reflect"
@@ -68,7 +69,6 @@ type AmConfig struct {
Title string `yaml:"title"`
Text string `yaml:"text"`
} `yaml:"userAgreement"`
ExternalPath string `yaml:"externalPath"`
} `yaml:"site"`
Database struct {
Driver string `yaml:"driver"`
@@ -91,13 +91,16 @@ type AmConfig struct {
Disclaimer string `yaml:"disclaimer"`
} `yaml:"email"`
Rendering struct {
TemplateDir string `yaml:"templatedir"`
CookieKey string `yaml:"cookiekey"`
CountryList struct {
Prioritize string `yaml:"prioritize"`
} `yaml:"countryList"`
VeniceCompatibleImageURLs bool `yaml:"veniceCompatibleImageURLs"`
} `yaml:"rendering"`
Resources struct {
ViewTemplateDir string `yaml:"viewTemplateDir"`
ExternalContentPath string `yaml:"externalContentPath"`
} `yaml:"resources"`
Posting struct {
ExternalDictionary string `yaml:"externalDictionary"`
Uploads struct {
@@ -256,6 +259,27 @@ func parseDataSize(s string) (int32, error) {
return int32(rc), nil
}
// AmOpenExternalContentPath opens the "external content path" specified in the configuration as a root filesystem.
func AmOpenExternalContentPath() (fs.FS, error) {
if GlobalConfig.Resources.ExternalContentPath == "" {
return nil, nil
}
finfo, err := os.Stat(GlobalConfig.Resources.ExternalContentPath)
if err != nil {
log.Errorf("external content path \"%s\" is not valid, ignored (%v)", GlobalConfig.Resources.ExternalContentPath, err)
return nil, nil
}
if !finfo.IsDir() {
log.Errorf("external content path \"%s\" is not a directory, ignored", GlobalConfig.Resources.ExternalContentPath)
return nil, nil
}
root, err := os.OpenRoot(GlobalConfig.Resources.ExternalContentPath)
if err != nil {
return nil, err
}
return root.FS(), nil
}
// SetupConfig loads the command line arguments, loads the config file, and prepares GlobalConfig.
func SetupConfig() {
argparse.MustParse(&CommandLine)
+3 -2
View File
@@ -17,7 +17,6 @@ site:
title: "Amsterdam User Agreement"
text: >
Text of this agreement is TBD.
externalPath: ""
database:
driver: "mysql"
dsn: "amsdb:x00yes2k@tcp(localhost)/amsterdam?parseTime=true&loc=UTC"
@@ -40,11 +39,13 @@ email:
The Amsterdam Project is not responsible for the contents of this message
Report abuses to: <abuse@example.com>
rendering:
templatedir: custom_templates
cookiekey: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
countryList:
prioritize: US
veniceCompatibleImageURLs: false
resources:
viewTemplateDir: ""
externalContentPath: ""
posting:
externalDictionary: ""
uploads:
+8
View File
@@ -315,8 +315,16 @@ func NewAccountUserAgreement(ctxt ui.AmContext) (string, any) {
return "error", ENOACCOUNT
}
// Load the user agreement from the resources.
agreementTitle, agreementBody, err := ui.AmLoadHTMLResource("useragreement.html")
if err != nil {
return "error", err
}
ctxt.SetLeftMenu("top")
ctxt.VarMap().Set("target", target)
ctxt.VarMap().Set("agreementTitle", agreementTitle)
ctxt.VarMap().Set("agreementBody", agreementBody)
ctxt.SetScratch("frame_suppressLogin", true)
ctxt.SetFrameTitle("New Account User Agreement")
return "framed", "agreement.jet"
+5 -6
View File
@@ -53,12 +53,11 @@ func setupEcho() *echo.Echo {
uiset := []echo.MiddlewareFunc{ui.SessionStoreInjector, ui.ContextCreator, ui.IPBanTest, ui.CookieLoginTest}
e.RouteNotFound("/*", ui.AmWrap(AmNotFoundHandler), uiset...)
if config.GlobalConfig.Site.ExternalPath != "" {
root, err := os.OpenRoot(config.GlobalConfig.Site.ExternalPath)
if err != nil {
panic(err)
}
fs := root.FS()
fs, err := config.AmOpenExternalContentPath()
if err != nil {
panic(err)
}
if fs != nil {
e.StaticFS("/ext", fs)
e.GET("/fext/*", ui.AmWrap(ui.AmStaticFramePage(fs, "/fext/")), uiset...)
}
+37
View File
@@ -0,0 +1,37 @@
<html>
<head>
<title>Amsterdam User Agreement</title>
</head>
<body>
<p><i>This is a "sample" user agreement, intended as a default. Amsterdam sites are encouraged to provide their
own version of the user agreement.</i></p>
<p>By creating a user account, you agree to abide by the following rules regarding your conduct on
our system. These rules may also be found as part of our <a href="/policy">Policy page</a>.</p>
<p>Your messages are your words, and you are responsible for them, as well as your overall behavior as a
member of the community. As a member of the community, you agree not to post messages that harass, solicit,
threaten, offend, embarrass or impersonate any other person or that disrupt the dialogue in the community,
or use this site to do any of the above. In addition, you agree not to post messages that violate other persons'
intellectual property, privacy or other rights.</p>
<p>When you post a message to this site, or to any community hosted on this server, you grant the site
the right to display your message on the page on which it was posted by you. It may also be displayed on
other pages on the site, or be reachable through searches or other means, but it will always and only be here,
on this server, and we have no intention of ever reusing, reprinting, or recreating your message anywhere else.
You lose no copyright to your words, and you are not beholden to us in any way, shape, or form.</p>
<p>By posting here, you also grant us (the moderator(s) of the conference to which you post, the host and
cohost(s) of the community in which that conference is located, or the administrators of the site) the right to
remove your message if we choose not to want it here. We do not edit messages, except in extreme cases of
messages which include HTML or other characters that damage the usability of the site. We do remove
messages that are inappropriate or offensive to the admins. However, in the case of messages or message
attachments that contain copyrighted information or data that has been distributed in violation of
copyright, we are required by law to delete these messages if and when the legitimate copyright holder
contacts us, gives us the name and/or type of information that was posted and the location it was posted
to on this site, and requests that it be removed. This policy is the direct result of the United
States Digital Millennium Copyright Act, and neither this site nor the Amsterdam team is responsible for
this law or its effects.</p>
<p>The administrators of this site are not responsible for the accuracy or integrity of the information
contained in any messages posted to the site, and will not be held liable for any damages of any kind
incurred as the result of any message posted on this site. Some messages and site content may contain links
to sites on the Internet and World Wide Web other than this site itself. The administrators are
not responsible for the content of these other sites, and will not be held liable for any of their content.</p>
</body>
</html>
+1
View File
@@ -17,6 +17,7 @@ func SetupUILayer() func() {
exitfuncs := make([]func(), 0, 2)
setupTemplates()
setupMenuCache()
setupResources()
exitfuncs = append(exitfuncs, setupSessionManager())
exitfuncs = append(exitfuncs, setupContext())
return func() {
+19
View File
@@ -16,6 +16,7 @@ import (
"io"
"io/fs"
"net/http"
"path/filepath"
"strings"
"github.com/labstack/echo/v4"
@@ -25,6 +26,13 @@ import (
//go:embed static/*
var static_data embed.FS
//go:embed resources/*
var static_resources embed.FS
func setupResources() {
// do nothing yet
}
// AmStaticFileHandler returns a handler for the files in the static embedded filesystem.
func AmStaticFileHandler() echo.HandlerFunc {
fsys, err := fs.Sub(static_data, "static")
@@ -85,6 +93,17 @@ func breakUpHTML(r io.Reader) (string, string, error) {
return title, body, nil
}
// AmLoadHTMLResource loads an HTML resource and splits it into title and body.
func AmLoadHTMLResource(resourceName string) (string, string, error) {
f, err := static_resources.Open(filepath.Join("resources", resourceName))
if err != nil {
return "", "", err
}
title, body, err := breakUpHTML(f)
f.Close()
return title, body, err
}
/* AmStaticFramePage generates a handler that will serve up data from an external filesystem "framed" inside
* the frame with the template engine.
* Parameters:
+20 -29
View File
@@ -15,6 +15,7 @@ import (
"errors"
"fmt"
"io"
"os"
"reflect"
"regexp"
"strconv"
@@ -202,28 +203,7 @@ func displayMemberCount(a jet.Arguments) reflect.Value {
// displayFullName extracts a full name from a contact record.
func displayFullName(a jet.Arguments) reflect.Value {
ci := a.Get(0).Convert(reflect.TypeFor[*database.ContactInfo]()).Interface().(*database.ContactInfo)
var rc strings.Builder
if ci.Prefix != nil && *ci.Prefix != "" {
rc.WriteString(*ci.Prefix)
rc.WriteString(" ")
}
if ci.GivenName != nil && *ci.GivenName != "" {
rc.WriteString(*ci.GivenName)
}
if ci.MiddleInit != nil && *ci.MiddleInit != "" {
rc.WriteString(" ")
rc.WriteString(*ci.MiddleInit)
rc.WriteString(".")
}
if ci.FamilyName != nil && *ci.FamilyName != "" {
rc.WriteString(" ")
rc.WriteString(*ci.FamilyName)
}
if ci.Suffix != nil && *ci.Suffix != "" {
rc.WriteString(" ")
rc.WriteString(*ci.Suffix)
}
return reflect.ValueOf(rc.String())
return reflect.ValueOf(ci.FullName(true))
}
// displayExpandCat displays a category expanded into a hierarchy.
@@ -305,13 +285,24 @@ func postRewrite(a jet.Arguments) reflect.Value {
// setupTemplates is called to set up the template renderer after the configuration is loaded.
func setupTemplates() {
views = jet.NewSet(
multi.NewLoader(
jet.NewOSFileSystemLoader(config.GlobalConfig.Rendering.TemplateDir),
embedfs.NewLoader("views/", static_views),
),
jet.DevelopmentMode(true),
)
// Set up the template loaders: the optional filesystem loader, then the embedded loader.
templateLoaders := make([]jet.Loader, 0, 2)
if config.GlobalConfig.Resources.ViewTemplateDir != "" {
finfo, err := os.Stat(config.GlobalConfig.Resources.ViewTemplateDir)
if err == nil {
if finfo.IsDir() {
templateLoaders = append(templateLoaders, jet.NewOSFileSystemLoader(config.GlobalConfig.Resources.ViewTemplateDir))
} else {
log.Errorf("view template directory %s is not a directory, ignored", config.GlobalConfig.Resources.ViewTemplateDir)
}
} else {
log.Errorf("view template directory %s is not valid, ignored (%v)", config.GlobalConfig.Resources.ViewTemplateDir, err)
}
}
templateLoaders = append(templateLoaders, embedfs.NewLoader("views/", static_views))
// Create the template renderer and add our globals to it.
views = jet.NewSet(multi.NewLoader(templateLoaders...), jet.DevelopmentMode(true))
views.AddGlobal("AmsterdamVersion", config.AMSTERDAM_VERSION)
views.AddGlobal("AmsterdamCopyright", config.AMSTERDAM_COPYRIGHT)
views.AddGlobal("GlobalConfig", config.GlobalConfig)
+3 -3
View File
@@ -1,6 +1,6 @@
{*
* Amsterdam Web Communities System
* Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved
* Copyright (c) 2025-2026 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
@@ -9,10 +9,10 @@
<div class="flex">
<div class="flex-1 p-4">
<div class="mb-8">
<h1 class="text-blue-800 text-4xl font-bold mb-2">{{ GlobalConfig.Site.UserAgreement.Title }}</h1>
<h1 class="text-blue-800 text-4xl font-bold mb-2">{{ agreementTitle }}</h1>
<hr class="border-2 border-gray-400 w-4/5 mb-4">
<p class="text-black text-sm mb-4">
{{ GlobalConfig.Site.UserAgreement.Text | raw }}
{{ agreementBody | raw }}
</p>
<div class="flex justify-center gap-4 mt-6">
<button type="button" class="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded font-medium transition-colors"