diff --git a/conference_ops.go b/conference_ops.go index 67252df..9a502cf 100644 --- a/conference_ops.go +++ b/conference_ops.go @@ -26,8 +26,6 @@ import ( log "github.com/sirupsen/logrus" ) -var ENOPERM error = errors.New("you are not permitted to perform this operation") - // slurpFile reads the contrents of a multipart.File into memory. func slurpFile(file *multipart.FileHeader) ([]byte, error) { f, err := file.Open() diff --git a/conferenceadmin.go b/conferenceadmin.go new file mode 100644 index 0000000..938cacb --- /dev/null +++ b/conferenceadmin.go @@ -0,0 +1,67 @@ +/* + * Amsterdam Web Communities System + * 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 + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +// Package main contains the high-level Amsterdam logic. +package main + +import ( + "net/http" + + "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/ui" +) + +/* EditConferenceForm displays the dialog for editing the conference properties. + * 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 EditConferenceForm(ctxt ui.AmContext) (string, any, error) { + comm := ctxt.CurrentCommunity() + conf := ctxt.GetScratch("currentConference").(*database.Conference) + myLevel := ctxt.GetScratch("levelInConference").(uint16) + if !conf.TestPermission("Conference.Change", myLevel) { + ctxt.SetRC(http.StatusForbidden) + return ui.ErrorPage(ctxt, ENOPERM) + } + + dlg, err := ui.AmLoadDialog("edit_conference") + if err != nil { + return ui.ErrorPage(ctxt, err) + } + dlg.SetCommunity(comm) + dlg.SetConference(conf, ctxt.GetScratch("currentAlias").(string)) + dlg.Field("name").Value = conf.Name + dlg.Field("descr").SetVal(conf.Description) + if comm.TestPermission("Community.Create", ctxt.EffectiveLevel()) { + f, err := conf.HiddenInList(ctxt.Ctx(), comm) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + dlg.Field("hide").SetChecked(f) + } else { + dlg.Field("hide").Disabled = true + } + dlg.Field("read_lvl").SetLevel(conf.ReadLevel) + dlg.Field("post_lvl").SetLevel(conf.PostLevel) + dlg.Field("create_lvl").SetLevel(conf.CreateLevel) + dlg.Field("hide_lvl").SetLevel(conf.HideLevel) + dlg.Field("nuke_lvl").SetLevel(conf.NukeLevel) + dlg.Field("change_lvl").SetLevel(conf.ChangeLevel) + dlg.Field("delete_lvl").SetLevel(conf.DeleteLevel) + flags, err := conf.Flags(ctxt.Ctx()) + if err != nil { + return ui.ErrorPage(ctxt, err) + } + dlg.Field("pic_in_post").SetChecked(flags.Get(database.ConferenceFlagPicturesInPosts)) + return dlg.Render(ctxt) +} diff --git a/database/community.go b/database/community.go index f0bac4c..b874e50 100644 --- a/database/community.go +++ b/database/community.go @@ -432,7 +432,7 @@ func (c *Community) PermissionLevel(perm string) uint16 { } } -// GetFlags retrieves the flags from the properties. +// Flags retrieves the flags from the properties. func (c *Community) Flags(ctx context.Context) (*util.OptionSet, error) { c.Mutex.Lock() defer c.Mutex.Unlock() @@ -761,7 +761,7 @@ func AmAutoJoinCommunities(ctx context.Context, user *User) error { return err } -// internalGetProp is a helper used by the property functions. +// internalGetCommProp is a helper used by the community property functions. func internalGetCommProp(ctx context.Context, cid int32, ndx int32) (*CommunityProperties, error) { var err error = nil key := fmt.Sprintf("%d:%d", cid, ndx) @@ -785,7 +785,7 @@ func internalGetCommProp(ctx context.Context, cid int32, ndx int32) (*CommunityP return rc.(*CommunityProperties), nil } -/* AmGetCommunityProperty retrieves the value of a user property. +/* AmGetCommunityProperty retrieves the value of a community property. * Parameters: * ctx - Standard Go context value. * cid - The ID of the community to get the property for. diff --git a/database/conference.go b/database/conference.go index 6ce1ac3..9725471 100644 --- a/database/conference.go +++ b/database/conference.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "git.erbosoft.com/amy/amsterdam/util" lru "github.com/hashicorp/golang-lru" "github.com/jmoiron/sqlx" log "github.com/sirupsen/logrus" @@ -40,6 +41,7 @@ type Conference struct { Description *string `db:"descr"` // conference description IconUrl *string `db:"icon_url"` // conference icon URL Color *string `db:"color"` // color for conference + flags *util.OptionSet } type ConferenceSettings struct { @@ -51,6 +53,23 @@ type ConferenceSettings struct { newflag bool } +// ConferenceProperties represents a property entry for a conference. +type ConferenceProperties struct { + ConfId int32 `db:"confid"` // conference ID + Index int32 `db:"ndx"` // property index + Data *string `db:"data"` // property data +} + +// Conference property indexes defined. +const ( + ConferencePropFlags = int32(0) +) + +// Flag values for conference property index ConferencePropFlags defined. +const ( + ConferenceFlagPicturesInPosts = uint(0) +) + // conferenceCache is the cache for Conference objects. var conferenceCache *lru.TwoQueueCache = nil @@ -60,6 +79,12 @@ var getConferenceMutex sync.Mutex // conferenceAliasMap stores alias mappings. var conferenceAliasMap sync.Map +// conferencePropCache is the cache for ConferenceProperties objects. +var conferencePropCache *lru.Cache = nil + +// getConferencePropMutex is a mutex on AmGetConferenceProperty. +var getConferencePropMutex sync.Mutex + // init initializes the conference cache. func init() { var err error @@ -67,6 +92,10 @@ func init() { if err != nil { panic(err) } + conferencePropCache, err = lru.New(100) + if err != nil { + panic(err) + } } // Save saves the conference settings. @@ -143,6 +172,20 @@ func (c *Conference) InCommunity(ctx context.Context, comm *Community) (bool, er return false, err } +// HiddenInList returns whether or not this conference is hidden in the community's conference list. +func (c *Conference) HiddenInList(ctx context.Context, comm *Community) (bool, error) { + row := amdb.QueryRowContext(ctx, "SELECT hide_list FROM commtoconf WHERE commid = ? AND confid = ?", comm.Id, c.ConfId) + var rc bool + err := row.Scan(&rc) + switch err { + case nil: + return rc, nil + case sql.ErrNoRows: + return false, errors.New("conference not in community") + } + return false, err +} + // ContainedBy returns the communities that contain this conference. func (c *Conference) ContainedBy(ctx context.Context) ([]*Community, error) { rs, err := amdb.QueryContext(ctx, "SELECT commid FROM commtoconf WHERE confid = ?", c.ConfId) @@ -214,6 +257,36 @@ func (c *Conference) TestPermission(perm string, level uint16) bool { } } +// Flags retrieves the flags from the properties. +func (c *Conference) Flags(ctx context.Context) (*util.OptionSet, error) { + c.Mutex.Lock() + defer c.Mutex.Unlock() + if c.flags == nil { + s, err := AmGetConferenceProperty(ctx, c.ConfId, ConferencePropFlags) + if err != nil { + return nil, err + } + if s == nil { + c.flags = util.NewOptionSet() + } else { + c.flags = util.OptionSetFromString(*s) + } + } + return c.flags, nil +} + +// SaveFlags writes the flags to the database and stores them. +func (c *Conference) SaveFlags(ctx context.Context, f *util.OptionSet) error { + s := f.AsString() + c.Mutex.Lock() + defer c.Mutex.Unlock() + err := AmSetConferenceProperty(ctx, c.ConfId, ConferencePropFlags, &s) + if err == nil { + c.flags = f + } + return err +} + // Settings returns the settings for a user. func (c *Conference) Settings(ctx context.Context, u *User) (*ConferenceSettings, error) { var dbdata []ConferenceSettings @@ -525,3 +598,77 @@ func AmGetCommunityConferences(ctx context.Context, cid int32, showHidden bool) } return rc, nil } + +// internalGetConfProp is a helper used by the conference property functions. +func internalGetConfProp(ctx context.Context, confid int32, ndx int32) (*ConferenceProperties, error) { + var err error = nil + key := fmt.Sprintf("%d:%d", confid, ndx) + getConferencePropMutex.Lock() + defer getConferencePropMutex.Unlock() + rc, ok := conferencePropCache.Get(key) + if !ok { + var dbdata []ConferenceProperties + if err = amdb.SelectContext(ctx, &dbdata, "SELECT * from propconf WHERE confid = ? AND ndx = ?", confid, ndx); err != nil { + return nil, err + } + if len(dbdata) == 0 { + return nil, nil + } + if len(dbdata) > 1 { + return nil, fmt.Errorf("AmGetConferenceProperty(%d): too many responses(%d)", confid, len(dbdata)) + } + rc = &(dbdata[0]) + conferencePropCache.Add(key, rc) + } + return rc.(*ConferenceProperties), nil +} + +/* AmGetConferenceProperty retrieves the value of a conference property. + * Parameters: + * ctx - Standard Go context value. + * confid - The ID of the conference to get the property for. + * ndx - The index of the property to retrieve. + * Returns: + * Value of the property string. + * Standard Go error status. + */ +func AmGetConferenceProperty(ctx context.Context, confid int32, ndx int32) (*string, error) { + p, err := internalGetConfProp(ctx, confid, ndx) + if err != nil { + return nil, err + } else if p == nil { + return nil, nil + } + return p.Data, nil +} + +/* AmSetConferenceProperty sets the value of a conference property. + * Parameters: + * ctx - Standard Go context value. + * confid - The ID of the conference to set the property for. + * ndx - The index of the property to set. + * val - The new value of the property. + * Returns: + * Standard Go error status. + */ +func AmSetConferenceProperty(ctx context.Context, confid int32, ndx int32, val *string) error { + p, err := internalGetConfProp(ctx, confid, ndx) + if err != nil { + return err + } + getConferencePropMutex.Lock() + defer getConferencePropMutex.Unlock() + if p != nil { + _, err = amdb.ExecContext(ctx, "UPDATE propconf SET data = ? WHERE confid = ? AND ndx = ?", val, confid, ndx) + if err == nil { + p.Data = val + } + } else { + prop := ConferenceProperties{ConfId: confid, Index: ndx, Data: val} + _, err := amdb.NamedExecContext(ctx, "INSERT INTO propconf (confid, ndx, data) VALUES(:confid, :ndx, :data)", prop) + if err == nil { + conferencePropCache.Add(fmt.Sprintf("%d:%d", confid, ndx), prop) + } + } + return err +} diff --git a/errors.go b/errors.go index 5d58baf..fdc5359 100644 --- a/errors.go +++ b/errors.go @@ -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 @@ -10,6 +10,7 @@ package main import ( + "errors" "net/http" "git.erbosoft.com/amy/amsterdam/ui" @@ -17,6 +18,9 @@ import ( log "github.com/sirupsen/logrus" ) +// ENOPERM is the standard "not permitted" error message. +var ENOPERM error = errors.New("you are not permitted to perform this operation") + /* NotImplPage is used for all TODO links, to show that something hasn't yet been implemented. * Parameters: * ctxt - The AmContext for the request. diff --git a/main.go b/main.go index 023af9a..b02dd3b 100644 --- a/main.go +++ b/main.go @@ -110,6 +110,7 @@ func setupEcho() *echo.Echo { confGroup.GET("/manage", ui.AmWrap(ConfManage)) confGroup.POST("/pseud", ui.AmWrap(SetPseud)) confGroup.GET("/fixseen", ui.AmWrap(ConfFixseen)) + confGroup.GET("/edit", ui.AmWrap(EditConferenceForm)) confGroup.GET("/hotlist", ui.AmWrap(AddToHotlist)) confGroup.GET("/invite", ui.AmWrap(InviteToConference)) confGroup.GET("/r/:topic", ui.AmWrap(ReadPosts), ui.SetTopic) diff --git a/ui/dialog.go b/ui/dialog.go index 263e5b4..b58dc5a 100644 --- a/ui/dialog.go +++ b/ui/dialog.go @@ -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 @@ -238,6 +238,13 @@ func (d *Dialog) SetCommunity(comm *database.Community) { } } +// SetConference alters a dialog's content to reflect the conference. +func (d *Dialog) SetConference(conf *database.Conference, alias string) { + d.Title = strings.ReplaceAll(d.Title, "[CONFNAME]", conf.Name) + d.Subtitle = strings.ReplaceAll(d.Subtitle, "[CONFNAME]", conf.Name) + d.Action = strings.ReplaceAll(d.Action, "[CONFID]", alias) +} + /* Field returns a pointer to a dialog's field, given its name. * Parameters: * name - The name of the field to find. diff --git a/ui/dialogs/edit_conference.yaml b/ui/dialogs/edit_conference.yaml new file mode 100644 index 0000000..4e70129 --- /dev/null +++ b/ui/dialogs/edit_conference.yaml @@ -0,0 +1,96 @@ +# +# 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/. +# +# +# 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/. +# +name: "conf.edit" +formName: "editconfform" +menuSelector: "community" +title: "Edit Conference:" +subtitle: "[CONFNAME]" +action: "/comm/[CID]/conf/[CONFID]/edit" +fields: + - type: "header" + name: "header1" + caption: "Basic Information" + - type: "text" + name: "name" + caption: "Conference Name" + required: true + size: 32 + maxlength: 128 + - type: "text" + name: "descr" + caption: "Description" + required: false + size: 32 + maxlength: 255 + - type: "checkbox" + name: "hide" + caption: "Hide conference in the community's conference list" + - type: "header" + name: "header2" + caption: "Security Information" + - type: "rolelist" + name: "read_lvl" + caption: "Security level required to read conference" + required: true + param: "Conference.Read" + - type: "rolelist" + name: "post_lvl" + caption: "Security level required to post to conference" + required: true + param: "Conference.Post" + - type: "rolelist" + name: "create_lvl" + caption: "Security level required to create new topics in conference" + required: true + param: "Conference.Create" + - type: "rolelist" + name: "hide_lvl" + caption: "Security level required to archive or freeze topics" + subcaption: "(or to hide posts of which you are not the owner)" + required: true + param: "Conference.Hide" + - type: "rolelist" + name: "nuke_lvl" + caption: "Security level required to delete topics or nuke posts" + subcaption: "(or to scribble posts of which you are not the owner)" + required: true + param: "Conference.Nuke" + - type: "rolelist" + name: "change_lvl" + caption: "Security level required to change conference attributes" + required: true + param: "Conference.Change" + - type: "rolelist" + name: "delete_lvl" + caption: "Security level required to delete conference" + required: true + param: "Conference.Delete" + - type: "header" + name: "header3" + caption: "Conference Properties" + - type: "checkbox" + name: "pic_in_post" + caption: "Display users' pictures next to their posts" + subcaption: "(user can override)" + - type: "button" + name: "update" + caption: "Update" + param: "blue" + - type: "button" + name: "cancel" + caption: "Cancel" + param: "red" diff --git a/ui/menudefs.yaml b/ui/menudefs.yaml index d7f3e43..839d067 100644 --- a/ui/menudefs.yaml +++ b/ui/menudefs.yaml @@ -92,7 +92,7 @@ menudefs: conference. Use with care and review all changes before applying them to the conference. items: - text: "Change Conference Information" - link: "/TODO/comm/[CID]/conf/[CONFID]/info" + link: "/comm/[CID]/conf/[CONFID]/edit" - text: "Manage Conference Aliases" link: "/TODO/comm/[CID]/conf/[CONFID]/aliases" - text: "Manage Conference Members" diff --git a/ui/views/dialog.jet b/ui/views/dialog.jet index e957478..7d4a101 100644 --- a/ui/views/dialog.jet +++ b/ui/views/dialog.jet @@ -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,12 @@
-

{{ amsterdam_dialog.Title }}

- {{ if amsterdam_dialog.Subtitle != "" }} - {{ amsterdam_dialog.Subtitle }} - {{ end }} +
+

{{ amsterdam_dialog.Title }}

+ {{ if amsterdam_dialog.Subtitle != "" }} + {{ amsterdam_dialog.Subtitle }} + {{ end }} +