diff --git a/conference.go b/conference.go index c719e5c..40ac4e2 100644 --- a/conference.go +++ b/conference.go @@ -291,7 +291,7 @@ func NewTopic(ctxt ui.AmContext) (string, any, error) { lines, _ := checker.Lines() // 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 { return ui.ErrorPage(ctxt, err) } diff --git a/database/audit.go b/database/audit.go index a6b148b..c5a3cc3 100644 --- a/database/audit.go +++ b/database/audit.go @@ -32,32 +32,49 @@ type AuditRecord struct { // These are the audit record types. const ( - AuditPublishToFrontPage = 1 - AuditLoginOK = 101 - AuditLoginFail = 102 - AuditAccountCreated = 103 - AuditVerifyEmailOK = 104 - AuditVerifyEmailFail = 105 - AuditSetUserContactInfo = 106 - AuditResendEmailConfirm = 107 - AuditChangePassword = 108 - AuditAdminSetUserContactInfo = 109 - AuditAdminChangeUserPassword = 110 - AuditAdminChangeUserAccount = 111 - AuditAdminSetAccountSecurity = 112 - AuditAdminLockUnlockAccount = 113 - AuditCommunityCreate = 201 - AuditCommunitySetMembership = 202 - AuditCommuntiyContactInfo = 203 - AuditCommunityFeatureSet = 204 - AuditCommunityName = 205 - AuditCommunityAlias = 206 - AuditCommunityCategory = 207 - AuditCommunityHideInfo = 208 - AuditCommunityMembersOnly = 209 - AuditCommunityJoinKey = 210 - AuditCommunitySecurity = 211 - AuditCommunityDelete = 212 + AuditPublishToFrontPage = 1 + AuditLoginOK = 101 + AuditLoginFail = 102 + AuditAccountCreated = 103 + AuditVerifyEmailOK = 104 + AuditVerifyEmailFail = 105 + AuditSetUserContactInfo = 106 + AuditResendEmailConfirm = 107 + AuditChangePassword = 108 + AuditAdminSetUserContactInfo = 109 + AuditAdminChangeUserPassword = 110 + AuditAdminChangeUserAccount = 111 + AuditAdminSetAccountSecurity = 112 + AuditAdminLockUnlockAccount = 113 + AuditCommunityCreate = 201 + AuditCommunitySetMembership = 202 + AuditCommuntiyContactInfo = 203 + AuditCommunityFeatureSet = 204 + AuditCommunityName = 205 + AuditCommunityAlias = 206 + AuditCommunityCategory = 207 + AuditCommunityHideInfo = 208 + AuditCommunityMembersOnly = 209 + AuditCommunityJoinKey = 210 + AuditCommunitySecurity = 211 + 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. diff --git a/database/conference.go b/database/conference.go index dab7909..61e0caf 100644 --- a/database/conference.go +++ b/database/conference.go @@ -21,29 +21,29 @@ import ( // Conference struct is the top-level structure for a conference. type Conference struct { Mutex sync.Mutex - ConfId int32 `db:"confid"` - CreateDate time.Time `db:"createdate"` - LastUpdate *time.Time `db:"lastupdate"` - ReadLevel uint16 `db:"read_lvl"` - PostLevel uint16 `db:"post_lvl"` - CreateLevel uint16 `db:"create_lvl"` - HideLevel uint16 `db:"hide_lvl"` - NukeLevel uint16 `db:"nuke_lvl"` - ChangeLevel uint16 `db:"change_lvl"` - DeleteLevel uint16 `db:"delete_lvl"` - TopTopic int16 `db:"top_topic"` - Name string `db:"name"` - Description *string `db:"descr"` - IconUrl *string `db:"icon_url"` - Color *string `db:"color"` + ConfId int32 `db:"confid"` // unique conference ID + CreateDate time.Time `db:"createdate"` // date of creation + LastUpdate *time.Time `db:"lastupdate"` // date of last update + ReadLevel uint16 `db:"read_lvl"` // level required to read + PostLevel uint16 `db:"post_lvl"` // level required to post + CreateLevel uint16 `db:"create_lvl"` // level required to create topics + HideLevel uint16 `db:"hide_lvl"` // level required to hide posts + NukeLevel uint16 `db:"nuke_lvl"` // level required to nuke posts + ChangeLevel uint16 `db:"change_lvl"` // level required to change conference + DeleteLevel uint16 `db:"delete_lvl"` // level required to delete conference + TopTopic int16 `db:"top_topic"` // highest topic number in use + Name string `db:"name"` // conference name + Description *string `db:"descr"` // conference description + IconUrl *string `db:"icon_url"` // conference icon URL + Color *string `db:"color"` // color for conference } type ConferenceSettings struct { - ConfId int32 `db:"confid"` - Uid int32 `db:"uid"` - DefaultPseud *string `db:"default_pseud"` - LastRead *time.Time `db:"last_read"` - LastPost *time.Time `db:"last_post"` + ConfId int32 `db:"confid"` // conference ID + Uid int32 `db:"uid"` // user ID + DefaultPseud *string `db:"default_pseud"` // default pseud to use in this conference + LastRead *time.Time `db:"last_read"` // last read time + LastPost *time.Time `db:"last_post"` // last post time } // conferenceCache is the cache for Conference objects. diff --git a/database/topic.go b/database/topic.go index 668fcd9..5391517 100644 --- a/database/topic.go +++ b/database/topic.go @@ -18,28 +18,28 @@ import ( // Topic is the top-level structure detailing topics. type Topic struct { - TopicId int32 `db:"topicid"` - ConfId int32 `db:"confid"` - Number int16 `db:"num"` - CreatorUid int32 `db:"creator_uid"` - TopMessage int32 `db:"top_message"` - Frozen bool `db:"frozen"` - Archived bool `db:"archived"` - Sticky bool `db:"sticky"` - CreateDate time.Time `db:"createdate"` - LastUpdate time.Time `db:"lastupdate"` - Name string `db:"name"` + TopicId int32 `db:"topicid"` // unique ID of the topic + ConfId int32 `db:"confid"` // conference this topic is in + Number int16 `db:"num"` // topic number + CreatorUid int32 `db:"creator_uid"` // UID of topic creator + TopMessage int32 `db:"top_message"` // highest message number in topic + Frozen bool `db:"frozen"` // frozen topic + Archived bool `db:"archived"` // archived topic + Sticky bool `db:"sticky"` // sticky topic + CreateDate time.Time `db:"createdate"` // creation date + LastUpdate time.Time `db:"lastupdate"` // last update date + Name string `db:"name"` // topic name } // TopicSettings contains per-user settings for topics, including the "last read" message pointer. type TopicSettings struct { - TopicId int32 `db:"topicid"` - Uid int32 `db:"uid"` - Hidden bool `db:"hidden"` - LastMessage int32 `db:"last_message"` - LastRead *time.Time `db:"last_read"` - LastPost *time.Time `db:"last_post"` - Subscribe bool `db:"subscribe"` + TopicId int32 `db:"topicid"` // unique ID of the topic + Uid int32 `db:"uid"` // UID of the user + Hidden bool `db:"hidden"` // has user hidden topic? + LastMessage int32 `db:"last_message"` // last message read + LastRead *time.Time `db:"last_read"` // time of last read + LastPost *time.Time `db:"last_post"` // time of last post + Subscribe bool `db:"subscribe"` // subscribed to topic updates? } // 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 Archived bool Subscribed bool + Hidden bool } func AmGetTopic(topicId int32) (*Topic, error) { @@ -72,19 +73,19 @@ func AmGetTopic(topicId int32) (*Topic, error) { // View and sort constants for AmListTopics. const ( - TopicViewAll = 0 - TopicViewNew = 1 - TopicViewActive = 2 - TopicViewAllVisible = 3 - TopicViewHidden = 4 - TopicViewArchive = 5 + TopicViewAll = 0 // list all topics + TopicViewNew = 1 // list only visible topics with new messages + TopicViewActive = 2 // list only visible topics, active first + TopicViewAllVisible = 3 // list only visible topics + TopicViewHidden = 4 // list only hidden topics + TopicViewArchive = 5 // list only archived, non-hidden topics - TopicSortID = 0 - TopicSortNumber = 1 - TopicSortName = 2 - TopicSortUnread = 3 - TopicSortTotal = 4 - TopicSortDate = 5 + TopicSortID = 0 // sort by topic ID + TopicSortNumber = 1 // sort by topic number + TopicSortName = 2 // sort by name + TopicSortUnread = 3 // sort by number of unread messages + TopicSortTotal = 4 // sort by total number of messages + TopicSortDate = 5 // sort by date of last update ) /* 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 { 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: - whereClause = "t.archived = 0 AND IFNULL(s.hidden,0) = 0" + whereClause = "t.archived = 0 AND hidden = 0" case TopicViewHidden: - whereClause = "IFNULL(s.hidden,0) = 1" + whereClause = "hidden = 1" case TopicViewArchive: - whereClause = "t.archived = 1 AND IFNULL(s.hidden,0) = 0" + whereClause = "t.archived = 1 AND hidden = 0" default: 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 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.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 = ? ") if whereClause != "" { fullStatement.WriteString("AND ") fullStatement.WriteString(whereClause) } fullStatement.WriteString(" ORDER BY ") - if ignoreSticky { + if !ignoreSticky { fullStatement.WriteString("t.sticky DESC, ") } if viewOption == TopicViewActive { @@ -204,13 +205,19 @@ func AmListTopics(confid int32, uid int32, viewOption int, sortOption int, ignor for rs.Next() { var rec TopicSummary 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) } 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 amdb.Exec("LOCK TABLES confs WRITE, topics WRITE, topicsettings WRITE, posts WRITE, postdata WRITE;") defer func() { @@ -227,11 +234,13 @@ func AmNewTopic(conf *Conference, user *User, title string, zeroPostPseud string conf.Mutex.Unlock() return nil, err } + // Retrieve the ID of the new topic. xid, err := rs.LastInsertId() if err != nil { conf.Mutex.Unlock() return nil, err } + // Get the topic. topic, err := AmGetTopic(int32(xid)) if err != nil { conf.Mutex.Unlock() @@ -258,10 +267,8 @@ func AmNewTopic(conf *Conference, user *User, title string, zeroPostPseud string if err != nil { return nil, err } - newPostId := int32(xid) - // 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 { return nil, err } @@ -276,7 +283,9 @@ func AmNewTopic(conf *Conference, user *User, title string, zeroPostPseud string amdb.Exec("UNLOCK TABLES;") 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 } diff --git a/database/user.go b/database/user.go index ad489ac..c86163a 100644 --- a/database/user.go +++ b/database/user.go @@ -30,9 +30,9 @@ import ( // UserPrefs represents the user's preferences in a table (one row per user). type UserPrefs struct { - Uid int32 `db:"uid"` - TimeZoneID string `db:"tzid"` - LocaleID string `db:"localeid"` + Uid int32 `db:"uid"` // user ID + TimeZoneID string `db:"tzid"` // ID of default timezone + LocaleID string `db:"localeid"` // ID of default locale } // 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. type User struct { Mutex sync.RWMutex - Uid int32 `db:"uid"` - Username string `db:"username"` - Passhash string `db:"passhash"` - Tokenauth *string `db:"tokenauth"` - ContactID int32 `db:"contactid"` - IsAnon bool `db:"is_anon"` - VerifyEMail bool `db:"verify_email"` - Lockout bool `db:"lockout"` - AccessTries int16 `db:"access_tries"` - EmailConfNum int32 `db:"email_confnum"` - BaseLevel uint16 `db:"base_lvl"` - Created time.Time `db:"created"` - LastAccess *time.Time `db:"lastaccess"` - PassReminder string `db:"passreminder"` - Description *string `db:"description"` - DOB *time.Time `db:"dob"` + Uid int32 `db:"uid"` // unique ID of user + Username string `db:"username"` // user name + Passhash string `db:"passhash"` // password hash + Tokenauth *string `db:"tokenauth"` // token authorization information + ContactID int32 `db:"contactid"` // contact information ID + IsAnon bool `db:"is_anon"` // is this the anonymous user? + VerifyEMail bool `db:"verify_email"` // is E-mail address verified? + Lockout bool `db:"lockout"` // is this user locked out? + AccessTries int16 `db:"access_tries"` // how many timews has the user tried to access? + EmailConfNum int32 `db:"email_confnum"` // E-mail confirmation number + BaseLevel uint16 `db:"base_lvl"` // base access level of the user + Created time.Time `db:"created"` // account creation time + LastAccess *time.Time `db:"lastaccess"` // last access (login) time + PassReminder string `db:"passreminder"` // last update time + Description *string `db:"description"` // description + DOB *time.Time `db:"dob"` // date of birth flags *util.OptionSet prefs *UserPrefs }