func (rb *RoomBinding) GetMessage(ctx scope.Context, id snowflake.Snowflake) (*proto.Message, error) { var msg Message nDays, err := rb.DbMap.SelectInt("SELECT retention_days FROM room WHERE name = $1", rb.Name) if err != nil { return nil, err } err = rb.DbMap.SelectOne( &msg, "SELECT room, id, previous_edit_id, parent, posted, edited, deleted,"+ " session_id, sender_id, sender_name, server_id, server_era, content, encryption_key_id"+ " FROM message WHERE room = $1 AND id = $2", rb.Name, id.String()) if err != nil { if err == sql.ErrNoRows { return nil, proto.ErrMessageNotFound } return nil, err } if nDays > 0 { threshold := time.Now().Add(time.Duration(-nDays) * 24 * time.Hour) if msg.Posted.Before(threshold) { return nil, proto.ErrMessageNotFound } } m := msg.ToBackend() return &m, nil }
func (b *AccountManagerBinding) SetUserKey( ctx scope.Context, accountID snowflake.Snowflake, key *security.ManagedKey) error { if !key.Encrypted() { return security.ErrKeyMustBeEncrypted } res, err := b.DbMap.Exec( "UPDATE account SET encrypted_user_key = $2 WHERE id = $1", accountID.String(), key.Ciphertext) if err != nil { if err == sql.ErrNoRows { return proto.ErrAccountNotFound } return err } n, err := res.RowsAffected() if err != nil { return err } if n == 0 { return proto.ErrAccountNotFound } return nil }
func (b *AccountManagerBinding) getOTP(db gorp.SqlExecutor, kms security.KMS, accountID snowflake.Snowflake) (*proto.OTP, error) { encryptedOTP, err := b.getRawOTP(db, accountID) if err != nil { return nil, err } key := security.ManagedKey{ KeyType: OTPKeyType, IV: encryptedOTP.IV, Ciphertext: encryptedOTP.EncryptedKey, ContextKey: "account", ContextValue: accountID.String(), } if err := kms.DecryptKey(&key); err != nil { return nil, err } uriBytes, err := security.DecryptGCM(&key, encryptedOTP.IV, encryptedOTP.Digest, encryptedOTP.EncryptedURI, nil) if err != nil { return nil, err } otp := &proto.OTP{ URI: string(uriBytes), Validated: encryptedOTP.Validated, } return otp, nil }
func (b *AccountManagerBinding) RevokeStaff(ctx scope.Context, accountID snowflake.Snowflake) error { _, err := b.DbMap.Exec( "DELETE FROM capability USING account"+ " WHERE account.id = $1 AND capability.id = account.staff_capability_id", accountID.String()) return err }
func (m *accountManager) GrantStaff( ctx scope.Context, accountID snowflake.Snowflake, kmsCred security.KMSCredential) error { m.b.Lock() defer m.b.Unlock() account, ok := m.b.accounts[accountID.String()] if !ok { return proto.ErrAccountNotFound } memAcc := account.(*memAccount) kms := kmsCred.KMS() key := memAcc.sec.SystemKey.Clone() if err := kms.DecryptKey(&key); err != nil { return err } nonce, err := kms.GenerateNonce(key.KeyType.BlockSize()) if err != nil { return err } capability, err := security.GrantSharedSecretCapability(&key, nonce, kmsCred.KMSType(), kmsCred) if err != nil { return err } memAcc.staffCapability = capability return nil }
func NewMessage( roomName string, sessionView proto.SessionView, id, parent snowflake.Snowflake, keyID, content string) ( *Message, error) { msg := &Message{ Room: roomName, ID: id.String(), Parent: parent.String(), Posted: id.Time(), Content: content, SessionID: sessionView.SessionID, SenderID: string(sessionView.ID), SenderName: sessionView.Name, ServerID: sessionView.ServerID, ServerEra: sessionView.ServerEra, SenderClientAddress: sessionView.ClientAddress, SenderIsManager: sessionView.IsManager, SenderIsStaff: sessionView.IsStaff, } if keyID != "" { msg.EncryptionKeyID = sql.NullString{ String: keyID, Valid: true, } } return msg, nil }
func (t *PMTracker) Room(ctx scope.Context, kms security.KMS, pmID snowflake.Snowflake, client *proto.Client) (proto.Room, *security.ManagedKey, error) { row, err := t.Backend.Get(PM{}, pmID.String()) if row == nil || err != nil { if row == nil || err == sql.ErrNoRows { return nil, nil, proto.ErrPMNotFound } } pm := row.(*PM).ToBackend() pmKey, modified, otherName, err := pm.Access(ctx, t.Backend, kms, client) if err != nil { return nil, nil, err } if modified { _, err := t.Backend.DbMap.Exec( "UPDATE pm SET receiver = $2, receiver_mac = $3, encrypted_receiver_key = $4 WHERE id = $1", pm.ID.String(), string(pm.Receiver), pm.ReceiverMAC, pm.EncryptedReceiverKey.Ciphertext) if err != nil { return nil, nil, err } } room := &PMRoomBinding{ RoomBinding: RoomBinding{ RoomName: fmt.Sprintf("pm:%s", pm.ID), RoomTitle: fmt.Sprintf("%s (private chat)", otherName), Backend: t.Backend, }, pm: pm, } return room, pmKey, nil }
func NewMessage( room *Room, sessionView *proto.SessionView, id, parent snowflake.Snowflake, keyID, content string) ( *Message, error) { msg := &Message{ Room: room.Name, ID: id.String(), Parent: parent.String(), Posted: id.Time(), Content: content, } if sessionView != nil { msg.SessionID = sessionView.SessionID msg.SenderID = string(sessionView.ID) msg.SenderName = sessionView.Name msg.ServerID = sessionView.ServerID msg.ServerEra = sessionView.ServerEra msg.SenderIsManager = sessionView.IsManager msg.SenderIsStaff = sessionView.IsStaff } if keyID != "" { msg.EncryptionKeyID = sql.NullString{ String: keyID, Valid: true, } } return msg, nil }
func (b *Backend) latest(ctx scope.Context, room *Room, n int, before snowflake.Snowflake) ( []proto.Message, error) { if n <= 0 { return nil, nil } // TODO: define constant if n > 1000 { n = 1000 } var query string args := []interface{}{room.Name, n} // Get the time before which messages will be expired nDays, err := b.DbMap.SelectInt("SELECT retention_days FROM room WHERE name = $1", room.Name) if err != nil { return nil, err } if nDays == 0 { if before.IsZero() { query = ("SELECT room, id, previous_edit_id, parent, posted, edited, deleted," + " session_id, sender_id, sender_name, server_id, server_era, content, encryption_key_id" + " FROM message WHERE room = $1 AND deleted IS NULL ORDER BY id DESC LIMIT $2") } else { query = ("SELECT room, id, previous_edit_id, parent, posted, edited, deleted," + " session_id, sender_id, sender_name, server_id, server_era, content, encryption_key_id" + " FROM message WHERE room = $1 AND id < $3 AND deleted IS NULL ORDER BY id DESC LIMIT $2") args = append(args, before.String()) } } else { threshold := time.Now().Add(time.Duration(-nDays) * 24 * time.Hour) if before.IsZero() { query = ("SELECT room, id, previous_edit_id, parent, posted, edited, deleted," + " session_id, sender_id, sender_name, server_id, server_era, content, encryption_key_id" + " FROM message WHERE room = $1 AND posted > $3 AND deleted IS NULL ORDER BY id DESC LIMIT $2") } else { query = ("SELECT room, id, previous_edit_id, parent, posted, edited, deleted," + " session_id, sender_id, sender_name, server_id, server_era, content, encryption_key_id" + " FROM message WHERE room = $1 AND id < $3 AND deleted IS NULL AND posted > $4 ORDER BY id DESC LIMIT $2") args = append(args, before.String()) } args = append(args, threshold) } msgs, err := b.DbMap.Select(Message{}, query, args...) if err != nil { return nil, err } results := make([]proto.Message, len(msgs)) for i, row := range msgs { msg := row.(*Message) results[len(msgs)-i-1] = msg.ToBackend() } return results, nil }
func (b *AccountManagerBinding) ChangeClientKey( ctx scope.Context, accountID snowflake.Snowflake, oldKey, newKey *security.ManagedKey) error { t, err := b.DbMap.Begin() if err != nil { return err } rollback := func() { if err := t.Rollback(); err != nil { backend.Logger(ctx).Printf("rollback error: %s", err) } } var account Account err = t.SelectOne( &account, "SELECT nonce, mac, encrypted_user_key, encrypted_private_key FROM account WHERE id = $1", accountID.String()) if err != nil { rollback() if err == sql.ErrNoRows { return proto.ErrAccountNotFound } return err } sec := account.Bind(b.Backend).accountSecurity() if err := sec.ChangeClientKey(oldKey, newKey); err != nil { rollback() return err } res, err := t.Exec( "UPDATE account SET mac = $2, encrypted_user_key = $3 WHERE id = $1", accountID.String(), sec.MAC, sec.UserKey.Ciphertext) if err != nil { rollback() return err } n, err := res.RowsAffected() if err != nil { rollback() return err } if n == 0 { rollback() return proto.ErrAccountNotFound } if err := t.Commit(); err != nil { return err } return nil }
func (m *accountManager) Get(ctx scope.Context, id snowflake.Snowflake) (proto.Account, error) { m.b.Lock() defer m.b.Unlock() account, ok := m.b.accounts[id.String()] if !ok { return nil, proto.ErrAccountNotFound } return account, nil }
func (b *AccountManagerBinding) getRawOTP(db gorp.SqlExecutor, accountID snowflake.Snowflake) (*OTP, error) { row, err := db.Get(OTP{}, accountID.String()) if row == nil || err != nil { if row == nil || err == sql.ErrNoRows { return nil, proto.ErrOTPNotEnrolled } return nil, err } return row.(*OTP), nil }
func (b *Backend) latest(ctx scope.Context, rb *RoomBinding, n int, before snowflake.Snowflake) ( []proto.Message, error) { if n <= 0 { return nil, nil } // TODO: define constant if n > 1000 { n = 1000 } var query string args := []interface{}{rb.RoomName, n} // Get the time before which messages will be expired nDays, err := b.DbMap.SelectInt("SELECT retention_days FROM room WHERE name = $1", rb.RoomName) if err != nil { return nil, err } cols, err := allColumns(b.DbMap, Message{}, "") if err != nil { return nil, err } if nDays == 0 { if before.IsZero() { query = fmt.Sprintf("SELECT %s FROM message WHERE room = $1 AND deleted IS NULL ORDER BY id DESC LIMIT $2", cols) } else { query = fmt.Sprintf("SELECT %s FROM message WHERE room = $1 AND id < $3 AND deleted IS NULL ORDER BY id DESC LIMIT $2", cols) args = append(args, before.String()) } } else { threshold := time.Now().Add(time.Duration(-nDays) * 24 * time.Hour) if before.IsZero() { query = fmt.Sprintf("SELECT %s FROM message WHERE room = $1 AND posted > $3 AND deleted IS NULL ORDER BY id DESC LIMIT $2", cols) } else { query = fmt.Sprintf( "SELECT %s FROM message WHERE room = $1 AND id < $3 AND deleted IS NULL AND posted > $4 ORDER BY id DESC LIMIT $2", cols) args = append(args, before.String()) } args = append(args, threshold) } msgs, err := b.DbMap.Select(Message{}, query, args...) if err != nil { return nil, err } results := make([]proto.Message, len(msgs)) for i, row := range msgs { msg := row.(*Message) results[len(msgs)-i-1] = msg.ToTransmission() } return results, nil }
func (rb *RoomBinding) getParentPostTime(id snowflake.Snowflake) (time.Time, error) { var row struct { Posted time.Time } err := rb.DbMap.SelectOne(&row, "SELECT posted FROM message WHERE room = $1 AND id = $2", rb.RoomName, id.String()) if err != nil { return time.Time{}, err } return row.Posted, nil }
func (m *accountManager) RevokeStaff(ctx scope.Context, accountID snowflake.Snowflake) error { m.b.Lock() defer m.b.Unlock() account, ok := m.b.accounts[accountID.String()] if !ok { return proto.ErrAccountNotFound } memAcc := account.(*memAccount) memAcc.staffCapability = nil return nil }
func (rb *RoomBinding) IsValidParent(id snowflake.Snowflake) (bool, error) { if id.String() == "" { return true, nil } if _, err := rb.getParentPostTime(id); err != nil { // check for nonexistant parent if err == sql.ErrNoRows { return false, nil } return false, err } return true, nil }
func (b *AccountManagerBinding) get( db gorp.SqlExecutor, id snowflake.Snowflake) (*AccountBinding, error) { accountCols, err := allColumns(b.DbMap, Account{}, "a") if err != nil { return nil, err } capabilityCols, err := allColumns(b.DbMap, Capability{}, "c", "ID", "staff_capability_id", "nonce", "staff_capability_nonce") if err != nil { return nil, err } var row AccountWithStaffCapability err = db.SelectOne( &row, fmt.Sprintf("SELECT %s, %s FROM account a LEFT OUTER JOIN capability c ON a.staff_capability_id = c.id WHERE a.id = $1", accountCols, capabilityCols), id.String()) if err != nil { if err == sql.ErrNoRows { return nil, proto.ErrAccountNotFound } return nil, err } ab := row.Bind(b.Backend) piCols, err := allColumns(b.DbMap, PersonalIdentity{}, "") if err != nil { return nil, err } rows, err := db.Select(PersonalIdentity{}, fmt.Sprintf("SELECT %s FROM personal_identity WHERE account_id = $1", piCols), id.String()) switch err { case sql.ErrNoRows: case nil: ab.identities = make([]proto.PersonalIdentity, len(rows)) for i, row := range rows { ab.identities[i] = &PersonalIdentityBinding{row.(*PersonalIdentity)} } default: return nil, err } return ab, nil }
func (b *AccountManagerBinding) ChangeName(ctx scope.Context, accountID snowflake.Snowflake, name string) error { res, err := b.DbMap.Exec("UPDATE account SET name = $2 WHERE id = $1", accountID.String(), name) if err != nil { if err == sql.ErrNoRows { return proto.ErrAccountNotFound } return err } n, err := res.RowsAffected() if err != nil { return err } if n < 1 { return proto.ErrAccountNotFound } return nil }
func (rb *ManagedRoomBinding) IsValidParent(id snowflake.Snowflake) (bool, error) { if id.String() == "" || rb.RetentionDays == 0 { return true, nil } posted, err := rb.getParentPostTime(id) if err != nil { // check for nonexistant parent if err == sql.ErrNoRows { return false, nil } return false, err } threshold := time.Now().Add(time.Duration(-rb.RetentionDays) * 24 * time.Hour) if posted.Before(threshold) { return false, nil } return true, nil }
func (t *agentTracker) SetClientKey( ctx scope.Context, agentID string, accessKey *security.ManagedKey, accountID snowflake.Snowflake, clientKey *security.ManagedKey) error { t.b.Lock() defer t.b.Unlock() agent, err := t.Get(ctx, agentID) if err != nil { return err } if err := agent.SetClientKey(accessKey, clientKey); err != nil { return err } agent.AccountID = accountID.String() return nil }
func (b *AccountManagerBinding) ValidateOTP(ctx scope.Context, kms security.KMS, accountID snowflake.Snowflake, password string) error { t, err := b.DbMap.Begin() if err != nil { return err } otp, err := b.getOTP(t, kms, accountID) if err != nil { rollback(ctx, t) return err } if err := otp.Validate(password); err != nil { rollback(ctx, t) return err } if otp.Validated { rollback(ctx, t) return nil } res, err := t.Exec("UPDATE otp SET validated = true WHERE account_id = $1", accountID.String()) if err != nil { rollback(ctx, t) return err } n, err := res.RowsAffected() if err != nil { rollback(ctx, t) return err } if n != 1 { rollback(ctx, t) return fmt.Errorf("failed to mark otp enrollment as validated") } if err := t.Commit(); err != nil { return err } return nil }
func (b *AccountManagerBinding) get( db gorp.SqlExecutor, id snowflake.Snowflake) (*AccountBinding, error) { var row AccountWithStaffCapability err := db.SelectOne( &row, "SELECT a.id, a.nonce, a.mac, a.encrypted_system_key, a.encrypted_user_key,"+ " a.encrypted_private_key, a.public_key,"+ " c.id AS staff_capability_id, c.nonce AS staff_capability_nonce,"+ " c.encrypted_private_data, c.public_data"+ " FROM account a LEFT OUTER JOIN capability c ON a.staff_capability_id = c.id"+ " WHERE a.id = $1", id.String()) if err != nil { if err == sql.ErrNoRows { return nil, proto.ErrAccountNotFound } return nil, err } ab := row.Bind(b.Backend) rows, err := db.Select( PersonalIdentity{}, "SELECT namespace, id, account_id, verified FROM personal_identity WHERE account_id = $1", id.String()) switch err { case sql.ErrNoRows: case nil: ab.identities = make([]proto.PersonalIdentity, len(rows)) for i, row := range rows { ab.identities[i] = &PersonalIdentityBinding{row.(*PersonalIdentity)} } default: return nil, err } return ab, nil }
func NewRoomMessageKeyBinding( rb *ManagedRoomBinding, keyID snowflake.Snowflake, msgKey *security.ManagedKey, nonce []byte) *RoomMessageKeyBinding { rmkb := &RoomMessageKeyBinding{ GrantManager: &proto.GrantManager{ Capabilities: &RoomMessageCapabilities{ Room: rb.Room, Executor: rb.Backend.DbMap, }, Managers: NewRoomManagerKeyBinding(rb), KeyEncryptingKey: &security.ManagedKey{ Ciphertext: rb.Room.EncryptedManagementKey, ContextKey: "room", ContextValue: rb.Room.Name, }, SubjectKeyPair: &security.ManagedKeyPair{ KeyPairType: security.Curve25519, IV: rb.Room.IV, EncryptedPrivateKey: rb.Room.EncryptedPrivateKey, PublicKey: rb.Room.PublicKey, }, PayloadKey: msgKey, SubjectNonce: nonce, }, MessageKey: MessageKey{ ID: keyID.String(), EncryptedKey: msgKey.Ciphertext, IV: msgKey.IV, Nonce: nonce, }, RoomMessageKey: RoomMessageKey{ Room: rb.Room.Name, KeyID: keyID.String(), Activated: time.Now(), }, } return rmkb }
func (atb *AgentTrackerBinding) SetClientKey( ctx scope.Context, agentID string, accessKey *security.ManagedKey, accountID snowflake.Snowflake, clientKey *security.ManagedKey) error { t, err := atb.Backend.DbMap.Begin() if err != nil { return err } rollback := func() { if err := t.Rollback(); err != nil { logging.Logger(ctx).Printf("rollback error: %s", err) } } agent, err := atb.getFromDB(agentID, atb.Backend.DbMap) if err != nil { rollback() return err } if err := agent.SetClientKey(accessKey, clientKey); err != nil { rollback() return err } err = atb.setClientKeyInDB( agentID, accountID.String(), agent.EncryptedClientKey.Ciphertext, t) if err != nil { rollback() return err } if err := t.Commit(); err != nil { return err } return nil }
func (b *AccountManagerBinding) Get( ctx scope.Context, id snowflake.Snowflake) (proto.Account, error) { var row AccountWithStaffCapability err := b.DbMap.SelectOne( &row, "SELECT a.id, a.nonce, a.mac, a.encrypted_system_key, a.encrypted_user_key,"+ " a.encrypted_private_key, a.public_key,"+ " c.id AS staff_capability_id, c.nonce AS staff_capability_nonce,"+ " c.encrypted_private_data, c.public_data"+ " FROM account a LEFT OUTER JOIN capability c ON a.staff_capability_id = c.id"+ " WHERE a.id = $1", id.String()) if err != nil { if err == sql.ErrNoRows { return nil, proto.ErrAccountNotFound } return nil, err } return row.Bind(b.Backend), nil }
func (rb *RoomBinding) IsValidParent(id snowflake.Snowflake) (bool, error) { if id.String() == "" || rb.RetentionDays == 0 { return true, nil } var row struct { Posted time.Time } err := rb.DbMap.SelectOne(&row, "SELECT posted FROM message WHERE room = $1 AND id = $2", rb.Name, id.String()) if err != nil { // check for nonexistant parent if err == sql.ErrNoRows { return false, nil } return false, err } threshold := time.Now().Add(time.Duration(-rb.RetentionDays) * 24 * time.Hour) if row.Posted.Before(threshold) { return false, nil } return true, nil }
func (et *EmailTracker) MarkDelivered(ctx scope.Context, accountID snowflake.Snowflake, id string) error { t, err := et.Backend.DbMap.Begin() if err != nil { return err } row, err := et.Backend.DbMap.Get(Email{}, id) if err != nil { rollback(ctx, t) if err == sql.ErrNoRows { return proto.ErrEmailNotFound } return err } email := row.(*Email) if email.AccountID != accountID.String() { rollback(ctx, t) return proto.ErrEmailNotFound } if email.Delivered.Valid { rollback(ctx, t) return proto.ErrEmailAlreadyDelivered } if _, err := t.Exec("UPDATE email SET delivered = NOW() WHERE id = $1", id); err != nil { rollback(ctx, t) return err } if err := t.Commit(); err != nil { return err } return nil }
func (b *AccountManagerBinding) GrantStaff( ctx scope.Context, accountID snowflake.Snowflake, kmsCred security.KMSCredential) error { // Look up the target account's (system) encrypted client key. This is // not part of the transaction, because we want to interact with KMS // before we proceed. That should be fine, since this is an infrequently // used action. var row struct { EncryptedClientKey []byte `db:"encrypted_system_key"` Nonce []byte `db:"nonce"` } err := b.DbMap.SelectOne( &row, "SELECT encrypted_system_key, nonce FROM account WHERE id = $1", accountID.String()) if err != nil { if err == sql.ErrNoRows { return proto.ErrAccountNotFound } return err } // Use kmsCred to obtain kms and decrypt the client's key. kms := kmsCred.KMS() clientKey := &security.ManagedKey{ KeyType: proto.ClientKeyType, Ciphertext: row.EncryptedClientKey, ContextKey: "nonce", ContextValue: base64.URLEncoding.EncodeToString(row.Nonce), } if err := kms.DecryptKey(clientKey); err != nil { return err } // Grant staff capability. This involves marshalling kmsCred to JSON and // encrypting it with the client key. nonce, err := kms.GenerateNonce(clientKey.KeyType.BlockSize()) if err != nil { return err } capability, err := security.GrantSharedSecretCapability(clientKey, nonce, kmsCred.KMSType(), kmsCred) if err != nil { return err } // Store capability and update account table. t, err := b.DbMap.Begin() if err != nil { return err } rollback := func() { if err := t.Rollback(); err != nil { backend.Logger(ctx).Printf("rollback error: %s", err) } } dbCap := &Capability{ ID: capability.CapabilityID(), NonceBytes: capability.Nonce(), EncryptedPrivateData: capability.EncryptedPayload(), PublicData: capability.PublicPayload(), } if err := t.Insert(dbCap); err != nil { rollback() return err } result, err := t.Exec( "UPDATE account SET staff_capability_id = $2 WHERE id = $1", accountID.String(), capability.CapabilityID()) if err != nil { rollback() return err } n, err := result.RowsAffected() if err != nil { rollback() return err } if n != 1 { rollback() return proto.ErrAccountNotFound } if err := t.Commit(); err != nil { return err } return nil }
func (b *AccountManagerBinding) ChangeEmail(ctx scope.Context, accountID snowflake.Snowflake, email string) (bool, error) { t, err := b.DbMap.Begin() if err != nil { return false, err } account, err := b.get(t, accountID) if err != nil { rollback(ctx, t) return false, err } other, err := b.resolve(t, "email", email) if err != nil && err != proto.ErrAccountNotFound { rollback(ctx, t) return false, err } if err == nil && other.ID() != accountID { rollback(ctx, t) return false, proto.ErrPersonalIdentityInUse } for _, pid := range account.identities { if pid.Namespace() == "email" && pid.ID() == email { if pid.Verified() { res, err := t.Exec("UPDATE account SET email = $2 WHERE id = $1", accountID.String(), email) if err != nil { if err == sql.ErrNoRows { return false, proto.ErrAccountNotFound } rollback(ctx, t) return false, err } n, err := res.RowsAffected() if err != nil { rollback(ctx, t) return false, err } if n < 1 { rollback(ctx, t) return false, proto.ErrAccountNotFound } if err := t.Commit(); err != nil { return false, err } return true, nil } rollback(ctx, t) return false, nil } } pid := &PersonalIdentity{ Namespace: "email", ID: email, AccountID: accountID.String(), } if err := t.Insert(pid); err != nil { rollback(ctx, t) return false, err } if err := t.Commit(); err != nil { return false, err } return false, nil }
func (rb *RoomBinding) GetMessage(ctx scope.Context, id snowflake.Snowflake) (*proto.Message, error) { var msg Message nDays, err := rb.DbMap.SelectInt("SELECT retention_days FROM room WHERE name = $1", rb.RoomName) if err != nil { return nil, err } cols, err := allColumns(rb.DbMap, Message{}, "") if err != nil { return nil, err } err = rb.DbMap.SelectOne(&msg, fmt.Sprintf("SELECT %s FROM message WHERE room = $1 AND id = $2", cols), rb.RoomName, id.String()) if err != nil { if err == sql.ErrNoRows { return nil, proto.ErrMessageNotFound } return nil, err } if nDays > 0 { threshold := time.Now().Add(time.Duration(-nDays) * 24 * time.Hour) if msg.Posted.Before(threshold) { return nil, proto.ErrMessageNotFound } } m := msg.ToBackend() return &m, nil }