| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684 |
- package state
- import (
- "bytes"
- "context"
- "database/sql"
- "embed"
- "encoding/hex"
- "errors"
- "fmt"
- "io/fs"
- "math"
- "net/http"
- "net/mail"
- "strconv"
- "strings"
- "time"
- "github.com/golang-migrate/migrate/v4"
- migratesqlite "github.com/golang-migrate/migrate/v4/database/sqlite"
- "github.com/golang-migrate/migrate/v4/source/httpfs"
- "modernc.org/sqlite"
- lib "modernc.org/sqlite/lib"
- "github.com/mk6i/open-oscar-server/wire"
- )
- const offlineInboxLimit = 10
- var (
- ErrKeywordCategoryExists = errors.New("keyword category already exists")
- ErrKeywordCategoryNotFound = errors.New("keyword category not found")
- ErrBARTItemExists = errors.New("BART asset already exists")
- ErrBARTItemNotFound = errors.New("BART asset not found")
- ErrKeywordExists = errors.New("keyword already exists")
- ErrKeywordInUse = errors.New("can't delete keyword that is associated with a user")
- ErrKeywordNotFound = errors.New("keyword not found")
- ErrOfflineInboxFull = errors.New("offline inbox full")
- errTooManyCategories = errors.New("there are too many keyword categories")
- errTooManyKeywords = errors.New("there are too many keywords")
- // ErrICQSearchEmptyCriteria indicates the caller did not provide any search
- // constraints. This prevents accidental full-table scans.
- ErrICQSearchEmptyCriteria = errors.New("ICQ search criteria is empty")
- )
- //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("sqlite", fmt.Sprintf("file:%s?_pragma=foreign_keys=on", dbFilePath))
- if err != nil {
- return nil, err
- }
- // Set the maximum number of open connections to 1.
- // This is crucial to prevent SQLITE_BUSY errors, which occur when the database
- // is locked due to concurrent access. By limiting the number of open connections
- // to 1, we ensure that all database operations are serialized, thus avoiding
- // any potential locking issues.
- db.SetMaxOpenConns(1)
- store := &SQLiteUserStore{db: db}
- if err := store.runMigrations(); err != nil {
- return nil, fmt.Errorf("failed to run migrations: %w", err)
- }
- return store, nil
- }
- 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 := migratesqlite.WithInstance(f.db, &migratesqlite.Config{})
- if err != nil {
- return fmt.Errorf("cannot create database driver: %v", err)
- }
- m, err := migrate.NewWithInstance("httpfs", sourceInstance, "sqlite", 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
- }
- func (f SQLiteUserStore) AllUsers(ctx context.Context) ([]User, error) {
- q := `SELECT identScreenName, displayScreenName, isICQ, isBot FROM users`
- rows, err := f.db.QueryContext(ctx, q)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var users []User
- for rows.Next() {
- var identSN, displaySN string
- var isICQ, isBot bool
- if err := rows.Scan(&identSN, &displaySN, &isICQ, &isBot); err != nil {
- return nil, err
- }
- users = append(users, User{
- IdentScreenName: NewIdentScreenName(identSN),
- DisplayScreenName: DisplayScreenName(displaySN),
- IsICQ: isICQ,
- IsBot: isBot,
- })
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return users, nil
- }
- func (f SQLiteUserStore) FindByUIN(ctx context.Context, UIN uint32) (User, error) {
- users, err := f.queryUsers(ctx, `identScreenName = ?`, []any{strconv.Itoa(int(UIN))})
- if err != nil {
- return User{}, fmt.Errorf("FindByUIN: %w", err)
- }
- if len(users) == 0 {
- return User{}, ErrNoUser
- }
- return users[0], nil
- }
- func (f SQLiteUserStore) FindByICQEmail(ctx context.Context, email string) (User, error) {
- users, err := f.queryUsers(ctx, `icq_basicInfo_emailAddress = ?`, []any{email})
- if err != nil {
- return User{}, fmt.Errorf("FindByICQEmail: %w", err)
- }
- if len(users) == 0 {
- return User{}, ErrNoUser
- }
- return users[0], nil
- }
- func (f SQLiteUserStore) FindByAIMEmail(ctx context.Context, email string) (User, error) {
- users, err := f.queryUsers(ctx, `emailAddress = ?`, []any{email})
- if err != nil {
- return User{}, fmt.Errorf("FindByAIMEmail: %w", err)
- }
- if len(users) == 0 {
- return User{}, ErrNoUser
- }
- return users[0], nil
- }
- func (f SQLiteUserStore) FindByAIMKeyword(ctx context.Context, keyword string) ([]User, error) {
- where := `
- (SELECT id FROM aimKeyword WHERE name = ?) IN
- (aim_keyword1, aim_keyword2, aim_keyword3, aim_keyword4, aim_keyword5)
- `
- users, err := f.queryUsers(ctx, where, []any{keyword})
- if err != nil {
- return nil, err
- }
- return users, nil
- }
- func (f SQLiteUserStore) FindByICQName(ctx context.Context, firstName, lastName, nickName string) ([]User, error) {
- var args []any
- var clauses []string
- if firstName != "" {
- args = append(args, firstName)
- clauses = append(clauses, `LOWER(icq_basicInfo_firstName) = LOWER(?)`)
- }
- if lastName != "" {
- args = append(args, lastName)
- clauses = append(clauses, `LOWER(icq_basicInfo_lastName) = LOWER(?)`)
- }
- if nickName != "" {
- args = append(args, nickName)
- clauses = append(clauses, `LOWER(icq_basicInfo_nickName) = LOWER(?)`)
- }
- whereClause := strings.Join(clauses, " AND ")
- users, err := f.queryUsers(ctx, whereClause, args)
- if err != nil {
- return nil, fmt.Errorf("FindByICQName: %w", err)
- }
- return users, nil
- }
- func (f SQLiteUserStore) FindByAIMNameAndAddr(ctx context.Context, info AIMNameAndAddr) ([]User, error) {
- var args []any
- var clauses []string
- if info.FirstName != "" {
- args = append(args, info.FirstName)
- clauses = append(clauses, `LOWER(aim_firstName) = LOWER(?)`)
- }
- if info.LastName != "" {
- args = append(args, info.LastName)
- clauses = append(clauses, `LOWER(aim_lastName) = LOWER(?)`)
- }
- if info.MiddleName != "" {
- args = append(args, info.MiddleName)
- clauses = append(clauses, `LOWER(aim_middleName) = LOWER(?)`)
- }
- if info.MaidenName != "" {
- args = append(args, info.MaidenName)
- clauses = append(clauses, `LOWER(aim_maidenName) = LOWER(?)`)
- }
- if info.Country != "" {
- args = append(args, info.Country)
- clauses = append(clauses, `LOWER(aim_country) = LOWER(?)`)
- }
- if info.State != "" {
- args = append(args, info.State)
- clauses = append(clauses, `LOWER(aim_state) = LOWER(?)`)
- }
- if info.City != "" {
- args = append(args, info.City)
- clauses = append(clauses, `LOWER(aim_city) = LOWER(?)`)
- }
- if info.NickName != "" {
- args = append(args, info.NickName)
- clauses = append(clauses, `LOWER(aim_nickName) = LOWER(?)`)
- }
- if info.ZIPCode != "" {
- args = append(args, info.ZIPCode)
- clauses = append(clauses, `LOWER(aim_zipCode) = LOWER(?)`)
- }
- if info.Address != "" {
- args = append(args, info.Address)
- clauses = append(clauses, `LOWER(aim_address) = LOWER(?)`)
- }
- whereClause := strings.Join(clauses, " AND ")
- users, err := f.queryUsers(ctx, whereClause, args)
- if err != nil {
- return nil, fmt.Errorf("FindByAIMNameAndAddr: %w", err)
- }
- return users, nil
- }
- // icqInterestsWhereClause builds SQL and args for matching a category code and
- // keyword(s) against the four stored interest slots (same semantics as
- // FindByICQInterests).
- func icqInterestsWhereClause(code uint16, keywords []string) (cond string, args []any) {
- var clauses []string
- for i := 1; i <= 4; i++ {
- var subClauses []string
- args = append(args, code)
- for _, key := range keywords {
- subClauses = append(subClauses, fmt.Sprintf("icq_interests_keyword%d LIKE ?", i))
- args = append(args, "%"+key+"%")
- }
- clauses = append(clauses, fmt.Sprintf("(icq_interests_code%d = ? AND (%s))", i, strings.Join(subClauses, " OR ")))
- }
- return strings.Join(clauses, " OR "), args
- }
- // icqAffiliationsWhereClause builds SQL and args for matching a category code and
- // keyword(s) across the six stored affiliation slots (three current + three past),
- // using the same OR-of-slots pattern as icqInterestsWhereClause.
- func icqAffiliationsWhereClause(code uint16, keywords []string, slots []struct{ codeCol, keyCol string }) (cond string, args []any) {
- var clauses []string
- for _, slot := range slots {
- var subClauses []string
- args = append(args, code)
- for _, key := range keywords {
- subClauses = append(subClauses, fmt.Sprintf("%s LIKE ?", slot.keyCol))
- args = append(args, "%"+key+"%")
- }
- clauses = append(clauses, fmt.Sprintf("(%s = ? AND (%s))", slot.codeCol, strings.Join(subClauses, " OR ")))
- }
- return strings.Join(clauses, " OR "), args
- }
- func (f SQLiteUserStore) FindByICQInterests(ctx context.Context, code uint16, keywords []string) ([]User, error) {
- cond, args := icqInterestsWhereClause(code, keywords)
- users, err := f.queryUsers(ctx, cond, args)
- if err != nil {
- return nil, fmt.Errorf("FindByICQInterests: %w", err)
- }
- return users, nil
- }
- func (f SQLiteUserStore) FindByICQKeyword(ctx context.Context, keyword string) ([]User, error) {
- var args []any
- var clauses []string
- for i := 1; i <= 4; i++ {
- args = append(args, "%"+keyword+"%")
- clauses = append(clauses, fmt.Sprintf("icq_interests_keyword%d LIKE ?", i))
- }
- whereClause := strings.Join(clauses, " OR ")
- users, err := f.queryUsers(ctx, whereClause, args)
- if err != nil {
- return nil, fmt.Errorf("FindByICQKeyword: %w", err)
- }
- return users, nil
- }
- // ICQUserSearchCriteria represents an AND-combined set of filters for ICQ
- // white-pages style searches (SNAC(15,02) / 0x07D0 / 0x055F).
- //
- // Fields are pointers so callers can distinguish "unset" from "set to the
- // zero value". For example, MinAge == nil means no minimum age constraint,
- // while MinAge != nil && *MinAge == 0 means the client explicitly set 0.
- type ICQUserSearchCriteria struct {
- // Identity
- UIN *uint32
- // Basic name/email (case-insensitive substring matches). When non-nil, the
- // string is non-empty.
- FirstName *string
- LastName *string
- NickName *string
- Email *string
- // Demographics
- MinAge *uint16
- MaxAge *uint16
- Gender *uint8
- SpokenLanguage *uint8
- // Location / work (non-nil *string fields are non-empty)
- City *string
- State *string
- CountryCode *uint16
- Company *string // non-nil => non-empty
- Position *string // non-nil => non-empty
- DepartmentName *string // non-nil => non-empty
- OccupationCode *uint16
- // Directory nodes (code + keywords). InterestsCode and InterestsKeywords are
- // either both unset or both set; when code is non-nil, keywords must be non-empty.
- InterestsCode *uint16
- InterestsKeywords []string
- // AffiliationsCode and AffiliationsKeywords follow the same pairing rules as
- // InterestsCode / InterestsKeywords.
- AffiliationsCode *uint16
- AffiliationsKeywords []string
- // PastAffiliationsCode and PastAffiliationsKeywords follow the same pairing rules as
- // InterestsCode / InterestsKeywords.
- PastAffiliationsCode *uint16
- PastAffiliationsKeywords []string
- HomePageCategoryIndex *uint16
- HomePageKeywords []string
- // Whitepages search keywords string (TLV 0x0226). This is modeled as a
- // global keyword across searchable profile fields.
- AnyKeyword *string
- }
- // SearchICQUsers returns ICQ users whose stored profile fields satisfy every
- // non-empty constraint in c. Predicates are AND-combined. Only accounts with
- // isICQ = 1 are considered.
- //
- // If c contains no constraints, SearchICQUsers returns [ErrICQSearchEmptyCriteria].
- func (f SQLiteUserStore) SearchICQUsers(ctx context.Context, c ICQUserSearchCriteria) ([]User, error) {
- var args []any
- var clauses []string
- appendLike := func(column string, val string) {
- args = append(args, "%"+val+"%")
- clauses = append(clauses, fmt.Sprintf(`LOWER(%s) LIKE LOWER(?)`, column))
- }
- if c.UIN != nil {
- args = append(args, strconv.Itoa(int(*c.UIN)))
- clauses = append(clauses, `identScreenName = ?`)
- }
- if c.FirstName != nil {
- appendLike("icq_basicInfo_firstName", *c.FirstName)
- }
- if c.LastName != nil {
- appendLike("icq_basicInfo_lastName", *c.LastName)
- }
- if c.NickName != nil {
- appendLike("icq_basicInfo_nickName", *c.NickName)
- }
- if c.Email != nil {
- appendLike("icq_basicInfo_emailAddress", *c.Email)
- }
- if c.Gender != nil {
- args = append(args, *c.Gender)
- clauses = append(clauses, `icq_moreInfo_gender = ?`)
- }
- if c.SpokenLanguage != nil {
- args = append(args, *c.SpokenLanguage, *c.SpokenLanguage, *c.SpokenLanguage)
- clauses = append(clauses, `(icq_moreInfo_lang1 = ? OR icq_moreInfo_lang2 = ? OR icq_moreInfo_lang3 = ?)`)
- }
- if c.MinAge != nil {
- args = append(args, *c.MinAge)
- clauses = append(clauses, `(icq_moreInfo_birthYear > 0 AND (CAST(strftime('%Y','now') AS INTEGER) - icq_moreInfo_birthYear) >= ?)`)
- }
- if c.MaxAge != nil {
- args = append(args, *c.MaxAge)
- clauses = append(clauses, `(icq_moreInfo_birthYear > 0 AND (CAST(strftime('%Y','now') AS INTEGER) - icq_moreInfo_birthYear) <= ?)`)
- }
- if c.City != nil {
- appendLike("icq_basicInfo_city", *c.City)
- }
- if c.State != nil {
- args = append(args, "%"+*c.State+"%", "%"+*c.State+"%")
- clauses = append(clauses, `(LOWER(icq_basicInfo_state) LIKE LOWER(?) OR LOWER(icq_workInfo_state) LIKE LOWER(?))`)
- }
- if c.CountryCode != nil {
- args = append(args, *c.CountryCode, *c.CountryCode)
- clauses = append(clauses, `(icq_basicInfo_countryCode = ? OR icq_workInfo_countryCode = ?)`)
- }
- if c.Company != nil {
- appendLike("icq_workInfo_company", *c.Company)
- }
- if c.DepartmentName != nil {
- appendLike("icq_workInfo_department", *c.DepartmentName)
- }
- if c.Position != nil {
- appendLike("icq_workInfo_position", *c.Position)
- }
- if c.OccupationCode != nil {
- args = append(args, *c.OccupationCode)
- clauses = append(clauses, `icq_workInfo_occupationCode = ?`)
- }
- if c.InterestsCode != nil {
- intCond, intArgs := icqInterestsWhereClause(*c.InterestsCode, c.InterestsKeywords)
- clauses = append(clauses, "("+intCond+")")
- args = append(args, intArgs...)
- }
- if c.AffiliationsCode != nil {
- slots := []struct{ codeCol, keyCol string }{
- {"icq_affiliations_currentCode1", "icq_affiliations_currentKeyword1"},
- {"icq_affiliations_currentCode2", "icq_affiliations_currentKeyword2"},
- {"icq_affiliations_currentCode3", "icq_affiliations_currentKeyword3"},
- }
- affCond, affArgs := icqAffiliationsWhereClause(*c.AffiliationsCode, c.AffiliationsKeywords, slots)
- clauses = append(clauses, "("+affCond+")")
- args = append(args, affArgs...)
- }
- if c.PastAffiliationsCode != nil {
- slots := []struct{ codeCol, keyCol string }{
- {"icq_affiliations_pastCode1", "icq_affiliations_pastKeyword1"},
- {"icq_affiliations_pastCode2", "icq_affiliations_pastKeyword2"},
- {"icq_affiliations_pastCode3", "icq_affiliations_pastKeyword3"},
- }
- affCond, affArgs := icqAffiliationsWhereClause(*c.PastAffiliationsCode, c.PastAffiliationsKeywords, slots)
- clauses = append(clauses, "("+affCond+")")
- args = append(args, affArgs...)
- }
- if c.HomePageCategoryIndex != nil {
- args = append(args, *c.HomePageCategoryIndex)
- clauses = append(clauses, `icq_homepageCategory_index = ?`)
- }
- if len(c.HomePageKeywords) > 0 {
- var kwClauses []string
- for _, kw := range c.HomePageKeywords {
- kwClauses = append(kwClauses, `LOWER(icq_homepageCategory_description) LIKE LOWER(?)`)
- args = append(args, "%"+kw+"%")
- }
- clauses = append(clauses, fmt.Sprintf("(%s)", strings.Join(kwClauses, " OR ")))
- }
- if c.AnyKeyword != nil {
- var kwClauses []string
- for i := 1; i <= 4; i++ {
- kwClauses = append(kwClauses, fmt.Sprintf("icq_interests_keyword%d LIKE ?", i))
- args = append(args, "%"+*c.AnyKeyword+"%")
- }
- clauses = append(clauses, fmt.Sprintf("(%s)", strings.Join(kwClauses, " OR ")))
- }
- if len(clauses) == 0 {
- return nil, ErrICQSearchEmptyCriteria
- }
- clauses = append(clauses, `isICQ = 1`)
- whereClause := strings.Join(clauses, " AND ")
- users, err := f.queryUsers(ctx, whereClause, args)
- if err != nil {
- return nil, fmt.Errorf("SearchICQUsers: %w", err)
- }
- return users, nil
- }
- func (f SQLiteUserStore) User(ctx context.Context, screenName IdentScreenName) (*User, error) {
- users, err := f.queryUsers(ctx, `identScreenName = ?`, []any{screenName.String()})
- if err != nil {
- return nil, fmt.Errorf("User: %w", err)
- }
- if len(users) == 0 {
- return nil, nil
- }
- return &users[0], nil
- }
- // RequiresAuthorization reports whether adding owner as a contact by requester
- // is still blocked: owner requires authorization and requester does not have a
- // pre-authorization grant from owner.
- func (f SQLiteUserStore) RequiresAuthorization(ctx context.Context, owner, requester IdentScreenName) (bool, error) {
- u, err := f.User(ctx, owner)
- if err != nil {
- return false, fmt.Errorf("RequiresAuthorization: %w", err)
- }
- if u == nil || !u.ICQInfo.Permissions.AuthRequired {
- return false, nil
- }
- var one int
- err = f.db.QueryRowContext(ctx,
- `SELECT 1 FROM contactPreauth WHERE ownerScreenName = ? AND authorizedScreenName = ? LIMIT 1`,
- owner.String(), requester.String(),
- ).Scan(&one)
- if errors.Is(err, sql.ErrNoRows) {
- return true, nil
- }
- if err != nil {
- return false, fmt.Errorf("RequiresAuthorization: %w", err)
- }
- return false, nil
- }
- // RecordPreAuth records that owner has pre-authorized requester to add owner
- // without a further authorization prompt. No-ops when either user is not
- // registered. The operation is idempotent.
- func (f SQLiteUserStore) RecordPreAuth(ctx context.Context, owner, requester IdentScreenName) error {
- _, err := f.db.ExecContext(ctx,
- `INSERT OR IGNORE INTO contactPreauth (ownerScreenName, authorizedScreenName, createdAt)
- VALUES (?, ?, UNIXEPOCH())`,
- owner.String(), requester.String(),
- )
- if err != nil {
- if sqliteErr, ok := err.(*sqlite.Error); ok && sqliteErr.Code() == lib.SQLITE_CONSTRAINT_FOREIGNKEY {
- return nil
- }
- return fmt.Errorf("RecordPreAuth: %w", err)
- }
- return nil
- }
- // HasBuddyAddedNotification reports whether the server has already sent a
- // "you were added" notification for requester being added by granter.
- func (f SQLiteUserStore) HasBuddyAddedNotification(ctx context.Context, granter, requester IdentScreenName) (bool, error) {
- var one int
- err := f.db.QueryRowContext(ctx,
- `SELECT 1 FROM buddyAddedNotifications WHERE granterScreenName = ? AND requesterScreenName = ? LIMIT 1`,
- granter.String(), requester.String(),
- ).Scan(&one)
- if errors.Is(err, sql.ErrNoRows) {
- return false, nil
- }
- if err != nil {
- return false, fmt.Errorf("HasBuddyAddedNotification: %w", err)
- }
- return true, nil
- }
- // RecordBuddyAddedNotification records that the server has sent a "you were added"
- // notification for requester being added by granter. No-ops when either user is
- // not registered. The operation is idempotent.
- func (f SQLiteUserStore) RecordBuddyAddedNotification(ctx context.Context, granter, requester IdentScreenName) error {
- _, err := f.db.ExecContext(ctx,
- `INSERT OR IGNORE INTO buddyAddedNotifications (granterScreenName, requesterScreenName, createdAt) VALUES (?, ?, UNIXEPOCH())`,
- granter.String(), requester.String(),
- )
- if err != nil {
- if sqliteErr, ok := err.(*sqlite.Error); ok && sqliteErr.Code() == lib.SQLITE_CONSTRAINT_FOREIGNKEY {
- return nil
- }
- return fmt.Errorf("RecordBuddyAddedNotification: %w", err)
- }
- return nil
- }
- // queryUsers retrieves a list of users from the database based on the
- // specified WHERE clause and query parameters. Returns a slice of User objects
- // or an error if the query fails.
- func (f SQLiteUserStore) queryUsers(ctx context.Context, whereClause string, queryParams []any) ([]User, error) {
- q := `
- SELECT
- identScreenName,
- displayScreenName,
- emailAddress,
- authKey,
- strongMD5Pass,
- weakMD5Pass,
- confirmStatus,
- regStatus,
- suspendedStatus,
- isBot,
- isICQ,
- icq_affiliations_currentCode1,
- icq_affiliations_currentCode2,
- icq_affiliations_currentCode3,
- icq_affiliations_currentKeyword1,
- icq_affiliations_currentKeyword2,
- icq_affiliations_currentKeyword3,
- icq_affiliations_pastCode1,
- icq_affiliations_pastCode2,
- icq_affiliations_pastCode3,
- icq_affiliations_pastKeyword1,
- icq_affiliations_pastKeyword2,
- icq_affiliations_pastKeyword3,
- icq_basicInfo_address,
- icq_basicInfo_cellPhone,
- icq_basicInfo_city,
- icq_basicInfo_countryCode,
- icq_basicInfo_emailAddress,
- icq_basicInfo_fax,
- icq_basicInfo_firstName,
- icq_basicInfo_gmtOffset,
- icq_basicInfo_lastName,
- icq_basicInfo_nickName,
- icq_basicInfo_phone,
- icq_basicInfo_publishEmail,
- icq_basicInfo_state,
- icq_basicInfo_zipCode,
- icq_basicInfo_originCity,
- icq_basicInfo_originState,
- icq_basicInfo_originCountryCode,
- icq_interests_code1,
- icq_interests_code2,
- icq_interests_code3,
- icq_interests_code4,
- icq_interests_keyword1,
- icq_interests_keyword2,
- icq_interests_keyword3,
- icq_interests_keyword4,
- icq_moreInfo_birthDay,
- icq_moreInfo_birthMonth,
- icq_moreInfo_birthYear,
- icq_moreInfo_gender,
- icq_moreInfo_homePageAddr,
- icq_moreInfo_lang1,
- icq_moreInfo_lang2,
- icq_moreInfo_lang3,
- icq_notes,
- icq_permissions_authRequired,
- icq_permissions_webAware,
- icq_permissions_allowSpam,
- icq_workInfo_address,
- icq_workInfo_city,
- icq_workInfo_company,
- icq_workInfo_countryCode,
- icq_workInfo_department,
- icq_workInfo_fax,
- icq_workInfo_occupationCode,
- icq_workInfo_phone,
- icq_workInfo_position,
- icq_workInfo_state,
- icq_workInfo_webPage,
- icq_workInfo_zipCode,
- icq_homepageCategory_enabled,
- icq_homepageCategory_index,
- icq_homepageCategory_description,
- aim_firstName,
- aim_lastName,
- aim_middleName,
- aim_maidenName,
- aim_country,
- aim_state,
- aim_city,
- aim_nickName,
- aim_zipCode,
- aim_address,
- tocConfig,
- lastWarnUpdate,
- lastWarnLevel,
- offlineMsgCount
- FROM users
- WHERE %s
- `
- q = fmt.Sprintf(q, whereClause)
- rows, err := f.db.QueryContext(ctx, q, queryParams...)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var users []User
- for rows.Next() {
- var u User
- var sn string
- var lastWarnUpdateUnix int64
- err := rows.Scan(
- &sn,
- &u.DisplayScreenName,
- &u.EmailAddress,
- &u.AuthKey,
- &u.StrongMD5Pass,
- &u.WeakMD5Pass,
- &u.ConfirmStatus,
- &u.RegStatus,
- &u.SuspendedStatus,
- &u.IsBot,
- &u.IsICQ,
- &u.ICQInfo.Affiliations.CurrentCode1,
- &u.ICQInfo.Affiliations.CurrentCode2,
- &u.ICQInfo.Affiliations.CurrentCode3,
- &u.ICQInfo.Affiliations.CurrentKeyword1,
- &u.ICQInfo.Affiliations.CurrentKeyword2,
- &u.ICQInfo.Affiliations.CurrentKeyword3,
- &u.ICQInfo.Affiliations.PastCode1,
- &u.ICQInfo.Affiliations.PastCode2,
- &u.ICQInfo.Affiliations.PastCode3,
- &u.ICQInfo.Affiliations.PastKeyword1,
- &u.ICQInfo.Affiliations.PastKeyword2,
- &u.ICQInfo.Affiliations.PastKeyword3,
- &u.ICQInfo.Basic.Address,
- &u.ICQInfo.Basic.CellPhone,
- &u.ICQInfo.Basic.City,
- &u.ICQInfo.Basic.CountryCode,
- &u.ICQInfo.Basic.EmailAddress,
- &u.ICQInfo.Basic.Fax,
- &u.ICQInfo.Basic.FirstName,
- &u.ICQInfo.Basic.GMTOffset,
- &u.ICQInfo.Basic.LastName,
- &u.ICQInfo.Basic.Nickname,
- &u.ICQInfo.Basic.Phone,
- &u.ICQInfo.Basic.PublishEmail,
- &u.ICQInfo.Basic.State,
- &u.ICQInfo.Basic.ZIPCode,
- &u.ICQInfo.Basic.OriginallyFromCity,
- &u.ICQInfo.Basic.OriginallyFromState,
- &u.ICQInfo.Basic.OriginallyFromCountryCode,
- &u.ICQInfo.Interests.Code1,
- &u.ICQInfo.Interests.Code2,
- &u.ICQInfo.Interests.Code3,
- &u.ICQInfo.Interests.Code4,
- &u.ICQInfo.Interests.Keyword1,
- &u.ICQInfo.Interests.Keyword2,
- &u.ICQInfo.Interests.Keyword3,
- &u.ICQInfo.Interests.Keyword4,
- &u.ICQInfo.More.BirthDay,
- &u.ICQInfo.More.BirthMonth,
- &u.ICQInfo.More.BirthYear,
- &u.ICQInfo.More.Gender,
- &u.ICQInfo.More.HomePageAddr,
- &u.ICQInfo.More.Lang1,
- &u.ICQInfo.More.Lang2,
- &u.ICQInfo.More.Lang3,
- &u.ICQInfo.Notes.Notes,
- &u.ICQInfo.Permissions.AuthRequired,
- &u.ICQInfo.Permissions.WebAware,
- &u.ICQInfo.Permissions.AllowSpam,
- &u.ICQInfo.Work.Address,
- &u.ICQInfo.Work.City,
- &u.ICQInfo.Work.Company,
- &u.ICQInfo.Work.CountryCode,
- &u.ICQInfo.Work.Department,
- &u.ICQInfo.Work.Fax,
- &u.ICQInfo.Work.OccupationCode,
- &u.ICQInfo.Work.Phone,
- &u.ICQInfo.Work.Position,
- &u.ICQInfo.Work.State,
- &u.ICQInfo.Work.WebPage,
- &u.ICQInfo.Work.ZIPCode,
- &u.ICQInfo.HomepageCategory.Enabled,
- &u.ICQInfo.HomepageCategory.Index,
- &u.ICQInfo.HomepageCategory.Description,
- &u.AIMDirectoryInfo.FirstName,
- &u.AIMDirectoryInfo.LastName,
- &u.AIMDirectoryInfo.MiddleName,
- &u.AIMDirectoryInfo.MaidenName,
- &u.AIMDirectoryInfo.Country,
- &u.AIMDirectoryInfo.State,
- &u.AIMDirectoryInfo.City,
- &u.AIMDirectoryInfo.NickName,
- &u.AIMDirectoryInfo.ZIPCode,
- &u.AIMDirectoryInfo.Address,
- &u.TOCConfig,
- &lastWarnUpdateUnix,
- &u.LastWarnLevel,
- &u.OfflineMsgCount,
- )
- if err != nil {
- return nil, err
- }
- u.IdentScreenName = NewIdentScreenName(sn)
- u.LastWarnUpdate = time.Unix(lastWarnUpdateUnix, 0).UTC()
- users = append(users, u)
- }
- if err = rows.Err(); err != nil {
- return nil, err
- }
- return users, nil
- }
- func (f SQLiteUserStore) InsertUser(ctx context.Context, u User) error {
- if u.DisplayScreenName.IsUIN() && !u.IsICQ {
- return errors.New("inserting user with UIN and isICQ=false")
- }
- q := `
- INSERT INTO users (identScreenName, displayScreenName, authKey, weakMD5Pass, strongMD5Pass, isICQ, isBot)
- VALUES (?, ?, ?, ?, ?, ?, ?)
- ON CONFLICT (identScreenName) DO NOTHING
- `
- result, err := f.db.ExecContext(ctx,
- q,
- u.IdentScreenName.String(),
- u.DisplayScreenName,
- u.AuthKey,
- u.WeakMD5Pass,
- u.StrongMD5Pass,
- u.IsICQ,
- u.IsBot,
- )
- if err != nil {
- return err
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return err
- }
- if rowsAffected == 0 {
- return ErrDupUser
- }
- return nil
- }
- func (f SQLiteUserStore) DeleteUser(ctx context.Context, screenName IdentScreenName) error {
- q := `
- DELETE FROM users WHERE identScreenName = ?
- `
- result, err := f.db.ExecContext(ctx, q, screenName.String())
- if err != nil {
- return err
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return err
- }
- if rowsAffected == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) SetUserPassword(ctx context.Context, screenName IdentScreenName, newPassword string) error {
- tx, err := f.db.Begin()
- if err != nil {
- return err
- }
- defer func() {
- if err != nil {
- err = errors.Join(err, tx.Rollback())
- }
- }()
- q := `
- SELECT
- authKey,
- isICQ
- FROM users
- WHERE identScreenName = ?
- `
- u := User{}
- err = tx.QueryRowContext(ctx, q, screenName.String()).Scan(
- &u.AuthKey,
- &u.IsICQ,
- )
- if errors.Is(err, sql.ErrNoRows) {
- return ErrNoUser
- }
- if err = u.HashPassword(newPassword); err != nil {
- return err
- }
- q = `
- UPDATE users
- SET authKey = ?, weakMD5Pass = ?, strongMD5Pass = ?
- WHERE identScreenName = ?
- `
- result, err := tx.ExecContext(ctx, q, u.AuthKey, u.WeakMD5Pass, u.StrongMD5Pass, screenName.String())
- if err != nil {
- return err
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return err
- }
- 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.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE identScreenName = ?", u.IdentScreenName.String()).Scan(&exists)
- if err != nil {
- return err // Handle possible SQL errors during the select
- }
- if exists == 0 {
- err = ErrNoUser // User does not exist
- return err
- }
- }
- return tx.Commit()
- }
- func (f SQLiteUserStore) Feedbag(ctx context.Context, screenName IdentScreenName) ([]wire.FeedbagItem, error) {
- q := `
- SELECT
- groupID,
- itemID,
- classID,
- name,
- attributes
- FROM feedbag
- WHERE screenName = ?
- `
- rows, err := f.db.QueryContext(ctx, 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.UnmarshalBE(&item.TLVLBlock, bytes.NewBuffer(attrs)); err != nil {
- return items, err
- }
- items = append(items, item)
- }
- return items, nil
- }
- func (f SQLiteUserStore) FeedbagLastModified(ctx context.Context, screenName IdentScreenName) (time.Time, error) {
- var lastModified sql.NullInt64
- q := `SELECT MAX(lastModified) FROM feedbag WHERE screenName = ?`
- err := f.db.QueryRowContext(ctx, q, screenName.String()).Scan(&lastModified)
- return time.Unix(lastModified.Int64, 0), err
- }
- func (f SQLiteUserStore) FeedbagDelete(ctx context.Context, screenName IdentScreenName, items []wire.FeedbagItem) error {
- // todo add transaction
- q := `DELETE FROM feedbag WHERE screenName = ? AND itemID = ? AND groupID = ?`
- for _, item := range items {
- if _, err := f.db.ExecContext(ctx, q, screenName.String(), item.ItemID, item.GroupID); err != nil {
- return err
- }
- }
- return nil
- }
- func (f SQLiteUserStore) FeedbagUpsert(ctx context.Context, screenName IdentScreenName, items []wire.FeedbagItem) error {
- q := `
- INSERT INTO feedbag (screenName, groupID, itemID, classID, name, attributes, pdMode, authPending, lastModified)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, UNIXEPOCH())
- ON CONFLICT (screenName, groupID, itemID)
- DO UPDATE SET classID = excluded.classID,
- name = excluded.name,
- attributes = excluded.attributes,
- pdMode = excluded.pdMode,
- authPending = excluded.authPending,
- lastModified = UNIXEPOCH()
- `
- for _, item := range items {
- buf := &bytes.Buffer{}
- if err := wire.MarshalBE(item.TLVLBlock, buf); err != nil {
- return err
- }
- if item.ClassID == wire.FeedbagClassIdBuddy ||
- item.ClassID == wire.FeedbagClassIDPermit ||
- item.ClassID == wire.FeedbagClassIdAlInfo ||
- item.ClassID == wire.FeedbagClassIDDeny {
- // insert screen name identifier
- item.Name = NewIdentScreenName(item.Name).String()
- }
- pdMode := uint8(0)
- if item.ClassID == wire.FeedbagClassIdPdinfo {
- var hasMode bool
- pdMode, hasMode = item.Uint8(wire.FeedbagAttributesPdMode)
- if !hasMode {
- // by default, QIP sends a PD info item entry with no mode
- pdMode = uint8(wire.FeedbagPDModePermitAll)
- }
- }
- authPending := item.ClassID == wire.FeedbagClassIdBuddy &&
- item.HasTag(wire.FeedbagAttributesPending)
- _, err := f.db.ExecContext(ctx,
- q,
- screenName.String(),
- item.GroupID,
- item.ItemID,
- item.ClassID,
- item.Name,
- buf.Bytes(),
- pdMode,
- authPending)
- if err != nil {
- return err
- }
- }
- return nil
- }
- func (f SQLiteUserStore) ClearBuddyListRegistry(ctx context.Context) error {
- if _, err := f.db.ExecContext(ctx, `DELETE FROM buddyListMode`); err != nil {
- return err
- }
- if _, err := f.db.ExecContext(ctx, `DELETE FROM clientSideBuddyList`); err != nil {
- return err
- }
- return nil
- }
- func (f SQLiteUserStore) RegisterBuddyList(ctx context.Context, user IdentScreenName) error {
- q := `
- INSERT INTO buddyListMode (screenName, clientSidePDMode) VALUES(?, ?)
- ON CONFLICT (screenName) DO NOTHING
- `
- _, err := f.db.ExecContext(ctx, q, user.String(), wire.FeedbagPDModePermitAll)
- return err
- }
- func (f SQLiteUserStore) UnregisterBuddyList(ctx context.Context, user IdentScreenName) error {
- if _, err := f.db.ExecContext(ctx, `DELETE FROM buddyListMode WHERE screenName = ?`, user.String()); err != nil {
- return err
- }
- if _, err := f.db.ExecContext(ctx, `DELETE FROM clientSideBuddyList WHERE me = ?`, user.String()); err != nil {
- return err
- }
- return nil
- }
- func (f SQLiteUserStore) UseFeedbag(ctx context.Context, screenName IdentScreenName) error {
- q := `
- INSERT INTO buddyListMode (screenName, useFeedbag)
- VALUES (?, ?)
- ON CONFLICT (screenName)
- DO UPDATE SET clientSidePDMode = 0,
- useFeedbag = true
- `
- _, err := f.db.ExecContext(ctx, q, screenName.String(), true)
- return err
- }
- func (f SQLiteUserStore) SetPDMode(ctx context.Context, me IdentScreenName, pdMode wire.FeedbagPDMode) error {
- alreadySet, err := f.isPDModeEqual(ctx, me, pdMode)
- if err != nil {
- return fmt.Errorf("isPDModeEqual: %w", err)
- }
- if alreadySet {
- return nil
- }
- tx, err := f.db.Begin()
- if err != nil {
- return err
- }
- defer func() {
- _ = tx.Rollback()
- }()
- if err := setClientSidePDMode(ctx, tx, me, pdMode); err != nil {
- return fmt.Errorf("setClientSidePDMode: %w", err)
- }
- if err := clearClientSidePDFlags(ctx, tx, me, pdMode); err != nil {
- return fmt.Errorf("clearClientSidePDFlags: %w", err)
- }
- if err := clearBlankClientSideBuddies(ctx, tx, me, pdMode); err != nil {
- return fmt.Errorf("clearBlankClientSideBuddies: %w", err)
- }
- if err := tx.Commit(); err != nil {
- return fmt.Errorf("commit: %w", err)
- }
- return nil
- }
- // isPDModeEqual indicates whether the current permit/deny mode is already set
- // to pdMode.
- func (f SQLiteUserStore) isPDModeEqual(ctx context.Context, me IdentScreenName, pdMode wire.FeedbagPDMode) (bool, error) {
- q := `
- SELECT true
- FROM buddyListMode
- WHERE screenName = ? AND clientSidePDMode = ?
- `
- var isEqual bool
- err := f.db.QueryRowContext(ctx, q, me.String(), pdMode).Scan(&isEqual)
- if err != nil && !errors.Is(err, sql.ErrNoRows) {
- return false, err
- }
- return isEqual, nil
- }
- // setClientSidePDMode sets the permit/deny mode for my client-side buddy list.
- func setClientSidePDMode(ctx context.Context, tx *sql.Tx, me IdentScreenName, pdMode wire.FeedbagPDMode) error {
- q := `
- INSERT INTO buddyListMode (screenName, clientSidePDMode) VALUES(?, ?)
- ON CONFLICT (screenName)
- DO UPDATE SET clientSidePDMode = excluded.clientSidePDMode
- `
- _, err := tx.ExecContext(ctx, q, me.String(), pdMode)
- if err != nil {
- return err
- }
- return nil
- }
- // clearBlankClientSideBuddies removes client-side buddy where all flags
- // (isBuddy, isPermit, isDeny) are false.
- func clearBlankClientSideBuddies(ctx context.Context, tx *sql.Tx, me IdentScreenName, pdMode wire.FeedbagPDMode) error {
- q := `
- DELETE FROM clientSideBuddyList
- WHERE isBuddy IS FALSE
- AND isPermit IS FALSE
- AND isDeny IS FALSE
- AND me = ?
- `
- _, err := tx.ExecContext(ctx, q, me.String(), pdMode)
- return err
- }
- // clearClientSidePDFlags clears permit/deny flags.
- func clearClientSidePDFlags(ctx context.Context, tx *sql.Tx, me IdentScreenName, pdMode wire.FeedbagPDMode) error {
- q := `
- UPDATE clientSideBuddyList
- SET isDeny = false, isPermit = false
- WHERE me = ?
- `
- _, err := tx.ExecContext(ctx, q, me.String(), pdMode)
- return err
- }
- func (f SQLiteUserStore) AddBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
- q := `
- INSERT INTO clientSideBuddyList (me, them, isBuddy)
- VALUES (?, ?, true)
- ON CONFLICT (me, them) DO UPDATE SET isBuddy = true
- `
- _, err := f.db.ExecContext(ctx, q, me.String(), them.String())
- return err
- }
- func (f SQLiteUserStore) RemoveBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
- q := `
- UPDATE clientSideBuddyList
- SET isBuddy = false
- WHERE me = ?
- AND them = ?
- `
- _, err := f.db.ExecContext(ctx, q, me.String(), them.String())
- return err
- }
- func (f SQLiteUserStore) DenyBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
- q := `
- INSERT INTO clientSideBuddyList (me, them, isDeny)
- VALUES (?, ?, 1)
- ON CONFLICT (me, them) DO UPDATE SET isDeny = 1
- `
- _, err := f.db.ExecContext(ctx, q, me.String(), them.String())
- return err
- }
- func (f SQLiteUserStore) RemoveDenyBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
- q := `
- UPDATE clientSideBuddyList
- SET isDeny = false
- WHERE me = ?
- AND them = ?
- `
- _, err := f.db.ExecContext(ctx, q, me.String(), them.String())
- return err
- }
- func (f SQLiteUserStore) PermitBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
- q := `
- INSERT INTO clientSideBuddyList (me, them, isPermit)
- VALUES (?, ?, 1)
- ON CONFLICT (me, them) DO UPDATE SET isPermit = 1
- `
- _, err := f.db.ExecContext(ctx, q, me.String(), them.String())
- return err
- }
- func (f SQLiteUserStore) RemovePermitBuddy(ctx context.Context, me IdentScreenName, them IdentScreenName) error {
- q := `
- UPDATE clientSideBuddyList
- SET isPermit = false
- WHERE me = ?
- AND them = ?
- `
- _, err := f.db.ExecContext(ctx, q, me.String(), them.String())
- return err
- }
- func (f SQLiteUserStore) Profile(ctx context.Context, screenName IdentScreenName) (UserProfile, error) {
- q := `
- SELECT IFNULL(body, ''), IFNULL(mimeType, ''), IFNULL(updateTime, 0)
- FROM profile
- WHERE screenName = ?
- `
- var profile UserProfile
- var updateTimeUnix int64
- err := f.db.QueryRowContext(ctx, q, screenName.String()).Scan(&profile.ProfileText, &profile.MIMEType, &updateTimeUnix)
- if errors.Is(err, sql.ErrNoRows) {
- return UserProfile{}, nil
- }
- if err != nil {
- return UserProfile{}, err
- }
- if updateTimeUnix > 0 {
- profile.UpdateTime = time.Unix(updateTimeUnix, 0).UTC()
- }
- return profile, nil
- }
- func (f SQLiteUserStore) SetProfile(ctx context.Context, screenName IdentScreenName, profile UserProfile) error {
- var updateTimeUnix int64
- if !profile.UpdateTime.IsZero() {
- updateTimeUnix = profile.UpdateTime.Unix()
- }
- q := `
- INSERT INTO profile (screenName, body, mimeType, updateTime)
- VALUES (?, ?, ?, ?)
- ON CONFLICT (screenName)
- DO UPDATE SET body = excluded.body,
- mimeType = excluded.mimeType,
- updateTime = excluded.updateTime
- `
- _, err := f.db.ExecContext(ctx, q, screenName.String(), profile.ProfileText, profile.MIMEType, updateTimeUnix)
- return err
- }
- func (f SQLiteUserStore) SetDirectoryInfo(ctx context.Context, screenName IdentScreenName, info AIMNameAndAddr) error {
- q := `
- UPDATE users SET
- aim_firstName = ?,
- aim_lastName = ?,
- aim_middleName = ?,
- aim_maidenName = ?,
- aim_country = ?,
- aim_state = ?,
- aim_city = ?,
- aim_nickName = ?,
- aim_zipCode = ?,
- aim_address = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- info.FirstName,
- info.LastName,
- info.MiddleName,
- info.MaidenName,
- info.Country,
- info.State,
- info.City,
- info.NickName,
- info.ZIPCode,
- info.Address,
- screenName.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) InsertBARTItem(ctx context.Context, hash []byte, blob []byte, itemType uint16) error {
- q := `
- INSERT INTO bartItem (hash, body, type)
- VALUES (?, ?, ?)
- `
- _, err := f.db.ExecContext(ctx, q, hash, blob, itemType)
- if err != nil {
- if liteErr, ok := err.(*sqlite.Error); ok {
- code := liteErr.Code()
- if code == lib.SQLITE_CONSTRAINT_PRIMARYKEY {
- return ErrBARTItemExists
- }
- }
- return err
- }
- return nil
- }
- func (f SQLiteUserStore) BARTItem(ctx context.Context, hash []byte) ([]byte, error) {
- q := `
- SELECT body
- FROM bartItem
- WHERE hash = ?
- `
- var body []byte
- err := f.db.QueryRowContext(ctx, q, hash).Scan(&body)
- if errors.Is(err, sql.ErrNoRows) {
- err = nil
- }
- return body, err
- }
- // BARTItem represents a BART asset with its hash and type.
- type BARTItem struct {
- Hash string
- Type uint16
- }
- func (f SQLiteUserStore) ListBARTItems(ctx context.Context, itemType uint16) ([]BARTItem, error) {
- q := `
- SELECT hash, type
- FROM bartItem
- WHERE type = ?
- `
- rows, err := f.db.QueryContext(ctx, q, itemType)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var items []BARTItem
- for rows.Next() {
- var item BARTItem
- var hashBytes []byte
- err := rows.Scan(&hashBytes, &item.Type)
- if err != nil {
- return nil, err
- }
- item.Hash = hex.EncodeToString(hashBytes)
- items = append(items, item)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return items, nil
- }
- func (f SQLiteUserStore) DeleteBARTItem(ctx context.Context, hash []byte) error {
- q := `
- DELETE FROM bartItem
- WHERE hash = ?
- `
- result, err := f.db.ExecContext(ctx, q, hash)
- if err != nil {
- return err
- }
- rowsAffected, err := result.RowsAffected()
- if err != nil {
- return err
- }
- if rowsAffected == 0 {
- return ErrBARTItemNotFound
- }
- return nil
- }
- func (f SQLiteUserStore) ChatRoomByCookie(ctx context.Context, chatCookie string) (ChatRoom, error) {
- chatRoom := ChatRoom{}
- q := `
- SELECT exchange, name, created, creator
- FROM chatRoom
- WHERE lower(cookie) = lower(?)
- `
- var creator string
- err := f.db.QueryRowContext(ctx, q, chatCookie).Scan(
- &chatRoom.exchange,
- &chatRoom.name,
- &chatRoom.createTime,
- &creator,
- )
- if errors.Is(err, sql.ErrNoRows) {
- err = fmt.Errorf("%w: %s", ErrChatRoomNotFound, chatCookie)
- }
- chatRoom.creator = NewIdentScreenName(creator)
- return chatRoom, err
- }
- func (f SQLiteUserStore) ChatRoomByName(ctx context.Context, exchange uint16, name string) (ChatRoom, error) {
- chatRoom := ChatRoom{
- exchange: exchange,
- }
- q := `
- SELECT name, created, creator
- FROM chatRoom
- WHERE exchange = ? AND lower(name) = lower(?)
- `
- var creator string
- err := f.db.QueryRowContext(ctx, q, exchange, name).Scan(
- &chatRoom.name,
- &chatRoom.createTime,
- &creator,
- )
- if errors.Is(err, sql.ErrNoRows) {
- err = ErrChatRoomNotFound
- }
- chatRoom.creator = NewIdentScreenName(creator)
- return chatRoom, err
- }
- func (f SQLiteUserStore) CreateChatRoom(ctx context.Context, chatRoom *ChatRoom) error {
- chatRoom.createTime = time.Now().UTC()
- q := `
- INSERT INTO chatRoom (cookie, exchange, name, created, creator)
- VALUES (?, ?, ?, ?, ?)
- `
- _, err := f.db.ExecContext(ctx,
- q,
- chatRoom.Cookie(),
- chatRoom.Exchange(),
- chatRoom.Name(),
- chatRoom.createTime,
- chatRoom.Creator().String(),
- )
- if err != nil {
- if strings.Contains(err.Error(), "constraint failed") {
- err = ErrDupChatRoom
- }
- err = fmt.Errorf("CreateChatRoom: %w", err)
- }
- return err
- }
- func (f SQLiteUserStore) AllChatRooms(ctx context.Context, exchange uint16) ([]ChatRoom, error) {
- q := `
- SELECT created, creator, name
- FROM chatRoom
- WHERE exchange = ?
- ORDER BY created ASC
- `
- rows, err := f.db.QueryContext(ctx, 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.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
- }
- func (f SQLiteUserStore) DeleteChatRooms(ctx context.Context, exchange uint16, names []string) error {
- if len(names) == 0 {
- return nil
- }
- // Build the query with placeholders for each name
- placeholders := make([]string, len(names))
- args := make([]interface{}, 0, len(names)+1)
- args = append(args, exchange)
- for i, name := range names {
- placeholders[i] = "?"
- args = append(args, name)
- }
- q := fmt.Sprintf(`
- DELETE FROM chatRoom
- WHERE exchange = ? AND name IN (%s)
- `, strings.Join(placeholders, ","))
- _, err := f.db.ExecContext(ctx, q, args...)
- if err != nil {
- return fmt.Errorf("DeleteChatRooms: %w", err)
- }
- return nil
- }
- func (f SQLiteUserStore) UpdateDisplayScreenName(ctx context.Context, displayScreenName DisplayScreenName) error {
- q := `
- UPDATE users
- SET displayScreenName = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.ExecContext(ctx, q, displayScreenName.String(), displayScreenName.IdentScreenName().String())
- return err
- }
- func (f SQLiteUserStore) UpdateEmailAddress(ctx context.Context, screenName IdentScreenName, emailAddress *mail.Address) error {
- q := `
- UPDATE users
- SET emailAddress = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.ExecContext(ctx, q, emailAddress.Address, screenName.String())
- return err
- }
- func (f SQLiteUserStore) EmailAddress(ctx context.Context, screenName IdentScreenName) (*mail.Address, error) {
- q := `
- SELECT emailAddress
- FROM users
- WHERE identScreenName = ?
- `
- var emailAddress string
- err := f.db.QueryRowContext(ctx, 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
- }
- func (f SQLiteUserStore) UpdateRegStatus(ctx context.Context, screenName IdentScreenName, regStatus uint16) error {
- q := `
- UPDATE users
- SET regStatus = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.ExecContext(ctx, q, regStatus, screenName.String())
- return err
- }
- func (f SQLiteUserStore) RegStatus(ctx context.Context, screenName IdentScreenName) (uint16, error) {
- q := `
- SELECT regStatus
- FROM users
- WHERE identScreenName = ?
- `
- var regStatus uint16
- err := f.db.QueryRowContext(ctx, 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
- }
- func (f SQLiteUserStore) UpdateConfirmStatus(ctx context.Context, screenName IdentScreenName, confirmStatus bool) error {
- q := `
- UPDATE users
- SET confirmStatus = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.ExecContext(ctx, q, confirmStatus, screenName.String())
- return err
- }
- func (f SQLiteUserStore) ConfirmStatus(ctx context.Context, screenName IdentScreenName) (bool, error) {
- q := `
- SELECT confirmStatus
- FROM users
- WHERE identScreenName = ?
- `
- var confirmStatus bool
- err := f.db.QueryRowContext(ctx, 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
- }
- func (f SQLiteUserStore) UpdateSuspendedStatus(ctx context.Context, suspendedStatus uint16, screenName IdentScreenName) error {
- q := `
- UPDATE users
- SET suspendedStatus = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.ExecContext(ctx, q, suspendedStatus, screenName.String())
- return err
- }
- func (f SQLiteUserStore) SetBotStatus(ctx context.Context, isBot bool, screenName IdentScreenName) error {
- q := `
- UPDATE users
- SET isBot = ?
- WHERE identScreenName = ?
- `
- _, err := f.db.ExecContext(ctx, q, isBot, screenName.String())
- return err
- }
- func (f SQLiteUserStore) SetWorkInfo(ctx context.Context, name IdentScreenName, data ICQWorkInfo) error {
- q := `
- UPDATE users SET
- icq_workInfo_company = ?,
- icq_workInfo_department = ?,
- icq_workInfo_occupationCode = ?,
- icq_workInfo_position = ?,
- icq_workInfo_address = ?,
- icq_workInfo_city = ?,
- icq_workInfo_countryCode = ?,
- icq_workInfo_fax = ?,
- icq_workInfo_phone = ?,
- icq_workInfo_state = ?,
- icq_workInfo_webPage = ?,
- icq_workInfo_zipCode = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- data.Company,
- data.Department,
- data.OccupationCode,
- data.Position,
- data.Address,
- data.City,
- data.CountryCode,
- data.Fax,
- data.Phone,
- data.State,
- data.WebPage,
- data.ZIPCode,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) SetPermissions(ctx context.Context, name IdentScreenName, data ICQPermissions) error {
- q := `
- UPDATE users SET
- icq_permissions_authRequired = ?,
- icq_permissions_webAware = ?,
- icq_permissions_allowSpam = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- data.AuthRequired,
- data.WebAware,
- data.AllowSpam,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- // SetHomepageCategory updates the user's homepage category information.
- // This is used by the V5 META_SET_HPCAT (0x0442) command.
- // From iserverd db_users_sethpagecat_info() - updates user's homepage category.
- func (f SQLiteUserStore) SetHomepageCategory(ctx context.Context, name IdentScreenName, data ICQHomepageCategory) error {
- q := `
- UPDATE users SET
- icq_homepageCategory_enabled = ?,
- icq_homepageCategory_index = ?,
- icq_homepageCategory_description = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- data.Enabled,
- data.Index,
- data.Description,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) SetMoreInfo(ctx context.Context, name IdentScreenName, data ICQMoreInfo) error {
- q := `
- UPDATE users SET
- icq_moreInfo_birthDay = ?,
- icq_moreInfo_birthMonth = ?,
- icq_moreInfo_birthYear = ?,
- icq_moreInfo_gender = ?,
- icq_moreInfo_homePageAddr = ?,
- icq_moreInfo_lang1 = ?,
- icq_moreInfo_lang2 = ?,
- icq_moreInfo_lang3 = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- data.BirthDay,
- data.BirthMonth,
- data.BirthYear,
- data.Gender,
- data.HomePageAddr,
- data.Lang1,
- data.Lang2,
- data.Lang3,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) SetUserNotes(ctx context.Context, name IdentScreenName, data ICQUserNotes) error {
- q := `
- UPDATE users
- SET icq_notes = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- data.Notes,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) SetInterests(ctx context.Context, name IdentScreenName, data ICQInterests) error {
- q := `
- UPDATE users SET
- icq_interests_code1 = ?,
- icq_interests_keyword1 = ?,
- icq_interests_code2 = ?,
- icq_interests_keyword2 = ?,
- icq_interests_code3 = ?,
- icq_interests_keyword3 = ?,
- icq_interests_code4 = ?,
- icq_interests_keyword4 = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- data.Code1,
- data.Keyword1,
- data.Code2,
- data.Keyword2,
- data.Code3,
- data.Keyword3,
- data.Code4,
- data.Keyword4,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) SetAffiliations(ctx context.Context, name IdentScreenName, data ICQAffiliations) error {
- q := `
- UPDATE users SET
- icq_affiliations_currentCode1 = ?,
- icq_affiliations_currentKeyword1 = ?,
- icq_affiliations_currentCode2 = ?,
- icq_affiliations_currentKeyword2 = ?,
- icq_affiliations_currentCode3 = ?,
- icq_affiliations_currentKeyword3 = ?,
- icq_affiliations_pastCode1 = ?,
- icq_affiliations_pastKeyword1 = ?,
- icq_affiliations_pastCode2 = ?,
- icq_affiliations_pastKeyword2 = ?,
- icq_affiliations_pastCode3 = ?,
- icq_affiliations_pastKeyword3 = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- data.CurrentCode1,
- data.CurrentKeyword1,
- data.CurrentCode2,
- data.CurrentKeyword2,
- data.CurrentCode3,
- data.CurrentKeyword3,
- data.PastCode1,
- data.PastKeyword1,
- data.PastCode2,
- data.PastKeyword2,
- data.PastCode3,
- data.PastKeyword3,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) SetBasicInfo(ctx context.Context, name IdentScreenName, data ICQBasicInfo) error {
- q := `
- UPDATE users SET
- icq_basicInfo_cellPhone = ?,
- icq_basicInfo_countryCode = ?,
- icq_basicInfo_emailAddress = ?,
- icq_basicInfo_firstName = ?,
- icq_basicInfo_gmtOffset = ?,
- icq_basicInfo_address = ?,
- icq_basicInfo_city = ?,
- icq_basicInfo_fax = ?,
- icq_basicInfo_phone = ?,
- icq_basicInfo_state = ?,
- icq_basicInfo_lastName = ?,
- icq_basicInfo_nickName = ?,
- icq_basicInfo_publishEmail = ?,
- icq_basicInfo_zipCode = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- data.CellPhone,
- data.CountryCode,
- data.EmailAddress,
- data.FirstName,
- data.GMTOffset,
- data.Address,
- data.City,
- data.Fax,
- data.Phone,
- data.State,
- data.LastName,
- data.Nickname,
- data.PublishEmail,
- data.ZIPCode,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- // SetICQInfo updates all ICQ profile columns on the user row in one UPDATE.
- // Keep this column list aligned with SetMoreInfo, SetWorkInfo, SetPermissions,
- // SetUserNotes, SetInterests, SetAffiliations, and SetHomepageCategory. Basic-info
- // columns that are not on SetBasicInfo (e.g. originally-from) live only here.
- func (f SQLiteUserStore) SetICQInfo(ctx context.Context, name IdentScreenName, info ICQInfo) error {
- q := `
- UPDATE users SET
- icq_basicInfo_cellPhone = ?,
- icq_basicInfo_countryCode = ?,
- icq_basicInfo_emailAddress = ?,
- icq_basicInfo_firstName = ?,
- icq_basicInfo_gmtOffset = ?,
- icq_basicInfo_address = ?,
- icq_basicInfo_city = ?,
- icq_basicInfo_fax = ?,
- icq_basicInfo_phone = ?,
- icq_basicInfo_state = ?,
- icq_basicInfo_lastName = ?,
- icq_basicInfo_nickName = ?,
- icq_basicInfo_publishEmail = ?,
- icq_basicInfo_zipCode = ?,
- icq_basicInfo_originCity = ?,
- icq_basicInfo_originState = ?,
- icq_basicInfo_originCountryCode = ?,
- icq_moreInfo_birthDay = ?,
- icq_moreInfo_birthMonth = ?,
- icq_moreInfo_birthYear = ?,
- icq_moreInfo_gender = ?,
- icq_moreInfo_homePageAddr = ?,
- icq_moreInfo_lang1 = ?,
- icq_moreInfo_lang2 = ?,
- icq_moreInfo_lang3 = ?,
- icq_workInfo_company = ?,
- icq_workInfo_department = ?,
- icq_workInfo_occupationCode = ?,
- icq_workInfo_position = ?,
- icq_workInfo_address = ?,
- icq_workInfo_city = ?,
- icq_workInfo_countryCode = ?,
- icq_workInfo_fax = ?,
- icq_workInfo_phone = ?,
- icq_workInfo_state = ?,
- icq_workInfo_webPage = ?,
- icq_workInfo_zipCode = ?,
- icq_permissions_authRequired = ?,
- icq_permissions_webAware = ?,
- icq_permissions_allowSpam = ?,
- icq_notes = ?,
- icq_interests_code1 = ?,
- icq_interests_keyword1 = ?,
- icq_interests_code2 = ?,
- icq_interests_keyword2 = ?,
- icq_interests_code3 = ?,
- icq_interests_keyword3 = ?,
- icq_interests_code4 = ?,
- icq_interests_keyword4 = ?,
- icq_affiliations_currentCode1 = ?,
- icq_affiliations_currentKeyword1 = ?,
- icq_affiliations_currentCode2 = ?,
- icq_affiliations_currentKeyword2 = ?,
- icq_affiliations_currentCode3 = ?,
- icq_affiliations_currentKeyword3 = ?,
- icq_affiliations_pastCode1 = ?,
- icq_affiliations_pastKeyword1 = ?,
- icq_affiliations_pastCode2 = ?,
- icq_affiliations_pastKeyword2 = ?,
- icq_affiliations_pastCode3 = ?,
- icq_affiliations_pastKeyword3 = ?,
- icq_homepageCategory_enabled = ?,
- icq_homepageCategory_index = ?,
- icq_homepageCategory_description = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx, q,
- info.Basic.CellPhone,
- info.Basic.CountryCode,
- info.Basic.EmailAddress,
- info.Basic.FirstName,
- info.Basic.GMTOffset,
- info.Basic.Address,
- info.Basic.City,
- info.Basic.Fax,
- info.Basic.Phone,
- info.Basic.State,
- info.Basic.LastName,
- info.Basic.Nickname,
- info.Basic.PublishEmail,
- info.Basic.ZIPCode,
- info.Basic.OriginallyFromCity,
- info.Basic.OriginallyFromState,
- info.Basic.OriginallyFromCountryCode,
- info.More.BirthDay,
- info.More.BirthMonth,
- info.More.BirthYear,
- info.More.Gender,
- info.More.HomePageAddr,
- info.More.Lang1,
- info.More.Lang2,
- info.More.Lang3,
- info.Work.Company,
- info.Work.Department,
- info.Work.OccupationCode,
- info.Work.Position,
- info.Work.Address,
- info.Work.City,
- info.Work.CountryCode,
- info.Work.Fax,
- info.Work.Phone,
- info.Work.State,
- info.Work.WebPage,
- info.Work.ZIPCode,
- info.Permissions.AuthRequired,
- info.Permissions.WebAware,
- info.Permissions.AllowSpam,
- info.Notes.Notes,
- info.Interests.Code1,
- info.Interests.Keyword1,
- info.Interests.Code2,
- info.Interests.Keyword2,
- info.Interests.Code3,
- info.Interests.Keyword3,
- info.Interests.Code4,
- info.Interests.Keyword4,
- info.Affiliations.CurrentCode1,
- info.Affiliations.CurrentKeyword1,
- info.Affiliations.CurrentCode2,
- info.Affiliations.CurrentKeyword2,
- info.Affiliations.CurrentCode3,
- info.Affiliations.CurrentKeyword3,
- info.Affiliations.PastCode1,
- info.Affiliations.PastKeyword1,
- info.Affiliations.PastCode2,
- info.Affiliations.PastKeyword2,
- info.Affiliations.PastCode3,
- info.Affiliations.PastKeyword3,
- info.HomepageCategory.Enabled,
- info.HomepageCategory.Index,
- info.HomepageCategory.Description,
- name.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- func (f SQLiteUserStore) SaveMessage(ctx context.Context, offlineMessage OfflineMessage) (newCount int, err error) {
- buf := &bytes.Buffer{}
- if err := wire.MarshalBE(offlineMessage.Message, buf); err != nil {
- return 0, fmt.Errorf("marshal: %w", err)
- }
- var tx *sql.Tx
- tx, err = f.db.BeginTx(ctx, nil)
- if err != nil {
- return 0, fmt.Errorf("begin tx: %w", err)
- }
- defer func() {
- if err != nil {
- _ = tx.Rollback()
- }
- }()
- const countQuery = `
- SELECT COUNT(1)
- FROM offlineMessage
- WHERE sender = ? AND recipient = ?
- `
- var currentCount int
- if err = tx.QueryRowContext(
- ctx,
- countQuery,
- offlineMessage.Sender.String(),
- offlineMessage.Recipient.String(),
- ).Scan(¤tCount); err != nil {
- return 0, fmt.Errorf("count: %w", err)
- }
- if currentCount >= offlineInboxLimit {
- err = ErrOfflineInboxFull
- return 0, err
- }
- q := `
- INSERT INTO offlineMessage (sender, recipient, message, sent)
- VALUES (?, ?, ?, ?)
- `
- if _, err = tx.ExecContext(ctx,
- q,
- offlineMessage.Sender.String(),
- offlineMessage.Recipient.String(),
- buf.Bytes(),
- offlineMessage.Sent,
- ); err != nil {
- if sqliteErr, ok := err.(*sqlite.Error); ok && sqliteErr.Code() == lib.SQLITE_CONSTRAINT_FOREIGNKEY {
- err = ErrNoUser
- } else {
- err = fmt.Errorf("insert: %w", err)
- }
- return 0, err
- }
- newCount = currentCount + 1
- updateQuery := `
- UPDATE users
- SET offlineMsgCount = ?
- WHERE identScreenName = ?
- `
- _, err = tx.ExecContext(ctx,
- updateQuery,
- newCount,
- offlineMessage.Recipient.String(),
- )
- if err != nil {
- return 0, fmt.Errorf("update offlineMsgCount: %w", err)
- }
- if err = tx.Commit(); err != nil {
- return 0, fmt.Errorf("commit: %w", err)
- }
- return newCount, nil
- }
- func (f SQLiteUserStore) RetrieveMessages(ctx context.Context, recip IdentScreenName) ([]OfflineMessage, error) {
- q := `
- SELECT
- sender,
- message,
- sent
- FROM offlineMessage
- WHERE recipient = ?
- `
- rows, err := f.db.QueryContext(ctx, q, recip.String())
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var messages []OfflineMessage
- for rows.Next() {
- var sender string
- var buf []byte
- var sent time.Time
- if err := rows.Scan(&sender, &buf, &sent); err != nil {
- return nil, err
- }
- var msg wire.SNAC_0x04_0x06_ICBMChannelMsgToHost
- if err := wire.UnmarshalBE(&msg, bytes.NewBuffer(buf)); err != nil {
- return nil, fmt.Errorf("unmarshal: %w", err)
- }
- messages = append(messages, OfflineMessage{
- Sender: NewIdentScreenName(sender),
- Recipient: recip,
- Message: msg,
- Sent: sent,
- })
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return messages, nil
- }
- func (f SQLiteUserStore) DeleteMessages(ctx context.Context, recip IdentScreenName) error {
- q := `
- DELETE FROM offlineMessage WHERE recipient = ?
- `
- _, err := f.db.ExecContext(ctx, q, recip.String())
- return err
- }
- func (f SQLiteUserStore) BuddyIconMetadata(ctx context.Context, screenName IdentScreenName) (*wire.BARTID, error) {
- q := `
- SELECT
- groupID,
- itemID,
- classID,
- name,
- attributes
- FROM feedBag
- WHERE screenname = ? AND name = ? AND classID = ?
- `
- var item wire.FeedbagItem
- var attrs []byte
- err := f.db.QueryRowContext(ctx, q, screenName.String(), wire.BARTTypesBuddyIcon, wire.FeedbagClassIdBart).Scan(&item.GroupID, &item.ItemID, &item.ClassID, &item.Name, &attrs)
- if errors.Is(err, sql.ErrNoRows) {
- return nil, nil
- }
- if err != nil {
- return nil, err
- }
- if err := wire.UnmarshalBE(&item.TLVLBlock, bytes.NewBuffer(attrs)); err != nil {
- return nil, err
- }
- b, hasBuf := item.Bytes(wire.FeedbagAttributesBartInfo)
- if !hasBuf {
- return nil, errors.New("unable to extract icon payload")
- }
- bartInfo := wire.BARTInfo{}
- if err := wire.UnmarshalBE(&bartInfo, bytes.NewBuffer(b)); err != nil {
- return nil, err
- }
- return &wire.BARTID{
- Type: wire.BARTTypesBuddyIcon,
- BARTInfo: wire.BARTInfo{
- Flags: bartInfo.Flags,
- Hash: bartInfo.Hash,
- },
- }, nil
- }
- func (f SQLiteUserStore) SetKeywords(ctx context.Context, screenName IdentScreenName, keywords [5]string) error {
- q := `
- WITH interests AS (SELECT CASE WHEN name = ? THEN id ELSE NULL END AS aim_keyword1,
- CASE WHEN name = ? THEN id ELSE NULL END AS aim_keyword2,
- CASE WHEN name = ? THEN id ELSE NULL END AS aim_keyword3,
- CASE WHEN name = ? THEN id ELSE NULL END AS aim_keyword4,
- CASE WHEN name = ? THEN id ELSE NULL END AS aim_keyword5
- FROM aimKeyword
- WHERE name IN (?, ?, ?, ?, ?))
- UPDATE users
- SET aim_keyword1 = (SELECT aim_keyword1 FROM interests WHERE aim_keyword1 IS NOT NULL),
- aim_keyword2 = (SELECT aim_keyword2 FROM interests WHERE aim_keyword2 IS NOT NULL),
- aim_keyword3 = (SELECT aim_keyword3 FROM interests WHERE aim_keyword3 IS NOT NULL),
- aim_keyword4 = (SELECT aim_keyword4 FROM interests WHERE aim_keyword4 IS NOT NULL),
- aim_keyword5 = (SELECT aim_keyword5 FROM interests WHERE aim_keyword5 IS NOT NULL)
- WHERE identScreenName = ?
- `
- _, err := f.db.ExecContext(ctx, q,
- keywords[0], keywords[1], keywords[2], keywords[3], keywords[4],
- keywords[0], keywords[1], keywords[2], keywords[3], keywords[4],
- screenName.String())
- return err
- }
- func (f SQLiteUserStore) Categories(ctx context.Context) ([]Category, error) {
- q := `SELECT id, name FROM aimKeywordCategory ORDER BY name`
- rows, err := f.db.QueryContext(ctx, q)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var categories []Category
- for rows.Next() {
- category := Category{}
- if err := rows.Scan(&category.ID, &category.Name); err != nil {
- return nil, err
- }
- categories = append(categories, category)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return categories, nil
- }
- func (f SQLiteUserStore) CreateCategory(ctx context.Context, name string) (Category, error) {
- tx, err := f.db.Begin()
- if err != nil {
- return Category{}, err
- }
- defer tx.Rollback()
- q := `INSERT INTO aimKeywordCategory (name) VALUES (?)`
- res, err := tx.ExecContext(ctx, q, name)
- if err != nil {
- if sqliteErr, ok := err.(*sqlite.Error); ok && sqliteErr.Code() == lib.SQLITE_CONSTRAINT_UNIQUE {
- err = ErrKeywordCategoryExists
- }
- return Category{}, err
- }
- id, err := res.LastInsertId()
- if err != nil {
- return Category{}, err
- }
- if id > math.MaxUint8 {
- return Category{}, errTooManyCategories
- }
- if err := tx.Commit(); err != nil {
- return Category{}, err
- }
- return Category{
- ID: uint8(id),
- Name: name,
- }, nil
- }
- func (f SQLiteUserStore) DeleteCategory(ctx context.Context, categoryID uint8) error {
- q := `DELETE FROM aimKeywordCategory WHERE id = ?`
- res, err := f.db.ExecContext(ctx, q, categoryID)
- if err != nil {
- // Check if the error is a foreign key constraint violation
- if sqliteErr, ok := err.(*sqlite.Error); ok && sqliteErr.Code() == lib.SQLITE_CONSTRAINT_FOREIGNKEY {
- return ErrKeywordInUse
- }
- }
- c, err := res.RowsAffected()
- if err != nil {
- return err
- }
- if c == 0 {
- return ErrKeywordCategoryNotFound
- }
- return nil
- }
- func (f SQLiteUserStore) KeywordsByCategory(ctx context.Context, categoryID uint8) ([]Keyword, error) {
- q := `SELECT id, name FROM aimKeyword WHERE parent = ? ORDER BY name`
- if categoryID == 0 {
- q = `SELECT id, name FROM aimKeyword WHERE parent IS NULL ORDER BY name`
- }
- rows, err := f.db.QueryContext(ctx, q, categoryID)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var keywords []Keyword
- for rows.Next() {
- keyword := Keyword{}
- if err := rows.Scan(&keyword.ID, &keyword.Name); err != nil {
- return nil, err
- }
- keywords = append(keywords, keyword)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- if len(keywords) == 0 {
- var exists int
- err = f.db.QueryRow("SELECT COUNT(*) FROM aimKeywordCategory WHERE id = ?", categoryID).Scan(&exists)
- if err != nil {
- return nil, err
- }
- if exists == 0 {
- return nil, ErrKeywordCategoryNotFound
- }
- }
- return keywords, nil
- }
- func (f SQLiteUserStore) CreateKeyword(ctx context.Context, name string, categoryID uint8) (Keyword, error) {
- tx, err := f.db.Begin()
- if err != nil {
- return Keyword{}, err
- }
- defer tx.Rollback()
- q := `INSERT INTO aimKeyword (name, parent) VALUES (?, ?)`
- var parent interface{} = nil
- if categoryID != 0 {
- parent = categoryID
- }
- res, err := tx.ExecContext(ctx, q, name, parent)
- if err != nil {
- if sqliteErr, ok := err.(*sqlite.Error); ok && sqliteErr.Code() == lib.SQLITE_CONSTRAINT_UNIQUE {
- err = ErrKeywordExists
- } else if sqliteErr, ok := err.(*sqlite.Error); ok && sqliteErr.Code() == lib.SQLITE_CONSTRAINT_FOREIGNKEY {
- err = ErrKeywordCategoryNotFound
- }
- return Keyword{}, err
- }
- id, err := res.LastInsertId()
- if err != nil {
- return Keyword{}, err
- }
- if id > math.MaxUint8 {
- return Keyword{}, errTooManyKeywords
- }
- if err := tx.Commit(); err != nil {
- return Keyword{}, err
- }
- return Keyword{
- ID: uint8(id),
- Name: name,
- }, nil
- }
- func (f SQLiteUserStore) DeleteKeyword(ctx context.Context, id uint8) error {
- q := `DELETE FROM aimKeyword WHERE id = ?`
- res, err := f.db.ExecContext(ctx, q, id)
- if err != nil {
- // Check if the error is a foreign key constraint violation
- if sqliteErr, ok := err.(*sqlite.Error); ok && sqliteErr.Code() == lib.SQLITE_CONSTRAINT_FOREIGNKEY {
- return ErrKeywordInUse
- }
- }
- c, err := res.RowsAffected()
- if err != nil {
- return err
- }
- if c == 0 {
- return ErrKeywordNotFound
- }
- return nil
- }
- // InterestList returns a list of keywords grouped by category used to render
- // the AIM directory interests list. The list is made up of 3 types of elements:
- //
- // Categories
- //
- // ID: The category ID
- // Cookie: The category name
- // Type: [wire.ODirKeywordCategory]
- //
- // Keywords
- //
- // ID: The parent category ID
- // Cookie: The keyword name
- // Type: [wire.ODirKeyword]
- //
- // Top-level Keywords
- //
- // ID: 0 (does not have a parent category)
- // Cookie: The keyword name
- // Type: [wire.ODirKeyword]
- //
- // Keywords are grouped contiguously by category and preceded by the category
- // name. Top-level keywords appear by themselves. Categories and top-level
- // keywords are sorted alphabetically. Keyword groups are sorted alphabetically.
- //
- // Conceptually, the list looks like this:
- //
- // > Animals (top-level keyword, id=0)
- // > Artificial Intelligence (keyword, id=3)
- // > Cybersecurity (keyword, id=3)
- // > Music (category, id=1)
- // > Jazz (keyword, id=1)
- // > Rock (keyword, id=1)
- // > Sports (category, id=2)
- // > Basketball (keyword, id=2)
- // > Soccer (keyword, id=2)
- // > Tennis (keyword, id=2)
- // > Technology (category, id=3)
- // > Zoology (top-level keyword, id=0)
- func (f SQLiteUserStore) InterestList(ctx context.Context) ([]wire.ODirKeywordListItem, error) {
- q := `
- WITH categories AS (
- SELECT
- name AS grouping,
- id,
- 0 AS sortPrio,
- name
- FROM aimKeywordCategory
- UNION
- SELECT
- IFNULL(akc.name, ak.name) AS grouping,
- IFNULL(ak.parent, 0) AS id,
- CASE WHEN ak.parent IS NULL THEN 1 ELSE 2 END AS sortPrio,
- ak.name
- FROM aimKeyword ak
- LEFT JOIN aimKeywordCategory akc ON akc.id = ak.parent
- ORDER BY 1, 3, 4
- )
- SELECT
- id,
- sortPrio,
- name
- FROM categories
- `
- rows, err := f.db.QueryContext(ctx, q)
- if err != nil {
- return nil, err
- }
- defer rows.Close()
- var list []wire.ODirKeywordListItem
- for rows.Next() {
- msg := wire.ODirKeywordListItem{}
- var sortPrio int
- if err := rows.Scan(&msg.ID, &sortPrio, &msg.Name); err != nil {
- return nil, err
- }
- switch sortPrio {
- case 0:
- msg.Type = wire.ODirKeywordCategory
- case 1, 2:
- msg.Type = wire.ODirKeyword
- }
- list = append(list, msg)
- }
- if err := rows.Err(); err != nil {
- return nil, err
- }
- return list, nil
- }
- func (f SQLiteUserStore) SetTOCConfig(ctx context.Context, user IdentScreenName, config string) error {
- q := `
- UPDATE users
- SET tocConfig = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- config,
- user.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- // SetWarnLevel updates the last warn update time and warning level for a user.
- func (f SQLiteUserStore) SetWarnLevel(ctx context.Context, user IdentScreenName, lastWarnUpdate time.Time, lastWarnLevel uint16) error {
- q := `
- UPDATE users
- SET lastWarnUpdate = ?, lastWarnLevel = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- lastWarnUpdate.Unix(),
- lastWarnLevel,
- user.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
- // SetOfflineMsgCount updates the offline message count for a user.
- func (f SQLiteUserStore) SetOfflineMsgCount(ctx context.Context, screenName IdentScreenName, count int) error {
- q := `
- UPDATE users
- SET offlineMsgCount = ?
- WHERE identScreenName = ?
- `
- res, err := f.db.ExecContext(ctx,
- q,
- count,
- screenName.String(),
- )
- if err != nil {
- return fmt.Errorf("exec: %w", err)
- }
- c, err := res.RowsAffected()
- if err != nil {
- return fmt.Errorf("rows affected: %w", err)
- }
- if c == 0 {
- return ErrNoUser
- }
- return nil
- }
|