user_store.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. package state
  2. import (
  3. "bytes"
  4. "database/sql"
  5. "embed"
  6. "errors"
  7. "fmt"
  8. "io/fs"
  9. "net/http"
  10. "net/mail"
  11. "time"
  12. "github.com/mk6i/retro-aim-server/wire"
  13. "github.com/golang-migrate/migrate/v4"
  14. "github.com/golang-migrate/migrate/v4/database/sqlite3"
  15. "github.com/golang-migrate/migrate/v4/source/httpfs"
  16. sqlite "github.com/mattn/go-sqlite3"
  17. )
  18. //go:embed migrations/*
  19. var migrations embed.FS
  20. // SQLiteUserStore stores user feedbag (buddy list), profile, and
  21. // authentication credentials information in a SQLite database.
  22. type SQLiteUserStore struct {
  23. db *sql.DB
  24. }
  25. // NewSQLiteUserStore creates a new instance of SQLiteUserStore. If the
  26. // database does not already exist, a new one is created with the required
  27. // schema.
  28. func NewSQLiteUserStore(dbFilePath string) (*SQLiteUserStore, error) {
  29. db, err := sql.Open("sqlite3", dbFilePath)
  30. if err != nil {
  31. return nil, err
  32. }
  33. store := &SQLiteUserStore{db: db}
  34. return store, store.runMigrations()
  35. }
  36. func (f SQLiteUserStore) runMigrations() error {
  37. migrationFS, err := fs.Sub(migrations, "migrations")
  38. if err != nil {
  39. return fmt.Errorf("failed to prepare migration subdirectory: %v", err)
  40. }
  41. sourceInstance, err := httpfs.New(http.FS(migrationFS), ".")
  42. if err != nil {
  43. return fmt.Errorf("failed to create source instance from embedded filesystem: %v", err)
  44. }
  45. driver, err := sqlite3.WithInstance(f.db, &sqlite3.Config{})
  46. if err != nil {
  47. return fmt.Errorf("cannot create database driver: %v", err)
  48. }
  49. m, err := migrate.NewWithInstance("httpfs", sourceInstance, "sqlite3", driver)
  50. if err != nil {
  51. return fmt.Errorf("failed to create migrate instance: %v", err)
  52. }
  53. if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
  54. return fmt.Errorf("failed to run migrations: %v", err)
  55. }
  56. return nil
  57. }
  58. // AllUsers returns all stored users. It only populates the User.IdentScreenName and
  59. // User.DisplayScreenName fields in the return slice.
  60. func (f SQLiteUserStore) AllUsers() ([]User, error) {
  61. q := `SELECT identScreenName, displayScreenName FROM users`
  62. rows, err := f.db.Query(q)
  63. if err != nil {
  64. return nil, err
  65. }
  66. defer rows.Close()
  67. var users []User
  68. for rows.Next() {
  69. var identSN, displaySN string
  70. if err := rows.Scan(&identSN, &displaySN); err != nil {
  71. return nil, err
  72. }
  73. users = append(users, User{
  74. IdentScreenName: NewIdentScreenName(identSN),
  75. DisplayScreenName: DisplayScreenName(displaySN),
  76. })
  77. }
  78. if err := rows.Err(); err != nil {
  79. return nil, err
  80. }
  81. return users, nil
  82. }
  83. // User looks up a user by screen name. It populates the User record with
  84. // credentials that can be used to validate the user's password.
  85. func (f SQLiteUserStore) User(screenName IdentScreenName) (*User, error) {
  86. q := `
  87. SELECT
  88. displayScreenName,
  89. authKey,
  90. weakMD5Pass,
  91. strongMD5Pass
  92. FROM users
  93. WHERE identScreenName = ?
  94. `
  95. u := &User{
  96. IdentScreenName: screenName,
  97. }
  98. err := f.db.QueryRow(q, screenName.String()).
  99. Scan(&u.DisplayScreenName, &u.AuthKey, &u.WeakMD5Pass, &u.StrongMD5Pass)
  100. if errors.Is(err, sql.ErrNoRows) {
  101. return nil, nil
  102. }
  103. return u, err
  104. }
  105. // InsertUser inserts a user to the store. Return ErrDupUser if a user with the
  106. // same screen name already exists.
  107. func (f SQLiteUserStore) InsertUser(u User) error {
  108. q := `
  109. INSERT INTO users (identScreenName, displayScreenName, authKey, weakMD5Pass, strongMD5Pass)
  110. VALUES (?, ?, ?, ?, ?)
  111. ON CONFLICT (identScreenName) DO NOTHING
  112. `
  113. result, err := f.db.Exec(q, u.IdentScreenName.String(), u.DisplayScreenName, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass)
  114. if err != nil {
  115. return err
  116. }
  117. rowsAffected, err := result.RowsAffected()
  118. if err != nil {
  119. return err
  120. }
  121. if rowsAffected == 0 {
  122. return ErrDupUser
  123. }
  124. return nil
  125. }
  126. // DeleteUser deletes a user from the store. Return ErrNoUser if the user did
  127. // not exist prior to deletion.
  128. func (f SQLiteUserStore) DeleteUser(screenName IdentScreenName) error {
  129. q := `
  130. DELETE FROM users WHERE identScreenName = ?
  131. `
  132. result, err := f.db.Exec(q, screenName.String())
  133. if err != nil {
  134. return err
  135. }
  136. rowsAffected, err := result.RowsAffected()
  137. if err != nil {
  138. return err
  139. }
  140. if rowsAffected == 0 {
  141. return ErrNoUser
  142. }
  143. return nil
  144. }
  145. // SetUserPassword sets the user's password hashes and auth key. The following
  146. // fields must be set on u:
  147. // - AuthKey
  148. // - WeakMD5Pass
  149. // - StrongMD5Pass
  150. // - IdentScreenName
  151. func (f SQLiteUserStore) SetUserPassword(u User) (err error) {
  152. tx, err := f.db.Begin()
  153. if err != nil {
  154. return
  155. }
  156. defer func() {
  157. if err != nil {
  158. err = errors.Join(err, tx.Rollback())
  159. }
  160. }()
  161. q := `
  162. UPDATE users
  163. SET authKey = ?, weakMD5Pass = ?, strongMD5Pass = ?
  164. WHERE identScreenName = ?
  165. `
  166. result, err := tx.Exec(q, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass, u.IdentScreenName.String())
  167. if err != nil {
  168. return
  169. }
  170. rowsAffected, err := result.RowsAffected()
  171. if err != nil {
  172. return
  173. }
  174. if rowsAffected == 0 {
  175. // it's possible the user didn't change OR the user doesn't exist.
  176. // check if the user exists.
  177. var exists int
  178. err = tx.QueryRow("SELECT COUNT(*) FROM users WHERE identScreenName = ?", u.IdentScreenName.String()).Scan(&exists)
  179. if err != nil {
  180. return // Handle possible SQL errors during the select
  181. }
  182. if exists == 0 {
  183. err = ErrNoUser // User does not exist
  184. return
  185. }
  186. }
  187. return tx.Commit()
  188. }
  189. // Feedbag fetches the contents of a user's feedbag (buddy list).
  190. func (f SQLiteUserStore) Feedbag(screenName IdentScreenName) ([]wire.FeedbagItem, error) {
  191. q := `
  192. SELECT
  193. groupID,
  194. itemID,
  195. classID,
  196. name,
  197. attributes
  198. FROM feedbag
  199. WHERE screenName = ?
  200. `
  201. rows, err := f.db.Query(q, screenName.String())
  202. if err != nil {
  203. return nil, err
  204. }
  205. defer rows.Close()
  206. var items []wire.FeedbagItem
  207. for rows.Next() {
  208. var item wire.FeedbagItem
  209. var attrs []byte
  210. if err := rows.Scan(&item.GroupID, &item.ItemID, &item.ClassID, &item.Name, &attrs); err != nil {
  211. return nil, err
  212. }
  213. if err := wire.Unmarshal(&item.TLVLBlock, bytes.NewBuffer(attrs)); err != nil {
  214. return items, err
  215. }
  216. items = append(items, item)
  217. }
  218. return items, nil
  219. }
  220. // FeedbagLastModified returns the last time a user's feedbag (buddy list) was
  221. // updated.
  222. func (f SQLiteUserStore) FeedbagLastModified(screenName IdentScreenName) (time.Time, error) {
  223. var lastModified sql.NullInt64
  224. q := `SELECT MAX(lastModified) FROM feedbag WHERE screenName = ?`
  225. err := f.db.QueryRow(q, screenName.String()).Scan(&lastModified)
  226. return time.Unix(lastModified.Int64, 0), err
  227. }
  228. // FeedbagDelete deletes an entry from a user's feedbag (buddy list).
  229. func (f SQLiteUserStore) FeedbagDelete(screenName IdentScreenName, items []wire.FeedbagItem) error {
  230. // todo add transaction
  231. q := `DELETE FROM feedbag WHERE screenName = ? AND itemID = ?`
  232. for _, item := range items {
  233. if _, err := f.db.Exec(q, screenName.String(), item.ItemID); err != nil {
  234. return err
  235. }
  236. }
  237. return nil
  238. }
  239. // FeedbagUpsert upserts an entry to a user's feedbag (buddy list). An entry is
  240. // created if it doesn't already exist, or modified if it already exists.
  241. func (f SQLiteUserStore) FeedbagUpsert(screenName IdentScreenName, items []wire.FeedbagItem) error {
  242. q := `
  243. INSERT INTO feedbag (screenName, groupID, itemID, classID, name, attributes, lastModified)
  244. VALUES (?, ?, ?, ?, ?, ?, UNIXEPOCH())
  245. ON CONFLICT (screenName, groupID, itemID)
  246. DO UPDATE SET classID = excluded.classID,
  247. name = excluded.name,
  248. attributes = excluded.attributes,
  249. lastModified = UNIXEPOCH()
  250. `
  251. for _, item := range items {
  252. buf := &bytes.Buffer{}
  253. if err := wire.Marshal(item.TLVLBlock, buf); err != nil {
  254. return err
  255. }
  256. // todo are these all the right items?
  257. if item.ClassID == wire.FeedbagClassIdBuddy ||
  258. item.ClassID == wire.FeedbagClassIDPermit ||
  259. item.ClassID == wire.FeedbagClassIDDeny {
  260. // insert screen name identifier
  261. item.Name = NewIdentScreenName(item.Name).String()
  262. }
  263. _, err := f.db.Exec(q,
  264. screenName.String(),
  265. item.GroupID,
  266. item.ItemID,
  267. item.ClassID,
  268. item.Name,
  269. buf.Bytes())
  270. if err != nil {
  271. return err
  272. }
  273. }
  274. return nil
  275. }
  276. // AdjacentUsers returns all users who have screenName in their buddy list.
  277. // Exclude users who are on screenName's block list.
  278. func (f SQLiteUserStore) AdjacentUsers(screenName IdentScreenName) ([]IdentScreenName, error) {
  279. q := `
  280. SELECT f.screenName
  281. FROM feedbag f
  282. WHERE f.name = ?
  283. AND f.classID = 0
  284. -- Don't show screenName that its blocked buddy is online
  285. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = ? AND name = f.screenName AND classID = 3)
  286. -- Don't show blocked buddy that screenName is online
  287. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = f.screenName AND name = f.name AND classID = 3)
  288. `
  289. rows, err := f.db.Query(q, screenName.String(), screenName.String(), screenName.String())
  290. if err != nil {
  291. return nil, err
  292. }
  293. defer rows.Close()
  294. var items []IdentScreenName
  295. for rows.Next() {
  296. var sn string
  297. if err := rows.Scan(&sn); err != nil {
  298. return nil, err
  299. }
  300. items = append(items, NewIdentScreenName(sn))
  301. }
  302. return items, nil
  303. }
  304. // Buddies returns all user's buddies. Don't return a buddy if the user has
  305. // them on their block list.
  306. func (f SQLiteUserStore) Buddies(screenName IdentScreenName) ([]IdentScreenName, error) {
  307. q := `
  308. SELECT f.name
  309. FROM feedbag f
  310. WHERE f.screenName = ? AND f.classID = 0
  311. -- Don't include buddy if they blocked screenName
  312. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = f.name AND name = ? AND classID = 3)
  313. -- Don't include buddy if screen name blocked them
  314. AND NOT EXISTS(SELECT 1 FROM feedbag WHERE screenName = ? AND name = f.name AND classID = 3)
  315. `
  316. rows, err := f.db.Query(q, screenName.String(), screenName.String(), screenName.String())
  317. if err != nil {
  318. return nil, err
  319. }
  320. defer rows.Close()
  321. var items []IdentScreenName
  322. for rows.Next() {
  323. var sn string
  324. if err := rows.Scan(&sn); err != nil {
  325. return nil, err
  326. }
  327. items = append(items, NewIdentScreenName(sn))
  328. }
  329. return items, nil
  330. }
  331. // BlockedState returns the BlockedState between two users.
  332. func (f SQLiteUserStore) BlockedState(screenName1, screenName2 IdentScreenName) (BlockedState, error) {
  333. q := `
  334. SELECT EXISTS(SELECT 1
  335. FROM feedbag f
  336. WHERE f.classID = 3
  337. AND f.screenName = ?
  338. AND f.name = ?)
  339. UNION ALL
  340. SELECT EXISTS(SELECT 1
  341. FROM feedbag f
  342. WHERE f.classID = 3
  343. AND f.screenName = ?
  344. AND f.name = ?)
  345. `
  346. row, err := f.db.Query(q, screenName1.String(), screenName2.String(), screenName2.String(), screenName1.String())
  347. if err != nil {
  348. return BlockedNo, err
  349. }
  350. defer row.Close()
  351. var blockedA bool
  352. if row.Next() {
  353. if err := row.Scan(&blockedA); err != nil {
  354. return BlockedNo, err
  355. }
  356. }
  357. var blockedB bool
  358. if row.Next() {
  359. if err := row.Scan(&blockedB); err != nil {
  360. return BlockedNo, err
  361. }
  362. }
  363. switch {
  364. case blockedA:
  365. return BlockedA, nil
  366. case blockedB:
  367. return BlockedB, nil
  368. default:
  369. return BlockedNo, nil
  370. }
  371. }
  372. // Profile fetches a user profile. Return empty string if the user
  373. // does not exist or has no profile.
  374. func (f SQLiteUserStore) Profile(screenName IdentScreenName) (string, error) {
  375. q := `
  376. SELECT IFNULL(body, '')
  377. FROM profile
  378. WHERE screenName = ?
  379. `
  380. var profile string
  381. err := f.db.QueryRow(q, screenName.String()).Scan(&profile)
  382. if err != nil && !errors.Is(err, sql.ErrNoRows) {
  383. return "", err
  384. }
  385. return profile, nil
  386. }
  387. // SetProfile sets the text contents of a user's profile.
  388. func (f SQLiteUserStore) SetProfile(screenName IdentScreenName, body string) error {
  389. q := `
  390. INSERT INTO profile (screenName, body)
  391. VALUES (?, ?)
  392. ON CONFLICT (screenName)
  393. DO UPDATE SET body = excluded.body
  394. `
  395. _, err := f.db.Exec(q, screenName.String(), body)
  396. return err
  397. }
  398. func (f SQLiteUserStore) BARTUpsert(itemHash []byte, body []byte) error {
  399. q := `
  400. INSERT INTO bartItem (hash, body)
  401. VALUES (?, ?)
  402. ON CONFLICT DO NOTHING
  403. `
  404. _, err := f.db.Exec(q, itemHash, body)
  405. return err
  406. }
  407. func (f SQLiteUserStore) BARTRetrieve(hash []byte) ([]byte, error) {
  408. q := `
  409. SELECT body
  410. FROM bartItem
  411. WHERE hash = ?
  412. `
  413. var body []byte
  414. err := f.db.QueryRow(q, hash).Scan(&body)
  415. if errors.Is(err, sql.ErrNoRows) {
  416. err = nil
  417. }
  418. return body, err
  419. }
  420. // ChatRoomByCookie looks up a chat room by cookie. Returns
  421. // ErrChatRoomNotFound if the room does not exist for cookie.
  422. func (f SQLiteUserStore) ChatRoomByCookie(cookie string) (ChatRoom, error) {
  423. chatRoom := ChatRoom{
  424. Cookie: cookie,
  425. }
  426. q := `
  427. SELECT exchange, name, created, creator
  428. FROM chatRoom
  429. WHERE cookie = ?
  430. `
  431. var creator string
  432. err := f.db.QueryRow(q, cookie).Scan(
  433. &chatRoom.Exchange,
  434. &chatRoom.Name,
  435. &chatRoom.CreateTime,
  436. &creator,
  437. )
  438. if errors.Is(err, sql.ErrNoRows) {
  439. err = ErrChatRoomNotFound
  440. }
  441. chatRoom.Creator = NewIdentScreenName(creator)
  442. return chatRoom, err
  443. }
  444. // ChatRoomByName looks up a chat room by exchange and name. Returns
  445. // ErrChatRoomNotFound if the room does not exist for exchange and name.
  446. func (f SQLiteUserStore) ChatRoomByName(exchange uint16, name string) (ChatRoom, error) {
  447. chatRoom := ChatRoom{
  448. Exchange: exchange,
  449. Name: name,
  450. }
  451. q := `
  452. SELECT cookie, created, creator
  453. FROM chatRoom
  454. WHERE exchange = ? AND name = ?
  455. `
  456. var creator string
  457. err := f.db.QueryRow(q, exchange, name).Scan(
  458. &chatRoom.Cookie,
  459. &chatRoom.CreateTime,
  460. &creator,
  461. )
  462. if errors.Is(err, sql.ErrNoRows) {
  463. err = ErrChatRoomNotFound
  464. }
  465. chatRoom.Creator = NewIdentScreenName(creator)
  466. return chatRoom, err
  467. }
  468. // CreateChatRoom creates a new chat room.
  469. func (f SQLiteUserStore) CreateChatRoom(chatRoom ChatRoom) error {
  470. q := `
  471. INSERT INTO chatRoom (cookie, exchange, name, created, creator)
  472. VALUES (?, ?, ?, ?, ?)
  473. `
  474. _, err := f.db.Exec(
  475. q,
  476. chatRoom.Cookie,
  477. chatRoom.Exchange,
  478. chatRoom.Name,
  479. chatRoom.CreateTime,
  480. chatRoom.Creator.String(),
  481. )
  482. if err != nil {
  483. if sqliteErr, ok := err.(sqlite.Error); ok {
  484. if sqliteErr.ExtendedCode == sqlite.ErrConstraintUnique || sqliteErr.ExtendedCode == sqlite.ErrConstraintPrimaryKey {
  485. err = ErrDupChatRoom
  486. }
  487. }
  488. err = fmt.Errorf("CreateChatRoom: %w", err)
  489. }
  490. return err
  491. }
  492. func (f SQLiteUserStore) AllChatRooms(exchange uint16) ([]ChatRoom, error) {
  493. q := `
  494. SELECT cookie, created, creator, name
  495. FROM chatRoom
  496. WHERE exchange = ?
  497. ORDER BY created ASC
  498. `
  499. rows, err := f.db.Query(q, exchange)
  500. if err != nil {
  501. return nil, err
  502. }
  503. defer rows.Close()
  504. var users []ChatRoom
  505. for rows.Next() {
  506. cr := ChatRoom{
  507. Exchange: exchange,
  508. }
  509. var creator string
  510. if err := rows.Scan(&cr.Cookie, &cr.CreateTime, &creator, &cr.Name); err != nil {
  511. return nil, err
  512. }
  513. cr.Creator = NewIdentScreenName(creator)
  514. users = append(users, cr)
  515. }
  516. if err := rows.Err(); err != nil {
  517. return nil, err
  518. }
  519. return users, nil
  520. }
  521. // UpdateDisplayScreenName updates the user's DisplayScreenName
  522. func (f SQLiteUserStore) UpdateDisplayScreenName(displayScreenName DisplayScreenName) error {
  523. q := `
  524. UPDATE users
  525. SET displayScreenName = ?
  526. WHERE identScreenName = ?
  527. `
  528. _, err := f.db.Exec(q, displayScreenName.String(), displayScreenName.IdentScreenName().String())
  529. return err
  530. }
  531. // UpdateEmailAddress updates the user's EmailAddress
  532. func (f SQLiteUserStore) UpdateEmailAddress(emailAddress *mail.Address, screenName IdentScreenName) error {
  533. q := `
  534. UPDATE users
  535. SET emailAddress = ?
  536. WHERE identScreenName = ?
  537. `
  538. _, err := f.db.Exec(q, emailAddress.Address, screenName.String())
  539. return err
  540. }
  541. // EmailAddressByName retrieves the user's EmailAddress
  542. func (f SQLiteUserStore) EmailAddressByName(screenName IdentScreenName) (*mail.Address, error) {
  543. q := `
  544. SELECT emailAddress
  545. FROM users
  546. WHERE identScreenName = ?
  547. `
  548. var emailAddress string
  549. err := f.db.QueryRow(q, screenName.String()).Scan(&emailAddress)
  550. // username isn't found for some reason
  551. if err != nil && !errors.Is(err, sql.ErrNoRows) {
  552. return nil, err
  553. }
  554. e, err := mail.ParseAddress(emailAddress)
  555. if err != nil {
  556. return nil, fmt.Errorf("%w: %w", ErrNoEmailAddress, err)
  557. }
  558. return e, nil
  559. }
  560. // UpdateRegStatus updates the user's registration status preference
  561. func (f SQLiteUserStore) UpdateRegStatus(regStatus uint16, screenName IdentScreenName) error {
  562. q := `
  563. UPDATE users
  564. SET regStatus = ?
  565. WHERE identScreenName = ?
  566. `
  567. _, err := f.db.Exec(q, regStatus, screenName.String())
  568. return err
  569. }
  570. // RegStatusByName retrieves the user's registration status preference
  571. func (f SQLiteUserStore) RegStatusByName(screenName IdentScreenName) (uint16, error) {
  572. q := `
  573. SELECT regStatus
  574. FROM users
  575. WHERE identScreenName = ?
  576. `
  577. var regStatus uint16
  578. err := f.db.QueryRow(q, screenName.String()).Scan(&regStatus)
  579. // username isn't found for some reason
  580. if err != nil && !errors.Is(err, sql.ErrNoRows) {
  581. return 0, err
  582. }
  583. return regStatus, nil
  584. }
  585. // UpdateConfirmStatus updates the user's confirmation status
  586. func (f SQLiteUserStore) UpdateConfirmStatus(confirmStatus bool, screenName IdentScreenName) error {
  587. q := `
  588. UPDATE users
  589. SET confirmStatus = ?
  590. WHERE identScreenName = ?
  591. `
  592. _, err := f.db.Exec(q, confirmStatus, screenName.String())
  593. return err
  594. }
  595. // ConfirmStatusByName retrieves the user's confirmation status
  596. func (f SQLiteUserStore) ConfirmStatusByName(screenName IdentScreenName) (bool, error) {
  597. q := `
  598. SELECT confirmStatus
  599. FROM users
  600. WHERE identScreenName = ?
  601. `
  602. var confirmStatus bool
  603. err := f.db.QueryRow(q, screenName.String()).Scan(&confirmStatus)
  604. // username isn't found for some reason
  605. if err != nil && !errors.Is(err, sql.ErrNoRows) {
  606. return false, err
  607. }
  608. return confirmStatus, nil
  609. }