| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686 |
- package state
- import (
- "bytes"
- "database/sql"
- "embed"
- "errors"
- "fmt"
- "io/fs"
- "net/http"
- "net/mail"
- "time"
- "github.com/mk6i/retro-aim-server/wire"
- "github.com/golang-migrate/migrate/v4"
- "github.com/golang-migrate/migrate/v4/database/sqlite3"
- "github.com/golang-migrate/migrate/v4/source/httpfs"
- sqlite "github.com/mattn/go-sqlite3"
- )
- //go:embed migrations/*
- var migrations embed.FS
- // SQLiteUserStore stores user feedbag (buddy list), profile, and
- // authentication credentials information in a SQLite database.
- type SQLiteUserStore struct {
- db *sql.DB
- }
- // NewSQLiteUserStore creates a new instance of SQLiteUserStore. If the
- // database does not already exist, a new one is created with the required
- // schema.
- func NewSQLiteUserStore(dbFilePath string) (*SQLiteUserStore, error) {
- db, err := sql.Open("sqlite3", dbFilePath)
- if err != nil {
- return nil, err
- }
- store := &SQLiteUserStore{db: db}
- return store, store.runMigrations()
- }
- func (f SQLiteUserStore) runMigrations() error {
- migrationFS, err := fs.Sub(migrations, "migrations")
- if err != nil {
- return fmt.Errorf("failed to prepare migration subdirectory: %v", err)
- }
- sourceInstance, err := httpfs.New(http.FS(migrationFS), ".")
- if err != nil {
- return fmt.Errorf("failed to create source instance from embedded filesystem: %v", err)
- }
- driver, err := sqlite3.WithInstance(f.db, &sqlite3.Config{})
- if err != nil {
- return fmt.Errorf("cannot create database driver: %v", err)
- }
- m, err := migrate.NewWithInstance("httpfs", sourceInstance, "sqlite3", driver)
- if err != nil {
- return fmt.Errorf("failed to create migrate instance: %v", err)
- }
- if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
- return fmt.Errorf("failed to run migrations: %v", err)
- }
- return nil
- }
- // AllUsers returns all stored users. It only populates the User.IdentScreenName and
- // User.DisplayScreenName fields in the return slice.
- func (f SQLiteUserStore) AllUsers() ([]User, error) {
- q := `SELECT identScreenName, displayScreenName FROM users`
- rows, err := f.db.Query(q)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var users []User
- for rows.Next() {
- var identSN, displaySN string
- if err := rows.Scan(&identSN, &displaySN); err != nil {
- return nil, err
- }
- users = append(users, User{
- IdentScreenName: NewIdentScreenName(identSN),
- DisplayScreenName: DisplayScreenName(displaySN),
- })
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return users, nil
- }
- // User looks up a user by screen name. It populates the User record with
- // credentials that can be used to validate the user's password.
- func (f SQLiteUserStore) User(screenName IdentScreenName) (*User, error) {
- q := `
- SELECT
- displayScreenName,
- authKey,
- weakMD5Pass,
- strongMD5Pass
- FROM users
- WHERE identScreenName = ?
- `
- u := &User{
- IdentScreenName: screenName,
- }
- err := f.db.QueryRow(q, screenName.String()).
- Scan(&u.DisplayScreenName, &u.AuthKey, &u.WeakMD5Pass, &u.StrongMD5Pass)
- if errors.Is(err, sql.ErrNoRows) {
- return nil, nil
- }
- return u, err
- }
- // InsertUser inserts a user to the store. Return ErrDupUser if a user with the
- // same screen name already exists.
- func (f SQLiteUserStore) InsertUser(u User) error {
- q := `
- INSERT INTO users (identScreenName, displayScreenName, authKey, weakMD5Pass, strongMD5Pass)
- VALUES (?, ?, ?, ?, ?)
- ON CONFLICT (identScreenName) DO NOTHING
- `
- result, err := f.db.Exec(q, u.IdentScreenName.String(), u.DisplayScreenName, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass)
- if err != nil {
- return err
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return err
- }
- if rowsAffected == 0 {
- return ErrDupUser
- }
- return nil
- }
- // DeleteUser deletes a user from the store. Return ErrNoUser if the user did
- // not exist prior to deletion.
- func (f SQLiteUserStore) DeleteUser(screenName IdentScreenName) error {
- q := `
- DELETE FROM users WHERE identScreenName = ?
- `
- result, err := f.db.Exec(q, screenName.String())
- if err != nil {
- return err
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return err
- }
- if rowsAffected == 0 {
- return ErrNoUser
- }
- return nil
- }
- // SetUserPassword sets the user's password hashes and auth key. The following
- // fields must be set on u:
- // - AuthKey
- // - WeakMD5Pass
- // - StrongMD5Pass
- // - IdentScreenName
- func (f SQLiteUserStore) SetUserPassword(u User) (err error) {
- tx, err := f.db.Begin()
- if err != nil {
- return
- }
- defer func() {
- if err != nil {
- err = errors.Join(err, tx.Rollback())
- }
- }()
- q := `
- UPDATE users
- SET authKey = ?, weakMD5Pass = ?, strongMD5Pass = ?
- WHERE identScreenName = ?
- `
- result, err := tx.Exec(q, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass, u.IdentScreenName.String())
- if err != nil {
- return
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return
- }
- if rowsAffected == 0 {
- // it's possible the user didn't change OR the user doesn't exist.
- // check if the user exists.
- var exists int
- err = tx.QueryRow("SELECT COUNT(*) FROM users WHERE identScreenName = ?", u.IdentScreenName.String()).Scan(&exists)
- if err != nil {
- return // Handle possible SQL errors during the select
- }
- if exists == 0 {
- err = ErrNoUser // User does not exist
- return
- }
- }
- return tx.Commit()
- }
- // Feedbag fetches the contents of a user's feedbag (buddy list).
- func (f SQLiteUserStore) Feedbag(screenName IdentScreenName) ([]wire.FeedbagItem, error) {
- q := `
- SELECT
- groupID,
- itemID,
- classID,
- name,
- attributes
- FROM feedbag
- WHERE screenName = ?
- `
- rows, err := f.db.Query(q, screenName.String())
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var items []wire.FeedbagItem
- for rows.Next() {
- var item wire.FeedbagItem
- var attrs []byte
- if err := rows.Scan(&item.GroupID, &item.ItemID, &item.ClassID, &item.Name, &attrs); err != nil {
- return nil, err
- }
- if err := wire.Unmarshal(&item.TLVLBlock, bytes.NewBuffer(attrs)); err != nil {
- return items, err
- }
- items = append(items, item)
- }
- return items, nil
- }
- // FeedbagLastModified returns the last time a user's feedbag (buddy list) was
- // updated.
- func (f SQLiteUserStore) FeedbagLastModified(screenName IdentScreenName) (time.Time, error) {
- var lastModified sql.NullInt64
- q := `SELECT MAX(lastModified) FROM feedbag WHERE screenName = ?`
- err := f.db.QueryRow(q, screenName.String()).Scan(&lastModified)
- return time.Unix(lastModified.Int64, 0), err
- }
- // FeedbagDelete deletes an entry from a user's feedbag (buddy list).
- func (f SQLiteUserStore) FeedbagDelete(screenName IdentScreenName, items []wire.FeedbagItem) error {
- // todo add transaction
- q := `DELETE FROM feedbag WHERE screenName = ? AND itemID = ?`
- for _, item := range items {
- if _, err := f.db.Exec(q, screenName.String(), item.ItemID); err != nil {
- return err
- }
- }
- return nil
- }
- // FeedbagUpsert upserts an entry to a user's feedbag (buddy list). An entry is
- // created if it doesn't already exist, or modified if it already exists.
- func (f SQLiteUserStore) FeedbagUpsert(screenName IdentScreenName, items []wire.FeedbagItem) error {
- q := `
- INSERT INTO feedbag (screenName, groupID, itemID, classID, name, attributes, lastModified)
- VALUES (?, ?, ?, ?, ?, ?, UNIXEPOCH())
- ON CONFLICT (screenName, groupID, itemID)
- DO UPDATE SET classID = excluded.classID,
- name = excluded.name,
- attributes = excluded.attributes,
- lastModified = UNIXEPOCH()
- `
- for _, item := range items {
- buf := &bytes.Buffer{}
- if err := wire.Marshal(item.TLVLBlock, buf); err != nil {
- return err
- }
- // todo are these all the right items?
- if item.ClassID == wire.FeedbagClassIdBuddy ||
- item.ClassID == wire.FeedbagClassIDPermit ||
- item.ClassID == wire.FeedbagClassIDDeny {
- // insert screen name identifier
- item.Name = NewIdentScreenName(item.Name).String()
- }
- _, err := f.db.Exec(q,
- screenName.String(),
- item.GroupID,
- item.ItemID,
- item.ClassID,
- item.Name,
- buf.Bytes())
- if err != nil {
- return err
- }
- }
- return nil
- }
- // AdjacentUsers returns all users who have screenName in their buddy list.
- // Exclude users who are on screenName's block list.
- func (f SQLiteUserStore) AdjacentUsers(screenName IdentScreenName) ([]IdentScreenName, error) {
- q := `
- SELECT f.screenName
- FROM feedbag f
- WHERE f.name = ?
- AND f.classID = 0
- -- Don't show screenName that its blocked buddy is online
- AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = ? AND name = f.screenName AND classID = 3)
- -- Don't show blocked buddy that screenName is online
- AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = f.screenName AND name = f.name AND classID = 3)
- `
- rows, err := f.db.Query(q, screenName.String(), screenName.String(), screenName.String())
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var items []IdentScreenName
- for rows.Next() {
- var sn string
- if err := rows.Scan(&sn); err != nil {
- return nil, err
- }
- items = append(items, NewIdentScreenName(sn))
- }
- return items, nil
- }
- // Buddies returns all user's buddies. Don't return a buddy if the user has
- // them on their block list.
- func (f SQLiteUserStore) Buddies(screenName IdentScreenName) ([]IdentScreenName, error) {
- q := `
- SELECT f.name
- FROM feedbag f
- WHERE f.screenName = ? AND f.classID = 0
- -- Don't include buddy if they blocked screenName
- AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = f.name AND name = ? AND classID = 3)
- -- Don't include buddy if screen name blocked them
- AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = ? AND name = f.name AND classID = 3)
- `
- rows, err := f.db.Query(q, screenName.String(), screenName.String(), screenName.String())
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var items []IdentScreenName
- for rows.Next() {
- var sn string
- if err := rows.Scan(&sn); err != nil {
- return nil, err
- }
- items = append(items, NewIdentScreenName(sn))
- }
- return items, nil
- }
- // BlockedState returns the BlockedState between two users.
- func (f SQLiteUserStore) BlockedState(screenName1, screenName2 IdentScreenName) (BlockedState, error) {
- q := `
- SELECT EXISTS(SELECT 1
- FROM feedbag f
- WHERE f.classID = 3
- AND f.screenName = ?
- AND f.name = ?)
- UNION ALL
- SELECT EXISTS(SELECT 1
- FROM feedbag f
- WHERE f.classID = 3
- AND f.screenName = ?
- AND f.name = ?)
- `
- row, err := f.db.Query(q, screenName1.String(), screenName2.String(), screenName2.String(), screenName1.String())
- if err != nil {
- return BlockedNo, err
- }
- defer row.Close()
- var blockedA bool
- if row.Next() {
- if err := row.Scan(&blockedA); err != nil {
- return BlockedNo, err
- }
- }
- var blockedB bool
- if row.Next() {
- if err := row.Scan(&blockedB); err != nil {
- return BlockedNo, err
- }
- }
- switch {
- case blockedA:
- return BlockedA, nil
- case blockedB:
- return BlockedB, nil
- default:
- return BlockedNo, nil
- }
- }
- // Profile fetches a user profile. Return empty string if the user
- // does not exist or has no profile.
- func (f SQLiteUserStore) Profile(screenName IdentScreenName) (string, error) {
- q := `
- SELECT IFNULL(body, '')
- FROM profile
- WHERE screenName = ?
- `
- var profile string
- err := f.db.QueryRow(q, screenName.String()).Scan(&profile)
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return "", err
- }
- return profile, nil
- }
- // SetProfile sets the text contents of a user's profile.
- func (f SQLiteUserStore) SetProfile(screenName IdentScreenName, body string) error {
- q := `
- INSERT INTO profile (screenName, body)
- VALUES (?, ?)
- ON CONFLICT (screenName)
- DO UPDATE SET body = excluded.body
- `
- _, err := f.db.Exec(q, screenName.String(), body)
- return err
- }
- func (f SQLiteUserStore) BARTUpsert(itemHash []byte, body []byte) error {
- q := `
- INSERT INTO bartItem (hash, body)
- VALUES (?, ?)
- ON CONFLICT DO NOTHING
- `
- _, err := f.db.Exec(q, itemHash, body)
- return err
- }
- func (f SQLiteUserStore) BARTRetrieve(hash []byte) ([]byte, error) {
- q := `
- SELECT body
- FROM bartItem
- WHERE hash = ?
- `
- var body []byte
- err := f.db.QueryRow(q, hash).Scan(&body)
- if errors.Is(err, sql.ErrNoRows) {
- err = nil
- }
- return body, err
- }
- // ChatRoomByCookie looks up a chat room by cookie. Returns
- // ErrChatRoomNotFound if the room does not exist for cookie.
- func (f SQLiteUserStore) ChatRoomByCookie(cookie string) (ChatRoom, error) {
- chatRoom := ChatRoom{
- Cookie: cookie,
- }
- q := `
- SELECT exchange, name, created, creator
- FROM chatRoom
- WHERE cookie = ?
- `
- var creator string
- err := f.db.QueryRow(q, cookie).Scan(
- &chatRoom.Exchange,
- &chatRoom.Name,
- &chatRoom.CreateTime,
- &creator,
- )
- if errors.Is(err, sql.ErrNoRows) {
- err = ErrChatRoomNotFound
- }
- chatRoom.Creator = NewIdentScreenName(creator)
- return chatRoom, err
- }
- // ChatRoomByName looks up a chat room by exchange and name. Returns
- // ErrChatRoomNotFound if the room does not exist for exchange and name.
- func (f SQLiteUserStore) ChatRoomByName(exchange uint16, name string) (ChatRoom, error) {
- chatRoom := ChatRoom{
- Exchange: exchange,
- Name: name,
- }
- q := `
- SELECT cookie, created, creator
- FROM chatRoom
- WHERE exchange = ? AND name = ?
- `
- var creator string
- err := f.db.QueryRow(q, exchange, name).Scan(
- &chatRoom.Cookie,
- &chatRoom.CreateTime,
- &creator,
- )
- if errors.Is(err, sql.ErrNoRows) {
- err = ErrChatRoomNotFound
- }
- chatRoom.Creator = NewIdentScreenName(creator)
- return chatRoom, err
- }
- // CreateChatRoom creates a new chat room.
- func (f SQLiteUserStore) CreateChatRoom(chatRoom ChatRoom) error {
- q := `
- INSERT INTO chatRoom (cookie, exchange, name, created, creator)
- VALUES (?, ?, ?, ?, ?)
- `
- _, err := f.db.Exec(
- q,
- chatRoom.Cookie,
- chatRoom.Exchange,
- chatRoom.Name,
- chatRoom.CreateTime,
- chatRoom.Creator.String(),
- )
- if err != nil {
- if sqliteErr, ok := err.(sqlite.Error); ok {
- if sqliteErr.ExtendedCode == sqlite.ErrConstraintUnique || sqliteErr.ExtendedCode == sqlite.ErrConstraintPrimaryKey {
- err = ErrDupChatRoom
- }
- }
- err = fmt.Errorf("CreateChatRoom: %w", err)
- }
- return err
- }
- func (f SQLiteUserStore) AllChatRooms(exchange uint16) ([]ChatRoom, error) {
- q := `
- SELECT cookie, created, creator, name
- FROM chatRoom
- WHERE exchange = ?
- ORDER BY created ASC
- `
- rows, err := f.db.Query(q, exchange)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var users []ChatRoom
- for rows.Next() {
- cr := ChatRoom{
- Exchange: exchange,
- }
- var creator string
- if err := rows.Scan(&cr.Cookie, &cr.CreateTime, &creator, &cr.Name); err != nil {
- return nil, err
- }
- cr.Creator = NewIdentScreenName(creator)
- users = append(users, cr)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return users, nil
- }
- // UpdateDisplayScreenName updates the user's DisplayScreenName
- func (f SQLiteUserStore) UpdateDisplayScreenName(displayScreenName DisplayScreenName) error {
- q := `
- UPDATE users
- SET displayScreenName = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.Exec(q, displayScreenName.String(), displayScreenName.IdentScreenName().String())
- return err
- }
- // UpdateEmailAddress updates the user's EmailAddress
- func (f SQLiteUserStore) UpdateEmailAddress(emailAddress *mail.Address, screenName IdentScreenName) error {
- q := `
- UPDATE users
- SET emailAddress = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.Exec(q, emailAddress.Address, screenName.String())
- return err
- }
- // EmailAddressByName retrieves the user's EmailAddress
- func (f SQLiteUserStore) EmailAddressByName(screenName IdentScreenName) (*mail.Address, error) {
- q := `
- SELECT emailAddress
- FROM users
- WHERE identScreenName = ?
- `
- var emailAddress string
- err := f.db.QueryRow(q, screenName.String()).Scan(&emailAddress)
- // username isn't found for some reason
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return nil, err
- }
- e, err := mail.ParseAddress(emailAddress)
- if err != nil {
- return nil, fmt.Errorf("%w: %w", ErrNoEmailAddress, err)
- }
- return e, nil
- }
- // UpdateRegStatus updates the user's registration status preference
- func (f SQLiteUserStore) UpdateRegStatus(regStatus uint16, screenName IdentScreenName) error {
- q := `
- UPDATE users
- SET regStatus = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.Exec(q, regStatus, screenName.String())
- return err
- }
- // RegStatusByName retrieves the user's registration status preference
- func (f SQLiteUserStore) RegStatusByName(screenName IdentScreenName) (uint16, error) {
- q := `
- SELECT regStatus
- FROM users
- WHERE identScreenName = ?
- `
- var regStatus uint16
- err := f.db.QueryRow(q, screenName.String()).Scan(®Status)
- // username isn't found for some reason
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return 0, err
- }
- return regStatus, nil
- }
- // UpdateConfirmStatus updates the user's confirmation status
- func (f SQLiteUserStore) UpdateConfirmStatus(confirmStatus bool, screenName IdentScreenName) error {
- q := `
- UPDATE users
- SET confirmStatus = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.Exec(q, confirmStatus, screenName.String())
- return err
- }
- // ConfirmStatusByName retrieves the user's confirmation status
- func (f SQLiteUserStore) ConfirmStatusByName(screenName IdentScreenName) (bool, error) {
- q := `
- SELECT confirmStatus
- FROM users
- WHERE identScreenName = ?
- `
- var confirmStatus bool
- err := f.db.QueryRow(q, screenName.String()).Scan(&confirmStatus)
- // username isn't found for some reason
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return false, err
- }
- return confirmStatus, nil
- }
|