added more add topic code and some documentation

This commit is contained in:
2025-11-07 21:36:04 -07:00
parent 161e51a640
commit 5dbff8b4b3
5 changed files with 134 additions and 108 deletions
+1 -1
View File
@@ -291,7 +291,7 @@ func NewTopic(ctxt ui.AmContext) (string, any, error) {
lines, _ := checker.Lines() lines, _ := checker.Lines()
// Add the topic! // Add the topic!
topic, err := database.AmNewTopic(conf, ctxt.CurrentUser(), topicName, zeroPostPseud, zeroPost, int32(lines)) topic, err := database.AmNewTopic(conf, ctxt.CurrentUser(), topicName, zeroPostPseud, zeroPost, int32(lines), ctxt.RemoteIP())
if err != nil { if err != nil {
return ui.ErrorPage(ctxt, err) return ui.ErrorPage(ctxt, err)
} }
+43 -26
View File
@@ -32,32 +32,49 @@ type AuditRecord struct {
// These are the audit record types. // These are the audit record types.
const ( const (
AuditPublishToFrontPage = 1 AuditPublishToFrontPage = 1
AuditLoginOK = 101 AuditLoginOK = 101
AuditLoginFail = 102 AuditLoginFail = 102
AuditAccountCreated = 103 AuditAccountCreated = 103
AuditVerifyEmailOK = 104 AuditVerifyEmailOK = 104
AuditVerifyEmailFail = 105 AuditVerifyEmailFail = 105
AuditSetUserContactInfo = 106 AuditSetUserContactInfo = 106
AuditResendEmailConfirm = 107 AuditResendEmailConfirm = 107
AuditChangePassword = 108 AuditChangePassword = 108
AuditAdminSetUserContactInfo = 109 AuditAdminSetUserContactInfo = 109
AuditAdminChangeUserPassword = 110 AuditAdminChangeUserPassword = 110
AuditAdminChangeUserAccount = 111 AuditAdminChangeUserAccount = 111
AuditAdminSetAccountSecurity = 112 AuditAdminSetAccountSecurity = 112
AuditAdminLockUnlockAccount = 113 AuditAdminLockUnlockAccount = 113
AuditCommunityCreate = 201 AuditCommunityCreate = 201
AuditCommunitySetMembership = 202 AuditCommunitySetMembership = 202
AuditCommuntiyContactInfo = 203 AuditCommuntiyContactInfo = 203
AuditCommunityFeatureSet = 204 AuditCommunityFeatureSet = 204
AuditCommunityName = 205 AuditCommunityName = 205
AuditCommunityAlias = 206 AuditCommunityAlias = 206
AuditCommunityCategory = 207 AuditCommunityCategory = 207
AuditCommunityHideInfo = 208 AuditCommunityHideInfo = 208
AuditCommunityMembersOnly = 209 AuditCommunityMembersOnly = 209
AuditCommunityJoinKey = 210 AuditCommunityJoinKey = 210
AuditCommunitySecurity = 211 AuditCommunitySecurity = 211
AuditCommunityDelete = 212 AuditCommunityDelete = 212
AuditConferenceCreate = 301
AuditConferenceSecurity = 302
AuditConferenceName = 303
AuditConferenceAlias = 304
AuditConferenceMembership = 305
AuditConferenceCreateTopic = 306
AuditConferenceDeleteTopic = 307
AudotConferenceFreezeTopic = 308
AuditConferenceArchiveTopic = 309
AuditConferencePostMessage = 310
AuditConferenceHideMessage = 311
AuditConferenceScribbleMessage = 312
AuditConferenceNukeMessage = 313
AuditConferenceUploadAttachment = 314
AuditConferenceDelete = 315
AuditConferenceMoveMessage = 316
AuditConferenceStickyTopic = 317
) )
// auditWriteQueue is a channel to store audit records in the background. // auditWriteQueue is a channel to store audit records in the background.
+20 -20
View File
@@ -21,29 +21,29 @@ import (
// Conference struct is the top-level structure for a conference. // Conference struct is the top-level structure for a conference.
type Conference struct { type Conference struct {
Mutex sync.Mutex Mutex sync.Mutex
ConfId int32 `db:"confid"` ConfId int32 `db:"confid"` // unique conference ID
CreateDate time.Time `db:"createdate"` CreateDate time.Time `db:"createdate"` // date of creation
LastUpdate *time.Time `db:"lastupdate"` LastUpdate *time.Time `db:"lastupdate"` // date of last update
ReadLevel uint16 `db:"read_lvl"` ReadLevel uint16 `db:"read_lvl"` // level required to read
PostLevel uint16 `db:"post_lvl"` PostLevel uint16 `db:"post_lvl"` // level required to post
CreateLevel uint16 `db:"create_lvl"` CreateLevel uint16 `db:"create_lvl"` // level required to create topics
HideLevel uint16 `db:"hide_lvl"` HideLevel uint16 `db:"hide_lvl"` // level required to hide posts
NukeLevel uint16 `db:"nuke_lvl"` NukeLevel uint16 `db:"nuke_lvl"` // level required to nuke posts
ChangeLevel uint16 `db:"change_lvl"` ChangeLevel uint16 `db:"change_lvl"` // level required to change conference
DeleteLevel uint16 `db:"delete_lvl"` DeleteLevel uint16 `db:"delete_lvl"` // level required to delete conference
TopTopic int16 `db:"top_topic"` TopTopic int16 `db:"top_topic"` // highest topic number in use
Name string `db:"name"` Name string `db:"name"` // conference name
Description *string `db:"descr"` Description *string `db:"descr"` // conference description
IconUrl *string `db:"icon_url"` IconUrl *string `db:"icon_url"` // conference icon URL
Color *string `db:"color"` Color *string `db:"color"` // color for conference
} }
type ConferenceSettings struct { type ConferenceSettings struct {
ConfId int32 `db:"confid"` ConfId int32 `db:"confid"` // conference ID
Uid int32 `db:"uid"` Uid int32 `db:"uid"` // user ID
DefaultPseud *string `db:"default_pseud"` DefaultPseud *string `db:"default_pseud"` // default pseud to use in this conference
LastRead *time.Time `db:"last_read"` LastRead *time.Time `db:"last_read"` // last read time
LastPost *time.Time `db:"last_post"` LastPost *time.Time `db:"last_post"` // last post time
} }
// conferenceCache is the cache for Conference objects. // conferenceCache is the cache for Conference objects.
+51 -42
View File
@@ -18,28 +18,28 @@ import (
// Topic is the top-level structure detailing topics. // Topic is the top-level structure detailing topics.
type Topic struct { type Topic struct {
TopicId int32 `db:"topicid"` TopicId int32 `db:"topicid"` // unique ID of the topic
ConfId int32 `db:"confid"` ConfId int32 `db:"confid"` // conference this topic is in
Number int16 `db:"num"` Number int16 `db:"num"` // topic number
CreatorUid int32 `db:"creator_uid"` CreatorUid int32 `db:"creator_uid"` // UID of topic creator
TopMessage int32 `db:"top_message"` TopMessage int32 `db:"top_message"` // highest message number in topic
Frozen bool `db:"frozen"` Frozen bool `db:"frozen"` // frozen topic
Archived bool `db:"archived"` Archived bool `db:"archived"` // archived topic
Sticky bool `db:"sticky"` Sticky bool `db:"sticky"` // sticky topic
CreateDate time.Time `db:"createdate"` CreateDate time.Time `db:"createdate"` // creation date
LastUpdate time.Time `db:"lastupdate"` LastUpdate time.Time `db:"lastupdate"` // last update date
Name string `db:"name"` Name string `db:"name"` // topic name
} }
// TopicSettings contains per-user settings for topics, including the "last read" message pointer. // TopicSettings contains per-user settings for topics, including the "last read" message pointer.
type TopicSettings struct { type TopicSettings struct {
TopicId int32 `db:"topicid"` TopicId int32 `db:"topicid"` // unique ID of the topic
Uid int32 `db:"uid"` Uid int32 `db:"uid"` // UID of the user
Hidden bool `db:"hidden"` Hidden bool `db:"hidden"` // has user hidden topic?
LastMessage int32 `db:"last_message"` LastMessage int32 `db:"last_message"` // last message read
LastRead *time.Time `db:"last_read"` LastRead *time.Time `db:"last_read"` // time of last read
LastPost *time.Time `db:"last_post"` LastPost *time.Time `db:"last_post"` // time of last post
Subscribe bool `db:"subscribe"` Subscribe bool `db:"subscribe"` // subscribed to topic updates?
} }
// TopicSummary is a smaller data structure that gets topic information to create the topic list display. // TopicSummary is a smaller data structure that gets topic information to create the topic list display.
@@ -53,6 +53,7 @@ type TopicSummary struct {
Frozen bool Frozen bool
Archived bool Archived bool
Subscribed bool Subscribed bool
Hidden bool
} }
func AmGetTopic(topicId int32) (*Topic, error) { func AmGetTopic(topicId int32) (*Topic, error) {
@@ -72,19 +73,19 @@ func AmGetTopic(topicId int32) (*Topic, error) {
// View and sort constants for AmListTopics. // View and sort constants for AmListTopics.
const ( const (
TopicViewAll = 0 TopicViewAll = 0 // list all topics
TopicViewNew = 1 TopicViewNew = 1 // list only visible topics with new messages
TopicViewActive = 2 TopicViewActive = 2 // list only visible topics, active first
TopicViewAllVisible = 3 TopicViewAllVisible = 3 // list only visible topics
TopicViewHidden = 4 TopicViewHidden = 4 // list only hidden topics
TopicViewArchive = 5 TopicViewArchive = 5 // list only archived, non-hidden topics
TopicSortID = 0 TopicSortID = 0 // sort by topic ID
TopicSortNumber = 1 TopicSortNumber = 1 // sort by topic number
TopicSortName = 2 TopicSortName = 2 // sort by name
TopicSortUnread = 3 TopicSortUnread = 3 // sort by number of unread messages
TopicSortTotal = 4 TopicSortTotal = 4 // sort by total number of messages
TopicSortDate = 5 TopicSortDate = 5 // sort by date of last update
) )
/* AmListTopics produces a list of topic summary information according to specific options. /* AmListTopics produces a list of topic summary information according to specific options.
@@ -121,13 +122,13 @@ func AmListTopics(confid int32, uid int32, viewOption int, sortOption int, ignor
if !ignoreSticky { if !ignoreSticky {
tail = "(t.sticky = 1 OR " + tail + ")" tail = "(t.sticky = 1 OR " + tail + ")"
} }
whereClause = "t.archived = 0 AND IFNULL(s.hidden,0) = 0 AND " + tail whereClause = "t.archived = 0 AND hidden = 0 AND " + tail
case TopicViewActive, TopicViewAllVisible: case TopicViewActive, TopicViewAllVisible:
whereClause = "t.archived = 0 AND IFNULL(s.hidden,0) = 0" whereClause = "t.archived = 0 AND hidden = 0"
case TopicViewHidden: case TopicViewHidden:
whereClause = "IFNULL(s.hidden,0) = 1" whereClause = "hidden = 1"
case TopicViewArchive: case TopicViewArchive:
whereClause = "t.archived = 1 AND IFNULL(s.hidden,0) = 0" whereClause = "t.archived = 1 AND hidden = 0"
default: default:
return nil, errors.New("invalid view option specified") return nil, errors.New("invalid view option specified")
} }
@@ -180,14 +181,14 @@ func AmListTopics(confid int32, uid int32, viewOption int, sortOption int, ignor
var fullStatement strings.Builder var fullStatement strings.Builder
fullStatement.WriteString("SELECT t.topicid, t.num, t.name, (t.top_message - IFNULL(s.last_message,-1)) AS unread, ") fullStatement.WriteString("SELECT t.topicid, t.num, t.name, (t.top_message - IFNULL(s.last_message,-1)) AS unread, ")
fullStatement.WriteString("(t.top_message + 1) AS total, t.lastupdate, t.frozen, t.archived, IFNULL(s.subscribe,0) AS subscribe, ") fullStatement.WriteString("(t.top_message + 1) AS total, t.lastupdate, t.frozen, t.archived, IFNULL(s.subscribe,0) AS subscribe, ")
fullStatement.WriteString("t.sticky, GREATEST(SIGN(t.top_message - IFNULL(s.last_message,-1)),0) AS newflag ") fullStatement.WriteString("IFNULL(s.hidden,0) AS hidden, t.sticky, GREATEST(SIGN(t.top_message - IFNULL(s.last_message,-1)),0) AS newflag ")
fullStatement.WriteString("FROM topics t LEFT JOIN topicsettings s ON t.topicid = s.topicid AND s.uid = ? WHERE t.confid = ? ") fullStatement.WriteString("FROM topics t LEFT JOIN topicsettings s ON t.topicid = s.topicid AND s.uid = ? WHERE t.confid = ? ")
if whereClause != "" { if whereClause != "" {
fullStatement.WriteString("AND ") fullStatement.WriteString("AND ")
fullStatement.WriteString(whereClause) fullStatement.WriteString(whereClause)
} }
fullStatement.WriteString(" ORDER BY ") fullStatement.WriteString(" ORDER BY ")
if ignoreSticky { if !ignoreSticky {
fullStatement.WriteString("t.sticky DESC, ") fullStatement.WriteString("t.sticky DESC, ")
} }
if viewOption == TopicViewActive { if viewOption == TopicViewActive {
@@ -204,13 +205,19 @@ func AmListTopics(confid int32, uid int32, viewOption int, sortOption int, ignor
for rs.Next() { for rs.Next() {
var rec TopicSummary var rec TopicSummary
rs.Scan(&rec.TopicID, &rec.Number, &rec.Name, &rec.Unread, &rec.Total, &rec.LastUpdate, &rec.Frozen, &rec.Archived, rs.Scan(&rec.TopicID, &rec.Number, &rec.Name, &rec.Unread, &rec.Total, &rec.LastUpdate, &rec.Frozen, &rec.Archived,
&rec.Subscribed) &rec.Subscribed, &rec.Hidden)
rc = append(rc, &rec) rc = append(rc, &rec)
} }
return rc, nil return rc, nil
} }
func AmNewTopic(conf *Conference, user *User, title string, zeroPostPseud string, zeroPost string, zeroPostLines int32) (*Topic, error) { func AmNewTopic(conf *Conference, user *User, title string, zeroPostPseud string, zeroPost string,
zeroPostLines int32, ipaddr string) (*Topic, error) {
var ar *AuditRecord = nil
defer func() {
AmStoreAudit(ar)
}()
unlock := true unlock := true
amdb.Exec("LOCK TABLES confs WRITE, topics WRITE, topicsettings WRITE, posts WRITE, postdata WRITE;") amdb.Exec("LOCK TABLES confs WRITE, topics WRITE, topicsettings WRITE, posts WRITE, postdata WRITE;")
defer func() { defer func() {
@@ -227,11 +234,13 @@ func AmNewTopic(conf *Conference, user *User, title string, zeroPostPseud string
conf.Mutex.Unlock() conf.Mutex.Unlock()
return nil, err return nil, err
} }
// Retrieve the ID of the new topic.
xid, err := rs.LastInsertId() xid, err := rs.LastInsertId()
if err != nil { if err != nil {
conf.Mutex.Unlock() conf.Mutex.Unlock()
return nil, err return nil, err
} }
// Get the topic.
topic, err := AmGetTopic(int32(xid)) topic, err := AmGetTopic(int32(xid))
if err != nil { if err != nil {
conf.Mutex.Unlock() conf.Mutex.Unlock()
@@ -258,10 +267,8 @@ func AmNewTopic(conf *Conference, user *User, title string, zeroPostPseud string
if err != nil { if err != nil {
return nil, err return nil, err
} }
newPostId := int32(xid)
// Add the post data. // Add the post data.
_, err = amdb.Exec("INSERT INTO postdata (postid, data) VALUES (?, ?)", newPostId, zeroPost) _, err = amdb.Exec("INSERT INTO postdata (postid, data) VALUES (?, ?)", int32(xid), zeroPost)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -276,7 +283,9 @@ func AmNewTopic(conf *Conference, user *User, title string, zeroPostPseud string
amdb.Exec("UNLOCK TABLES;") amdb.Exec("UNLOCK TABLES;")
unlock = false unlock = false
// TODO: audit record // create audit record
ar = AmNewAudit(AuditConferenceCreateTopic, user.Uid, ipaddr, fmt.Sprintf("confid=%d", conf.ConfId),
fmt.Sprintf("num=%d", topic.Number), fmt.Sprintf("name=%s", topic.Name))
return topic, nil return topic, nil
} }
+19 -19
View File
@@ -30,9 +30,9 @@ import (
// UserPrefs represents the user's preferences in a table (one row per user). // UserPrefs represents the user's preferences in a table (one row per user).
type UserPrefs struct { type UserPrefs struct {
Uid int32 `db:"uid"` Uid int32 `db:"uid"` // user ID
TimeZoneID string `db:"tzid"` TimeZoneID string `db:"tzid"` // ID of default timezone
LocaleID string `db:"localeid"` LocaleID string `db:"localeid"` // ID of default locale
} }
// ReadLocale reads the locale out of the prefs, adjusting for Go use. // ReadLocale reads the locale out of the prefs, adjusting for Go use.
@@ -100,22 +100,22 @@ func (p *UserPrefs) Location() *time.Location {
// User represents a user in the Amsterdam database. // User represents a user in the Amsterdam database.
type User struct { type User struct {
Mutex sync.RWMutex Mutex sync.RWMutex
Uid int32 `db:"uid"` Uid int32 `db:"uid"` // unique ID of user
Username string `db:"username"` Username string `db:"username"` // user name
Passhash string `db:"passhash"` Passhash string `db:"passhash"` // password hash
Tokenauth *string `db:"tokenauth"` Tokenauth *string `db:"tokenauth"` // token authorization information
ContactID int32 `db:"contactid"` ContactID int32 `db:"contactid"` // contact information ID
IsAnon bool `db:"is_anon"` IsAnon bool `db:"is_anon"` // is this the anonymous user?
VerifyEMail bool `db:"verify_email"` VerifyEMail bool `db:"verify_email"` // is E-mail address verified?
Lockout bool `db:"lockout"` Lockout bool `db:"lockout"` // is this user locked out?
AccessTries int16 `db:"access_tries"` AccessTries int16 `db:"access_tries"` // how many timews has the user tried to access?
EmailConfNum int32 `db:"email_confnum"` EmailConfNum int32 `db:"email_confnum"` // E-mail confirmation number
BaseLevel uint16 `db:"base_lvl"` BaseLevel uint16 `db:"base_lvl"` // base access level of the user
Created time.Time `db:"created"` Created time.Time `db:"created"` // account creation time
LastAccess *time.Time `db:"lastaccess"` LastAccess *time.Time `db:"lastaccess"` // last access (login) time
PassReminder string `db:"passreminder"` PassReminder string `db:"passreminder"` // last update time
Description *string `db:"description"` Description *string `db:"description"` // description
DOB *time.Time `db:"dob"` DOB *time.Time `db:"dob"` // date of birth
flags *util.OptionSet flags *util.OptionSet
prefs *UserPrefs prefs *UserPrefs
} }