service.go 64 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848
  1. package icq_legacy
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/google/uuid"
  11. "github.com/mk6i/open-oscar-server/state"
  12. "github.com/mk6i/open-oscar-server/wire"
  13. )
  14. // ICQLegacyService provides the service layer for legacy ICQ protocols (v2-v5).
  15. // It bridges legacy protocol handlers with the existing infrastructure, implementing
  16. // the LegacyService interface defined in server/icq_legacy/handler.go.
  17. //
  18. // Following the OSCAR foodgroup architecture pattern, this service contains all
  19. // business logic (authentication, messaging, contact list processing, status changes,
  20. // user info retrieval) while protocol handlers remain thin routing layers.
  21. //
  22. // Service methods accept typed request structs and return typed response structs,
  23. // keeping the service layer protocol-independent. Handlers are responsible for
  24. // parsing protocol-specific packets into request structs and building
  25. // protocol-specific response packets from the returned result structs.
  26. type ICQLegacyService struct {
  27. authService AuthService
  28. userManager UserManager
  29. accountManager AccountManager
  30. sessionRetriever SessionRetriever
  31. messageRelayer MessageRelayer
  32. buddyBroadcaster BuddyBroadcaster
  33. offlineMessageManager OfflineMessageManager
  34. icbmService ICBMService
  35. userFinder ICQUserFinder
  36. userUpdater ICQUserUpdater
  37. feedbagManager FeedbagManager
  38. relationshipFetcher RelationshipFetcher
  39. buddyListRegistry BuddyListRegistry
  40. buddyService BuddyService
  41. // legacySessionManager tracks legacy ICQ UDP sessions for presence, messaging,
  42. // and contact notifications (shared with protocol handlers).
  43. legacySessionManager *LegacySessionManager
  44. logger *slog.Logger
  45. timeNow func() time.Time
  46. }
  47. // NewICQLegacyService creates a new ICQLegacyService with the given dependencies.
  48. func NewICQLegacyService(
  49. authService AuthService,
  50. userManager UserManager,
  51. accountManager AccountManager,
  52. sessionRetriever SessionRetriever,
  53. messageRelayer MessageRelayer,
  54. buddyBroadcaster BuddyBroadcaster,
  55. offlineMessageManager OfflineMessageManager,
  56. userFinder ICQUserFinder,
  57. userUpdater ICQUserUpdater,
  58. feedbagManager FeedbagManager,
  59. relationshipFetcher RelationshipFetcher,
  60. buddyListRegistry BuddyListRegistry,
  61. buddyService BuddyService,
  62. icbmSvc ICBMService,
  63. legacySessionManager *LegacySessionManager,
  64. logger *slog.Logger,
  65. ) *ICQLegacyService {
  66. return &ICQLegacyService{
  67. authService: authService,
  68. userManager: userManager,
  69. accountManager: accountManager,
  70. sessionRetriever: sessionRetriever,
  71. messageRelayer: messageRelayer,
  72. buddyBroadcaster: buddyBroadcaster,
  73. offlineMessageManager: offlineMessageManager,
  74. userFinder: userFinder,
  75. userUpdater: userUpdater,
  76. feedbagManager: feedbagManager,
  77. relationshipFetcher: relationshipFetcher,
  78. buddyListRegistry: buddyListRegistry,
  79. buddyService: buddyService,
  80. icbmService: icbmSvc,
  81. legacySessionManager: legacySessionManager,
  82. logger: logger,
  83. timeNow: time.Now,
  84. }
  85. }
  86. // ValidateCredentials checks if the given UIN and password are valid.
  87. // Returns true if credentials are valid, false otherwise.
  88. // The password is validated by AuthService without registering a BOS session.
  89. func (s *ICQLegacyService) ValidateCredentials(ctx context.Context, uin uint32, password string) (bool, error) {
  90. if uin == 0 {
  91. return false, nil
  92. }
  93. block, err := s.legacyFLAPLogin(ctx, uin, password)
  94. if err != nil {
  95. if errors.Is(err, state.ErrNoUser) {
  96. return false, nil
  97. }
  98. return false, fmt.Errorf("AuthService.FLAPLogin: %w", err)
  99. }
  100. if block.HasTag(wire.LoginTLVTagsErrorSubcode) {
  101. s.logger.Debug("credentials validation failed", "uin", uin)
  102. return false, nil
  103. }
  104. s.logger.Debug("credentials validated successfully", "uin", uin)
  105. return true, nil
  106. }
  107. // AuthenticateUser validates user credentials and returns authentication result.
  108. // This is the service layer method for authentication that handlers call after
  109. // parsing login packets. It validates credentials and returns a typed result
  110. // struct containing success/failure and session data.
  111. //
  112. // The method does NOT contain any protocol-specific packet building logic.
  113. // Handlers are responsible for building protocol-specific responses based on
  114. // the returned AuthResult.
  115. func (s *ICQLegacyService) AuthenticateUser(ctx context.Context, req AuthRequest) (*AuthResult, error) {
  116. result := &AuthResult{
  117. Success: false,
  118. ErrorCode: 0x0001, // Default to bad password error
  119. }
  120. // Validate that UIN is provided
  121. if req.UIN == 0 {
  122. s.logger.Debug("authentication failed - no UIN provided")
  123. result.ErrorCode = 0x0002 // User not found
  124. return result, nil
  125. }
  126. block, err := s.legacyFLAPLogin(ctx, req.UIN, req.Password)
  127. if err != nil {
  128. if errors.Is(err, state.ErrNoUser) {
  129. s.logger.Debug("authentication failed - user not found", "uin", req.UIN)
  130. result.ErrorCode = 0x0002 // User not found
  131. return result, nil
  132. }
  133. return nil, fmt.Errorf("AuthService.FLAPLogin: %w", err)
  134. }
  135. if errCode, ok := block.Uint16BE(wire.LoginTLVTagsErrorSubcode); ok {
  136. s.logger.Debug("authentication failed",
  137. "uin", req.UIN,
  138. "error_code", fmt.Sprintf("0x%04X", errCode),
  139. )
  140. if errCode == wire.LoginErrICQUserErr {
  141. result.ErrorCode = 0x0002 // User not found
  142. }
  143. return result, nil
  144. }
  145. authCookie, ok := block.Bytes(wire.LoginTLVTagsAuthorizationCookie)
  146. if !ok {
  147. return nil, fmt.Errorf("authorization cookie missing from auth response")
  148. }
  149. serverCookie, err := s.authService.CrackCookie(authCookie)
  150. if err != nil {
  151. return nil, fmt.Errorf("AuthService.CrackCookie: %w", err)
  152. }
  153. instance, err := s.authService.RegisterBOSSession(ctx, serverCookie, func(sess *state.Session) {
  154. sess.OnSessionClose(func() {
  155. s.legacySessionManager.RemoveSession(req.UIN)
  156. })
  157. })
  158. if err != nil {
  159. return nil, fmt.Errorf("AuthService.RegisterBOSSession: %w", err)
  160. }
  161. if instance == nil {
  162. s.logger.Debug("authentication failed - missing OSCAR session", "uin", req.UIN)
  163. return result, nil
  164. }
  165. // Authentication successful
  166. result.Success = true
  167. result.ErrorCode = 0 // Success
  168. result.SessionID = uuid.New().String()
  169. result.oscarSession = instance
  170. s.logger.Info("user authenticated successfully",
  171. "uin", req.UIN,
  172. "version", req.Version,
  173. "status", fmt.Sprintf("0x%08X", req.Status),
  174. )
  175. return result, nil
  176. }
  177. func (s *ICQLegacyService) legacyFLAPLogin(ctx context.Context, uin uint32, password string) (wire.TLVRestBlock, error) {
  178. screenName := strconv.FormatUint(uint64(uin), 10)
  179. signonFrame := wire.FLAPSignonFrame{}
  180. signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsScreenName, screenName))
  181. signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsPlaintextPassword, password))
  182. return s.authService.FLAPLogin(ctx, signonFrame, "")
  183. }
  184. // ProcessContactList processes a contact list and returns online status for each contact.
  185. // This is the service layer method for contact list processing that handlers call after
  186. // parsing contact list packets. It checks the online status of each contact and returns
  187. // a ContactListResult containing the status of each contact.
  188. //
  189. // The method does NOT contain any protocol-specific packet building logic.
  190. // Handlers are responsible for:
  191. // - Parsing protocol-specific contact list packets into ContactListRequest
  192. // - Using the returned ContactListResult to send online/offline notifications
  193. // - Building protocol-specific response packets
  194. func (s *ICQLegacyService) ProcessContactList(ctx context.Context, instance *state.SessionInstance, req ContactListRequest) (*ContactListResult, error) {
  195. result := &ContactListResult{
  196. OnlineContacts: make([]ContactStatus, 0, len(req.Contacts)),
  197. }
  198. // Validate request
  199. if req.UIN == 0 {
  200. s.logger.Debug("ProcessContactList: invalid owner UIN")
  201. return result, nil
  202. }
  203. s.logger.Debug("ProcessContactList: processing contact list",
  204. "owner_uin", req.UIN,
  205. "contact_count", len(req.Contacts),
  206. )
  207. // Sync legacy contact list to clientSideBuddyList so the OSCAR
  208. // relationship query (AllRelationships) can discover this legacy user's
  209. // contacts. Legacy clients use client-side buddy lists, not feedbag.
  210. // Write both directions: forward (me has them) and reverse (them has me)
  211. // so that OSCAR clients using feedbag can also discover the legacy user
  212. // via the relationship query's clientSideBuddyList path.
  213. ownerScreenName := state.NewIdentScreenName(strconv.FormatUint(uint64(req.UIN), 10))
  214. for _, contactUIN := range req.Contacts {
  215. contactName := state.NewIdentScreenName(strconv.FormatUint(uint64(contactUIN), 10))
  216. // Forward: owner has contact on their list.
  217. snac := wire.SNAC_0x03_0x04_BuddyAddBuddies{}
  218. snac.Buddies = append(snac.Buddies, struct {
  219. ScreenName string `oscar:"len_prefix=uint8"`
  220. }{ScreenName: contactName.String()})
  221. if _, err := s.buddyService.AddBuddies(ctx, instance, wire.SNACFrame{}, snac); err != nil {
  222. s.logger.Error("ProcessContactList: failed to add buddy via BuddyService",
  223. "owner_uin", req.UIN,
  224. "contact_uin", contactUIN,
  225. "err", err,
  226. )
  227. }
  228. // Reverse: contact has owner on their list (so OSCAR clients see the legacy user).
  229. // Only add if the contact does NOT require authorization — otherwise the
  230. // legacy user must go through the auth request flow first.
  231. contactUser, userErr := s.userFinder.FindByUIN(ctx, contactUIN)
  232. if userErr == nil && contactUser.ICQInfo.Permissions.AuthRequired {
  233. continue
  234. }
  235. contactSession := state.NewSession()
  236. contactSession.SetUIN(contactUIN)
  237. contactSession.SetDisplayScreenName(state.DisplayScreenName(contactName.String()))
  238. contactSession.SetIdentScreenName(contactName)
  239. contactInstance := contactSession.AddInstance()
  240. snac = wire.SNAC_0x03_0x04_BuddyAddBuddies{}
  241. snac.Buddies = append(snac.Buddies, struct {
  242. ScreenName string `oscar:"len_prefix=uint8"`
  243. }{ScreenName: ownerScreenName.String()})
  244. if _, err := s.buddyService.AddBuddies(ctx, contactInstance, wire.SNACFrame{}, snac); err != nil {
  245. s.logger.Error("ProcessContactList: failed to add reverse buddy entry via BuddyService",
  246. "owner_uin", req.UIN,
  247. "contact_uin", contactUIN,
  248. "err", err,
  249. )
  250. }
  251. }
  252. // Check online status for each contact
  253. for _, contactUIN := range req.Contacts {
  254. status := ContactStatus{
  255. UIN: contactUIN,
  256. Online: false,
  257. Status: 0,
  258. Version: 0,
  259. }
  260. // Check if contact is online via legacy session manager
  261. legacySession := s.legacySessionManager.GetSession(contactUIN)
  262. if legacySession != nil {
  263. // Check visibility — invisible users should not appear online
  264. // unless the viewer is on their visible list
  265. contactStatus := legacySession.GetStatus()
  266. visible := true
  267. if contactStatus&ICQLegacyStatusInvisible != 0 {
  268. visible = legacySession.IsOnVisibleList(req.UIN)
  269. }
  270. if visible {
  271. status.Online = true
  272. status.Status = contactStatus
  273. s.logger.Debug("ProcessContactList: contact online (legacy)",
  274. "contact_uin", contactUIN,
  275. "status", fmt.Sprintf("0x%08X", status.Status),
  276. )
  277. }
  278. }
  279. // If not found in legacy sessions, check OSCAR sessions
  280. if !status.Online {
  281. contactScreenName := state.NewIdentScreenName(strconv.FormatUint(uint64(contactUIN), 10))
  282. oscarSession := s.sessionRetriever.RetrieveSession(contactScreenName)
  283. if oscarSession != nil {
  284. status.Online = true
  285. // Read actual OSCAR status and map to legacy
  286. userInfo := oscarSession.TLVUserInfo()
  287. oscarStatusVal, _ := userInfo.Uint32BE(wire.OServiceUserInfoStatus)
  288. status.Status = mapOSCARStatusToLegacy(oscarStatusVal)
  289. status.Version = 0 // OSCAR client, not legacy
  290. s.logger.Debug("ProcessContactList: contact online (OSCAR)",
  291. "contact_uin", contactUIN,
  292. "status", fmt.Sprintf("0x%08X", status.Status),
  293. )
  294. }
  295. }
  296. result.OnlineContacts = append(result.OnlineContacts, status)
  297. }
  298. s.logger.Debug("ProcessContactList: completed",
  299. "owner_uin", req.UIN,
  300. "total_contacts", len(req.Contacts),
  301. "online_count", countOnlineContacts(result.OnlineContacts),
  302. )
  303. return result, nil
  304. }
  305. // countOnlineContacts counts the number of online contacts in a ContactStatus slice.
  306. func countOnlineContacts(contacts []ContactStatus) int {
  307. count := 0
  308. for _, c := range contacts {
  309. if c.Online {
  310. count++
  311. }
  312. }
  313. return count
  314. }
  315. // ProcessUserAdd processes a user add request and returns information about the target user.
  316. // This is the service layer method for user add operations that handlers call after
  317. // parsing user add packets (CMD_USER_ADD). It checks if the target user is online
  318. // and returns their status, along with whether to send a "you were added" notification.
  319. //
  320. // The method does NOT contain any protocol-specific packet building logic.
  321. // Handlers are responsible for:
  322. // - Parsing protocol-specific user add packets into UserAddRequest
  323. // - Using the returned UserAddResult to send online notifications if target is online
  324. // - Building protocol-specific "you were added" notifications if SendYouWereAdded is true
  325. //
  326. // From iserverd v3_process_useradd() and v5_process_useradd() - when a user adds
  327. // someone to their contact list, the server checks if the target is online and
  328. // optionally sends a "you were added" notification to the target.
  329. func (s *ICQLegacyService) ProcessUserAdd(ctx context.Context, instance *state.SessionInstance, req UserAddRequest) (*UserAddResult, error) {
  330. result := &UserAddResult{
  331. TargetOnline: false,
  332. TargetStatus: 0,
  333. TargetVersion: 0,
  334. SendYouWereAdded: false,
  335. }
  336. // Validate request
  337. if req.FromUIN == 0 {
  338. s.logger.Debug("ProcessUserAdd: invalid from UIN")
  339. return result, nil
  340. }
  341. if req.TargetUIN == 0 {
  342. s.logger.Debug("ProcessUserAdd: invalid target UIN")
  343. return result, nil
  344. }
  345. s.logger.Debug("ProcessUserAdd: processing user add",
  346. "from_uin", req.FromUIN,
  347. "target_uin", req.TargetUIN,
  348. )
  349. // Sync to clientSideBuddyList so OSCAR's BuddyArrived/Departed
  350. // notifications reach this legacy user for the newly added contact.
  351. contactName := state.NewIdentScreenName(strconv.FormatUint(uint64(req.TargetUIN), 10))
  352. snac := wire.SNAC_0x03_0x04_BuddyAddBuddies{}
  353. snac.Buddies = append(snac.Buddies, struct {
  354. ScreenName string `oscar:"len_prefix=uint8"`
  355. }{ScreenName: contactName.String()})
  356. if _, err := s.buddyService.AddBuddies(ctx, instance, wire.SNACFrame{}, snac); err != nil {
  357. s.logger.Error("ProcessUserAdd: failed to add buddy via BuddyService",
  358. "from_uin", req.FromUIN,
  359. "target_uin", req.TargetUIN,
  360. "err", err,
  361. )
  362. }
  363. // Check if target user is online via legacy session manager
  364. legacySession := s.legacySessionManager.GetSession(req.TargetUIN)
  365. if legacySession != nil {
  366. result.TargetOnline = true
  367. result.TargetStatus = legacySession.GetStatus()
  368. // SendYouWereAdded is true for legacy clients so they know someone added them
  369. result.SendYouWereAdded = true
  370. s.logger.Debug("ProcessUserAdd: target online (legacy)",
  371. "target_uin", req.TargetUIN,
  372. "status", fmt.Sprintf("0x%08X", result.TargetStatus),
  373. )
  374. return result, nil
  375. }
  376. // Check if target user is online via OSCAR session
  377. targetScreenName := state.NewIdentScreenName(strconv.FormatUint(uint64(req.TargetUIN), 10))
  378. oscarSession := s.sessionRetriever.RetrieveSession(targetScreenName)
  379. if oscarSession != nil {
  380. result.TargetOnline = true
  381. result.TargetStatus = ICQLegacyStatusOnline
  382. result.TargetVersion = 0 // OSCAR client, not legacy
  383. // For OSCAR clients, we don't send "you were added" via legacy protocol
  384. // OSCAR has its own buddy notification mechanism
  385. result.SendYouWereAdded = false
  386. s.logger.Debug("ProcessUserAdd: target online (OSCAR)",
  387. "target_uin", req.TargetUIN,
  388. )
  389. return result, nil
  390. }
  391. // Target is offline
  392. s.logger.Debug("ProcessUserAdd: target offline",
  393. "target_uin", req.TargetUIN,
  394. )
  395. return result, nil
  396. }
  397. // ProcessMessage handles message routing and offline storage.
  398. // This is the service layer method for messaging that handlers call after
  399. // parsing message packets. It determines if the target user is online and
  400. // returns routing information, or stores the message for offline delivery.
  401. //
  402. // The method does NOT contain any protocol-specific packet building logic.
  403. // Handlers are responsible for:
  404. // - Parsing protocol-specific message packets into MessageRequest
  405. // - Using the returned MessageResult to route messages or confirm storage
  406. // - Building protocol-specific responses
  407. func (s *ICQLegacyService) ProcessMessage(ctx context.Context, session *LegacySession, req MessageRequest) (*MessageResult, error) {
  408. result := &MessageResult{
  409. Delivered: false,
  410. StoredOffline: false,
  411. TargetOnline: false,
  412. TargetVersion: 0,
  413. }
  414. // Validate request
  415. if req.FromUIN == 0 {
  416. s.logger.Debug("ProcessMessage: invalid sender UIN")
  417. return result, nil
  418. }
  419. if req.ToUIN == 0 {
  420. s.logger.Debug("ProcessMessage: invalid target UIN")
  421. return result, nil
  422. }
  423. s.logger.Debug("ProcessMessage: processing message",
  424. "from", req.FromUIN,
  425. "to", req.ToUIN,
  426. "type", fmt.Sprintf("0x%04X", req.MsgType),
  427. "msg_len", len(req.Message),
  428. )
  429. // Check if target user is online via legacy session manager
  430. legacySession := s.legacySessionManager.GetSession(req.ToUIN)
  431. if legacySession != nil {
  432. // Target is online via legacy protocol
  433. result.TargetOnline = true
  434. result.Delivered = true
  435. // Get the protocol version from the session
  436. // The handler will use this to route to the correct protocol handler
  437. result.TargetVersion = ICQLegacyVersionV5 // Default, actual version determined by session
  438. s.logger.Debug("ProcessMessage: target online (legacy)",
  439. "to", req.ToUIN,
  440. "status", fmt.Sprintf("0x%08X", legacySession.GetStatus()),
  441. )
  442. return result, nil
  443. }
  444. // Check if target user is online via OSCAR session
  445. toScreenName := state.NewIdentScreenName(strconv.FormatUint(uint64(req.ToUIN), 10))
  446. // Target is online via OSCAR protocol
  447. result.TargetOnline = true
  448. result.Delivered = true
  449. result.TargetVersion = 0 // OSCAR client, not legacy
  450. s.logger.Debug("ProcessMessage: target online (OSCAR)",
  451. "to", req.ToUIN,
  452. )
  453. // Send message to OSCAR client
  454. fromScreenName := state.NewIdentScreenName(strconv.FormatUint(uint64(req.FromUIN), 10))
  455. frame := wire.SNACFrame{
  456. FoodGroup: wire.ICBM,
  457. SubGroup: wire.ICBMChannelMsgToHost,
  458. }
  459. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  460. ChannelID: wire.ICBMChannelICQ,
  461. ScreenName: toScreenName.String(),
  462. TLVRestBlock: wire.TLVRestBlock{
  463. TLVList: wire.TLVList{
  464. wire.NewTLVLE(wire.ICBMTLVData, wire.ICBMCh4Message{
  465. UIN: fromScreenName.UIN(),
  466. MessageType: uint8(req.MsgType),
  467. Message: req.Message,
  468. }),
  469. wire.NewTLVBE(wire.ICBMTLVStore, []byte{}),
  470. },
  471. },
  472. }
  473. resp, err := s.icbmService.ChannelMsgToHost(ctx, session.Instance, frame, snac)
  474. if err != nil {
  475. s.logger.Error("ProcessMessage: failed to send to OSCAR client",
  476. "to", req.ToUIN,
  477. "err", err,
  478. )
  479. // Still mark as delivered since we attempted
  480. }
  481. if resp != nil && resp.Frame.FoodGroup == wire.ICBM && resp.Frame.SubGroup == wire.ICBMErr {
  482. if snErr, ok := resp.Body.(wire.SNACError); ok {
  483. if snErr.Code == wire.ErrorCodeNotLoggedOn {
  484. result.Delivered = false
  485. result.StoredOffline = true
  486. result.TargetOnline = false
  487. s.logger.Info("ProcessMessage: message stored for offline delivery",
  488. "from", req.FromUIN,
  489. "to", req.ToUIN,
  490. "type", fmt.Sprintf("0x%04X", req.MsgType),
  491. )
  492. }
  493. }
  494. }
  495. return result, nil
  496. }
  497. // RegisterNewUser creates a new user account for legacy ICQ registration.
  498. // It generates a new unique UIN, creates the user record with the provided
  499. // profile information, and stores the password hash.
  500. // Returns the newly assigned UIN on success.
  501. func (s *ICQLegacyService) RegisterNewUser(ctx context.Context, nickname, firstName, lastName, email, password string) (uint32, error) {
  502. // Generate a new UIN
  503. // For now, we'll use a simple approach: find the highest existing UIN and add 1
  504. // In production, this should be more robust (e.g., use a sequence in the database)
  505. newUIN, err := s.generateNewUIN(ctx)
  506. if err != nil {
  507. return 0, fmt.Errorf("generating new UIN: %w", err)
  508. }
  509. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(newUIN), 10))
  510. // Create the user with the generated password
  511. newUser := state.User{
  512. IdentScreenName: screenName,
  513. DisplayScreenName: state.DisplayScreenName(strconv.FormatUint(uint64(newUIN), 10)),
  514. IsICQ: true,
  515. ICQInfo: state.ICQInfo{
  516. Basic: state.ICQBasicInfo{
  517. Nickname: nickname,
  518. FirstName: firstName,
  519. LastName: lastName,
  520. EmailAddress: email,
  521. },
  522. },
  523. }
  524. // Generate auth key and password hash
  525. newUser.AuthKey = uuid.New().String()
  526. newUser.StrongMD5Pass = wire.StrongMD5PasswordHash(password, newUser.AuthKey)
  527. // Insert the user
  528. if err := s.userManager.InsertUser(ctx, newUser); err != nil {
  529. return 0, fmt.Errorf("inserting new user: %w", err)
  530. }
  531. s.logger.Info("registered new legacy ICQ user",
  532. "uin", newUIN,
  533. "nickname", nickname,
  534. "email", email,
  535. )
  536. return newUIN, nil
  537. }
  538. // generateNewUIN generates a new unique UIN for registration
  539. func (s *ICQLegacyService) generateNewUIN(ctx context.Context) (uint32, error) {
  540. // Start from a base UIN (e.g., 100000) and find the next available
  541. // This is a simple implementation - in production, use database sequences
  542. baseUIN := uint32(100000)
  543. for uin := baseUIN; uin < 999999999; uin++ {
  544. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  545. user, err := s.userManager.User(ctx, screenName)
  546. if err != nil {
  547. if errors.Is(err, state.ErrNoUser) {
  548. // This UIN is available
  549. return uin, nil
  550. }
  551. return 0, fmt.Errorf("checking UIN availability: %w", err)
  552. }
  553. if user == nil {
  554. // This UIN is available
  555. return uin, nil
  556. }
  557. }
  558. return 0, errors.New("no available UINs")
  559. }
  560. // GetOfflineMessages retrieves stored offline messages for the given UIN.
  561. // Messages are converted from the internal OSCAR ICBM format to the legacy
  562. // LegacyOfflineMessage format suitable for delivery by protocol handlers.
  563. func (s *ICQLegacyService) GetOfflineMessages(ctx context.Context, uin uint32) ([]LegacyOfflineMessage, error) {
  564. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  565. messages, err := s.offlineMessageManager.RetrieveMessages(ctx, screenName)
  566. if err != nil {
  567. return nil, fmt.Errorf("retrieving offline messages: %w", err)
  568. }
  569. result := make([]LegacyOfflineMessage, 0, len(messages))
  570. for _, msg := range messages {
  571. legacyMsg := LegacyOfflineMessage{
  572. FromUIN: msg.Sender.UIN(),
  573. ToUIN: uin,
  574. Timestamp: msg.Sent,
  575. }
  576. // Extract message text from OSCAR format
  577. if payload, hasIM := msg.Message.Bytes(wire.ICBMTLVAOLIMData); hasIM {
  578. msgText, err := wire.UnmarshalICBMMessageText(payload)
  579. if err == nil {
  580. legacyMsg.Message = msgText
  581. legacyMsg.MsgType = ICQLegacyMsgText
  582. }
  583. }
  584. // Restore the original legacy message type if it was stored.
  585. // Without this, auth requests and "you were added" messages
  586. // are delivered as normal text (0x0001), causing the client
  587. // to display the raw FE-delimited fields as garbage.
  588. if blob, hasBlob := msg.Message.Bytes(wire.ICBMTLVICQBlob); hasBlob && len(blob) >= 2 {
  589. legacyMsg.MsgType = uint16(blob[0]) | uint16(blob[1])<<8
  590. }
  591. result = append(result, legacyMsg)
  592. }
  593. return result, nil
  594. }
  595. // AckOfflineMessages acknowledges and deletes offline messages for the given UIN.
  596. // Called after a client has received all offline messages to clear them from storage.
  597. func (s *ICQLegacyService) AckOfflineMessages(ctx context.Context, uin uint32) error {
  598. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  599. return s.offlineMessageManager.DeleteMessages(ctx, screenName)
  600. }
  601. // GetUserInfo retrieves basic user information as a LegacyUserSearchResult.
  602. // It first attempts to find the user via ICQUserFinder, falling back to a
  603. // basic user lookup if that fails. Returns minimal info if the user is not found.
  604. func (s *ICQLegacyService) GetUserInfo(ctx context.Context, uin uint32) (*LegacyUserSearchResult, error) {
  605. user, err := s.userFinder.FindByUIN(ctx, uin)
  606. if err != nil {
  607. s.logger.Debug("FindByUIN failed, trying basic lookup", "uin", uin, "err", err)
  608. // Try basic user lookup as fallback
  609. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  610. basicUser, basicErr := s.userManager.User(ctx, screenName)
  611. if basicErr != nil {
  612. s.logger.Debug("basic user lookup also failed", "uin", uin, "err", basicErr)
  613. // Return minimal info
  614. return &LegacyUserSearchResult{
  615. UIN: uin,
  616. Nickname: strconv.FormatUint(uint64(uin), 10),
  617. }, nil
  618. }
  619. if basicUser != nil {
  620. return &LegacyUserSearchResult{
  621. UIN: uin,
  622. Nickname: basicUser.ICQInfo.Basic.Nickname,
  623. FirstName: basicUser.ICQInfo.Basic.FirstName,
  624. LastName: basicUser.ICQInfo.Basic.LastName,
  625. Email: basicUser.ICQInfo.Basic.EmailAddress,
  626. }, nil
  627. }
  628. // Return minimal info
  629. return &LegacyUserSearchResult{
  630. UIN: uin,
  631. Nickname: strconv.FormatUint(uint64(uin), 10),
  632. }, nil
  633. }
  634. return s.userToSearchResult(user), nil
  635. }
  636. // GetFullUserInfo retrieves the complete user record including all ICQ info fields.
  637. // This is used by V3 info packets that need home, work, and more info fields.
  638. // From iserverd v3_send_home_info(), v3_send_work_info(), etc.
  639. func (s *ICQLegacyService) GetFullUserInfo(ctx context.Context, uin uint32) (*state.User, error) {
  640. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  641. user, err := s.userManager.User(ctx, screenName)
  642. if err != nil {
  643. s.logger.Debug("GetFullUserInfo failed", "uin", uin, "err", err)
  644. return nil, err
  645. }
  646. return user, nil
  647. }
  648. // GetUserInfoForProtocol retrieves user info and returns it as a typed UserInfoResult.
  649. // This is the service layer method for user info retrieval that handlers call after
  650. // parsing info request packets. It consolidates user info retrieval from the database
  651. // and returns a typed result struct containing all user profile fields.
  652. //
  653. // The method does NOT contain any protocol-specific packet building logic.
  654. // Handlers are responsible for:
  655. // - Parsing protocol-specific info request packets
  656. // - Using the returned UserInfoResult to build protocol-specific response packets
  657. func (s *ICQLegacyService) GetUserInfoForProtocol(ctx context.Context, targetUIN uint32) (*UserInfoResult, error) {
  658. if targetUIN == 0 {
  659. s.logger.Debug("GetUserInfoForProtocol: invalid target UIN")
  660. return nil, nil
  661. }
  662. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(targetUIN), 10))
  663. user, err := s.userManager.User(ctx, screenName)
  664. if err != nil {
  665. if errors.Is(err, state.ErrNoUser) {
  666. s.logger.Debug("GetUserInfoForProtocol: user not found", "uin", targetUIN)
  667. // Return minimal info for non-existent user
  668. return &UserInfoResult{
  669. UIN: targetUIN,
  670. Nickname: strconv.FormatUint(uint64(targetUIN), 10),
  671. }, nil
  672. }
  673. s.logger.Debug("GetUserInfoForProtocol: failed to retrieve user", "uin", targetUIN, "err", err)
  674. return nil, fmt.Errorf("retrieving user info: %w", err)
  675. }
  676. if user == nil {
  677. s.logger.Debug("GetUserInfoForProtocol: user not found", "uin", targetUIN)
  678. // Return minimal info for non-existent user
  679. return &UserInfoResult{
  680. UIN: targetUIN,
  681. Nickname: strconv.FormatUint(uint64(targetUIN), 10),
  682. }, nil
  683. }
  684. // Build the result from user data
  685. result := &UserInfoResult{
  686. // Basic fields
  687. UIN: targetUIN,
  688. Nickname: user.ICQInfo.Basic.Nickname,
  689. FirstName: user.ICQInfo.Basic.FirstName,
  690. LastName: user.ICQInfo.Basic.LastName,
  691. Email: user.ICQInfo.Basic.EmailAddress,
  692. Gender: uint8(user.ICQInfo.More.Gender),
  693. Age: uint8(user.Age(s.timeNow)),
  694. // Extended fields - location
  695. City: user.ICQInfo.Basic.City,
  696. State: user.ICQInfo.Basic.State,
  697. Country: user.ICQInfo.Basic.CountryCode,
  698. Phone: user.ICQInfo.Basic.Phone,
  699. // Extended fields - more info
  700. Homepage: user.ICQInfo.More.HomePageAddr,
  701. About: user.ICQInfo.Notes.Notes,
  702. // Work info
  703. Company: user.ICQInfo.Work.Company,
  704. Department: user.ICQInfo.Work.Department,
  705. Position: user.ICQInfo.Work.Position,
  706. // Auth required
  707. AuthRequired: 1,
  708. }
  709. // Use UIN as fallback nickname if not set
  710. if result.Nickname == "" {
  711. result.Nickname = strconv.FormatUint(uint64(targetUIN), 10)
  712. }
  713. // Set auth required flag
  714. if user.ICQInfo.Permissions.AuthRequired {
  715. result.AuthRequired = 0
  716. }
  717. // Check if user is online
  718. // First check legacy sessions
  719. legacySession := s.legacySessionManager.GetSession(targetUIN)
  720. if legacySession != nil {
  721. result.Online = true
  722. result.Status = legacySession.GetStatus()
  723. s.logger.Debug("GetUserInfoForProtocol: user online (legacy)",
  724. "uin", targetUIN,
  725. "status", fmt.Sprintf("0x%08X", result.Status),
  726. )
  727. return result, nil
  728. }
  729. // Check OSCAR sessions
  730. oscarSession := s.sessionRetriever.RetrieveSession(screenName)
  731. if oscarSession != nil {
  732. result.Online = true
  733. result.Status = ICQLegacyStatusOnline
  734. s.logger.Debug("GetUserInfoForProtocol: user online (OSCAR)", "uin", targetUIN)
  735. }
  736. s.logger.Debug("GetUserInfoForProtocol: retrieved user info",
  737. "uin", targetUIN,
  738. "nickname", result.Nickname,
  739. "online", result.Online,
  740. )
  741. return result, nil
  742. }
  743. // SearchByUIN searches for a user by their UIN and returns their profile info.
  744. // Returns nil and an error if the user is not found.
  745. func (s *ICQLegacyService) SearchByUIN(ctx context.Context, uin uint32) (*LegacyUserSearchResult, error) {
  746. user, err := s.userFinder.FindByUIN(ctx, uin)
  747. if err != nil {
  748. return nil, err
  749. }
  750. return s.userToSearchResult(user), nil
  751. }
  752. // SearchByName searches for users by nickname, first name, last name, or email.
  753. // If email is provided, it takes priority over name-based search.
  754. // Returns a slice of matching users.
  755. func (s *ICQLegacyService) SearchByName(ctx context.Context, nick, first, last, email string) ([]LegacyUserSearchResult, error) {
  756. var users []state.User
  757. var err error
  758. if email != "" {
  759. user, findErr := s.userFinder.FindByICQEmail(ctx, email)
  760. if findErr == nil {
  761. users = []state.User{user}
  762. }
  763. } else {
  764. users, err = s.userFinder.FindByICQName(ctx, first, last, nick)
  765. }
  766. if err != nil {
  767. return nil, err
  768. }
  769. results := make([]LegacyUserSearchResult, 0, len(users))
  770. for _, user := range users {
  771. results = append(results, *s.userToSearchResult(user))
  772. }
  773. return results, nil
  774. }
  775. // WhitePagesSearch performs a comprehensive search across multiple user profile fields.
  776. // This is used by the V5 META_SEARCH_WHITE (0x0532) and META_SEARCH_WHITE2 (0x0533) commands.
  777. // From iserverd v5_search_by_white() and v5_search_by_white2() in search.cpp
  778. //
  779. // The search supports multiple criteria including:
  780. // - Personal info: first name, last name, nickname, email
  781. // - Demographics: age range, gender, language
  782. // - Location: city, state, country
  783. // - Work: company, department, position, occupation code
  784. // - Interests and affiliations with keywords
  785. // - Homepage category (White2 only)
  786. // - Online-only filter
  787. //
  788. // Results are limited to 40 users maximum, matching iserverd behavior.
  789. func (s *ICQLegacyService) WhitePagesSearch(ctx context.Context, criteria WhitePagesSearchCriteria) ([]LegacyUserSearchResult, error) {
  790. var allResults []state.User
  791. // Start with basic name/email search if those criteria are provided
  792. // This is the primary search method available in the current infrastructure
  793. hasBasicCriteria := criteria.Nickname != "" || criteria.FirstName != "" ||
  794. criteria.LastName != "" || criteria.Email != ""
  795. if hasBasicCriteria {
  796. if criteria.Email != "" {
  797. // Search by email first
  798. user, findErr := s.userFinder.FindByICQEmail(ctx, criteria.Email)
  799. if findErr == nil {
  800. allResults = []state.User{user}
  801. }
  802. }
  803. // If no email results or no email provided, search by name
  804. if len(allResults) == 0 && (criteria.Nickname != "" || criteria.FirstName != "" || criteria.LastName != "") {
  805. users, findErr := s.userFinder.FindByICQName(ctx, criteria.FirstName, criteria.LastName, criteria.Nickname)
  806. if findErr == nil {
  807. allResults = users
  808. }
  809. }
  810. }
  811. // Search by interests if interest criteria provided
  812. if criteria.InterestIndex > 0 && criteria.InterestIndex < 60000 {
  813. var keywords []string
  814. if criteria.InterestKeywords != "" {
  815. keywords = []string{criteria.InterestKeywords}
  816. }
  817. users, findErr := s.userFinder.FindByICQInterests(ctx, criteria.InterestIndex, keywords)
  818. if findErr == nil {
  819. allResults = s.mergeUserResults(allResults, users)
  820. }
  821. }
  822. // Search by keyword if interest keywords provided without index
  823. if criteria.InterestKeywords != "" && criteria.InterestIndex == 0 {
  824. users, findErr := s.userFinder.FindByICQKeyword(ctx, criteria.InterestKeywords)
  825. if findErr == nil {
  826. allResults = s.mergeUserResults(allResults, users)
  827. }
  828. }
  829. // Filter results based on additional criteria
  830. filteredResults := s.filterWhitePagesResults(allResults, criteria)
  831. // Convert to search results
  832. results := make([]LegacyUserSearchResult, 0, len(filteredResults))
  833. for _, user := range filteredResults {
  834. result := s.userToSearchResult(user)
  835. // Apply online-only filter if requested
  836. if criteria.OnlineOnly && !result.Online {
  837. continue
  838. }
  839. results = append(results, *result)
  840. // Limit to 40 results as per iserverd behavior
  841. if len(results) >= 40 {
  842. break
  843. }
  844. }
  845. s.logger.Debug("white pages search completed",
  846. "criteria_nickname", criteria.Nickname,
  847. "criteria_firstname", criteria.FirstName,
  848. "criteria_lastname", criteria.LastName,
  849. "criteria_email", criteria.Email,
  850. "criteria_online_only", criteria.OnlineOnly,
  851. "total_found", len(allResults),
  852. "filtered_results", len(results),
  853. )
  854. return results, nil
  855. }
  856. // mergeUserResults merges two user slices, removing duplicates by UIN
  857. func (s *ICQLegacyService) mergeUserResults(existing, new []state.User) []state.User {
  858. seen := make(map[string]bool)
  859. for _, u := range existing {
  860. seen[u.IdentScreenName.String()] = true
  861. }
  862. result := make([]state.User, len(existing))
  863. copy(result, existing)
  864. for _, u := range new {
  865. if !seen[u.IdentScreenName.String()] {
  866. result = append(result, u)
  867. seen[u.IdentScreenName.String()] = true
  868. }
  869. }
  870. return result
  871. }
  872. // filterWhitePagesResults filters users based on white pages search criteria
  873. func (s *ICQLegacyService) filterWhitePagesResults(users []state.User, criteria WhitePagesSearchCriteria) []state.User {
  874. if len(users) == 0 {
  875. return users
  876. }
  877. result := make([]state.User, 0, len(users))
  878. for _, user := range users {
  879. // Filter by age range
  880. if criteria.MinAge > 0 && criteria.MaxAge > 0 {
  881. age := uint16(user.Age(s.timeNow))
  882. if age < criteria.MinAge || age > criteria.MaxAge {
  883. continue
  884. }
  885. }
  886. // Filter by gender (1=female, 2=male)
  887. if criteria.Gender > 0 && criteria.Gender < 16 {
  888. if uint8(user.ICQInfo.More.Gender) != criteria.Gender {
  889. continue
  890. }
  891. }
  892. // Filter by language
  893. if criteria.Language > 0 && criteria.Language < 127 {
  894. langMatch := false
  895. if user.ICQInfo.More.Lang1 == criteria.Language ||
  896. user.ICQInfo.More.Lang2 == criteria.Language ||
  897. user.ICQInfo.More.Lang3 == criteria.Language {
  898. langMatch = true
  899. }
  900. if !langMatch {
  901. continue
  902. }
  903. }
  904. // Filter by country (home country is in ICQBasicInfo)
  905. if criteria.Country > 0 && criteria.Country < 20000 {
  906. if user.ICQInfo.Basic.CountryCode != criteria.Country {
  907. continue
  908. }
  909. }
  910. // Filter by city (case-insensitive partial match, home city is in ICQBasicInfo)
  911. if criteria.City != "" {
  912. if !containsIgnoreCase(user.ICQInfo.Basic.City, criteria.City) {
  913. continue
  914. }
  915. }
  916. // Filter by state (case-insensitive partial match, home state is in ICQBasicInfo)
  917. if criteria.State != "" {
  918. if !containsIgnoreCase(user.ICQInfo.Basic.State, criteria.State) {
  919. continue
  920. }
  921. }
  922. // Filter by company (case-insensitive partial match)
  923. if criteria.Company != "" {
  924. if !containsIgnoreCase(user.ICQInfo.Work.Company, criteria.Company) {
  925. continue
  926. }
  927. }
  928. // Filter by position (case-insensitive partial match)
  929. if criteria.Position != "" {
  930. if !containsIgnoreCase(user.ICQInfo.Work.Position, criteria.Position) {
  931. continue
  932. }
  933. }
  934. // Filter by work code/occupation
  935. if criteria.WorkCode > 0 && criteria.WorkCode < 127 {
  936. if uint8(user.ICQInfo.Work.OccupationCode) != criteria.WorkCode {
  937. continue
  938. }
  939. }
  940. result = append(result, user)
  941. }
  942. return result
  943. }
  944. // containsIgnoreCase checks if s contains substr (case-insensitive).
  945. func containsIgnoreCase(s, substr string) bool {
  946. if substr == "" {
  947. return true
  948. }
  949. if s == "" {
  950. return false
  951. }
  952. // Simple case-insensitive contains using lowercase comparison
  953. sLower := strings.ToLower(s)
  954. substrLower := strings.ToLower(substr)
  955. return strings.Contains(sLower, substrLower)
  956. }
  957. // ChangeStatus updates a user's status in the service layer.
  958. // The actual session status is managed by the session object; this method
  959. // provides a hook for logging and any future business logic.
  960. func (s *ICQLegacyService) ChangeStatus(ctx context.Context, uin uint32, status uint32) error {
  961. // Status is managed by the session, this is just for logging
  962. s.logger.Debug("status changed",
  963. "uin", uin,
  964. "status", fmt.Sprintf("0x%08X", status),
  965. )
  966. return nil
  967. }
  968. // ProcessStatusChange processes a status change and returns notification targets.
  969. // This is the service layer method for status changes that handlers call after
  970. // parsing status change packets. It determines which users should be notified
  971. // of the status change (users who have this user in their contact list).
  972. //
  973. // The method does NOT contain any protocol-specific packet building logic.
  974. // The method does NOT directly send packets to other sessions.
  975. // Handlers are responsible for:
  976. // - Parsing protocol-specific status change packets into StatusChangeRequest
  977. // - Using the returned StatusChangeResult to send notifications to each target
  978. // - Building protocol-specific notification packets
  979. func (s *ICQLegacyService) ProcessStatusChange(ctx context.Context, req StatusChangeRequest) (*StatusChangeResult, error) {
  980. result := &StatusChangeResult{
  981. NotifyTargets: make([]NotifyTarget, 0),
  982. }
  983. // Validate request
  984. if req.UIN == 0 {
  985. s.logger.Debug("ProcessStatusChange: invalid UIN")
  986. return result, nil
  987. }
  988. s.logger.Debug("ProcessStatusChange: processing status change",
  989. "uin", req.UIN,
  990. "old_status", fmt.Sprintf("0x%08X", req.OldStatus),
  991. "new_status", fmt.Sprintf("0x%08X", req.NewStatus),
  992. )
  993. // Get all users who have this user in their contact list
  994. // These are the users who should be notified of the status change
  995. // Use the legacy session manager's NotifyContactsOfStatus method
  996. session := s.legacySessionManager.GetSession(req.UIN)
  997. if session != nil {
  998. // Get all UINs that should be notified (users who have this user in their contact list)
  999. contactsToNotify := s.legacySessionManager.NotifyContactsOfStatus(session)
  1000. for _, contactUIN := range contactsToNotify {
  1001. result.NotifyTargets = append(result.NotifyTargets, NotifyTarget{
  1002. UIN: contactUIN,
  1003. Version: 0, // Version will be determined by handler from session
  1004. })
  1005. }
  1006. }
  1007. // Also notify OSCAR clients who have this user as a buddy
  1008. // by broadcasting through the buddy broadcaster
  1009. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(req.UIN), 10))
  1010. // Update the legacy session's OSCAR instance status and broadcast
  1011. // using session.TLVUserInfo() — exactly like OSCAR's SetUserInfoFields does.
  1012. statusSession := s.legacySessionManager.GetSession(req.UIN)
  1013. if statusSession != nil && statusSession.Instance != nil {
  1014. oscarStatus := mapLegacyStatusToOSCAR(req.NewStatus)
  1015. statusSession.Instance.SetUserStatusBitmask(oscarStatus)
  1016. if oscarStatus != wire.OServiceUserStatusAvailable {
  1017. statusSession.Instance.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
  1018. } else {
  1019. statusSession.Instance.ClearUserInfoFlag(wire.OServiceUserFlagUnavailable)
  1020. }
  1021. // Mirror OSCAR's SetUserInfoFields: if invisible, send departure;
  1022. // otherwise send arrival with updated TLVUserInfo.
  1023. if statusSession.Instance.Session().Invisible() {
  1024. if err := s.buddyBroadcaster.BroadcastBuddyDeparted(ctx, screenName); err != nil {
  1025. s.logger.Debug("ProcessStatusChange: failed to broadcast departure", "err", err)
  1026. }
  1027. } else {
  1028. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, screenName, statusSession.Instance.Session().TLVUserInfo()); err != nil {
  1029. s.logger.Debug("ProcessStatusChange: failed to broadcast to OSCAR clients", "err", err)
  1030. }
  1031. }
  1032. }
  1033. s.logger.Debug("ProcessStatusChange: completed",
  1034. "uin", req.UIN,
  1035. "notify_count", len(result.NotifyTargets),
  1036. )
  1037. return result, nil
  1038. }
  1039. // NotifyStatusChange broadcasts a status change to OSCAR clients who have
  1040. // this user as a buddy. Legacy clients are notified separately by the handler
  1041. // using ProcessStatusChange results.
  1042. func (s *ICQLegacyService) NotifyStatusChange(ctx context.Context, uin uint32, status uint32) error {
  1043. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1044. oscarStatus := mapLegacyStatusToOSCAR(status)
  1045. // Update the OSCAR instance so session.TLVUserInfo() is correct
  1046. notifySession := s.legacySessionManager.GetSession(uin)
  1047. if notifySession != nil && notifySession.Instance != nil {
  1048. notifySession.Instance.SetUserStatusBitmask(oscarStatus)
  1049. if oscarStatus != wire.OServiceUserStatusAvailable {
  1050. notifySession.Instance.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
  1051. } else {
  1052. notifySession.Instance.ClearUserInfoFlag(wire.OServiceUserFlagUnavailable)
  1053. }
  1054. // Invisible: send departure so OSCAR clients see user as offline
  1055. if status&ICQLegacyStatusInvisible != 0 {
  1056. if err := s.buddyBroadcaster.BroadcastBuddyDeparted(ctx, screenName); err != nil {
  1057. s.logger.Debug("NotifyStatusChange: failed to broadcast departure", "err", err)
  1058. }
  1059. return nil
  1060. }
  1061. userInfo := notifySession.Instance.Session().TLVUserInfo()
  1062. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, screenName, userInfo); err != nil {
  1063. s.logger.Debug("NotifyStatusChange: failed to broadcast arrival", "err", err)
  1064. }
  1065. return nil
  1066. }
  1067. // Fallback if no session/instance — invisible should not broadcast arrival
  1068. if status&ICQLegacyStatusInvisible != 0 {
  1069. if err := s.buddyBroadcaster.BroadcastBuddyDeparted(ctx, screenName); err != nil {
  1070. s.logger.Debug("NotifyStatusChange: failed to broadcast departure", "err", err)
  1071. }
  1072. return nil
  1073. }
  1074. // Build minimal user info for OSCAR clients
  1075. userFlags := uint16(wire.OServiceUserFlagICQ | wire.OServiceUserFlagOSCARFree)
  1076. if oscarStatus != wire.OServiceUserStatusAvailable {
  1077. userFlags |= wire.OServiceUserFlagUnavailable
  1078. }
  1079. userInfo := wire.TLVUserInfo{
  1080. ScreenName: screenName.String(),
  1081. TLVBlock: wire.TLVBlock{
  1082. TLVList: wire.TLVList{
  1083. wire.NewTLVBE(wire.OServiceUserInfoUserFlags, userFlags),
  1084. wire.NewTLVBE(wire.OServiceUserInfoStatus, oscarStatus),
  1085. wire.NewTLVBE(wire.OServiceUserInfoSignonTOD, uint32(s.timeNow().Unix())),
  1086. wire.NewTLVBE(wire.OServiceUserInfoICQDC, wire.ICQDCInfo{}),
  1087. },
  1088. },
  1089. }
  1090. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, screenName, userInfo); err != nil {
  1091. s.logger.Debug("NotifyStatusChange: failed to broadcast arrival", "err", err)
  1092. }
  1093. return nil
  1094. }
  1095. // NotifyUserOnline broadcasts a user arrival to OSCAR clients who have
  1096. // this user as a buddy. Called after legacy login completes.
  1097. func (s *ICQLegacyService) NotifyUserOnline(ctx context.Context, uin uint32, status uint32) error {
  1098. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1099. // Register buddy list so OSCAR's AllRelationships can discover this legacy user.
  1100. // This mirrors what OSCAR and TOC servers do during signon.
  1101. if err := s.buddyListRegistry.RegisterBuddyList(ctx, screenName); err != nil {
  1102. s.logger.Error("NotifyUserOnline: failed to register buddy list", "uin", uin, "err", err)
  1103. }
  1104. // Update the legacy session's OSCAR instance status bitmask so that
  1105. // session.TLVUserInfo() reflects the correct status.
  1106. oscarStatus := mapLegacyStatusToOSCAR(status)
  1107. onlineSession := s.legacySessionManager.GetSession(uin)
  1108. if onlineSession != nil && onlineSession.Instance != nil {
  1109. onlineSession.Instance.SetUserStatusBitmask(oscarStatus)
  1110. if oscarStatus != wire.OServiceUserStatusAvailable {
  1111. onlineSession.Instance.SetUserInfoFlag(wire.OServiceUserFlagUnavailable)
  1112. } else {
  1113. onlineSession.Instance.ClearUserInfoFlag(wire.OServiceUserFlagUnavailable)
  1114. }
  1115. // Invisible: don't broadcast arrival — user should appear offline
  1116. if status&ICQLegacyStatusInvisible != 0 {
  1117. return nil
  1118. }
  1119. // Use the session's TLVUserInfo — same as OSCAR's SetUserInfoFields
  1120. userInfo := onlineSession.Instance.Session().TLVUserInfo()
  1121. if err := s.buddyBroadcaster.BroadcastBuddyArrived(ctx, screenName, userInfo); err != nil {
  1122. s.logger.Debug("NotifyUserOnline: failed to broadcast arrival", "uin", uin, "err", err)
  1123. }
  1124. }
  1125. return nil
  1126. }
  1127. // NotifyUserOffline broadcasts a user departure to OSCAR clients who have
  1128. // this user as a buddy. Legacy clients are notified separately by the handler.
  1129. func (s *ICQLegacyService) NotifyUserOffline(ctx context.Context, uin uint32) error {
  1130. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1131. if err := s.buddyBroadcaster.BroadcastBuddyDeparted(ctx, screenName); err != nil {
  1132. s.logger.Debug("failed to broadcast departure", "err", err)
  1133. }
  1134. // Unregister buddy list so OSCAR's AllRelationships no longer includes
  1135. // this legacy user. Mirrors OSCAR/TOC signoff behavior.
  1136. if err := s.buddyListRegistry.UnregisterBuddyList(ctx, screenName); err != nil {
  1137. s.logger.Error("NotifyUserOffline: failed to unregister buddy list", "uin", uin, "err", err)
  1138. }
  1139. return nil
  1140. }
  1141. // UpdateBasicInfo updates a user's basic profile information (nickname, name, email, etc.).
  1142. // This is used by V3/V5 handlers when processing META_SET_BASIC (0x03EA) commands.
  1143. func (s *ICQLegacyService) UpdateBasicInfo(ctx context.Context, uin uint32, info state.ICQBasicInfo) error {
  1144. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1145. return s.userUpdater.SetBasicInfo(ctx, screenName, info)
  1146. }
  1147. // UpdateWorkInfo updates a user's work information (company, department, position, etc.).
  1148. // This is used by V3/V5 handlers when processing META_SET_WORK (0x03F4) commands.
  1149. func (s *ICQLegacyService) UpdateWorkInfo(ctx context.Context, uin uint32, info state.ICQWorkInfo) error {
  1150. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1151. return s.userUpdater.SetWorkInfo(ctx, screenName, info)
  1152. }
  1153. // UpdateMoreInfo updates a user's additional profile information (homepage, birthday, languages, etc.).
  1154. // This is used by V3/V5 handlers when processing META_SET_MORE (0x03FE) commands.
  1155. func (s *ICQLegacyService) UpdateMoreInfo(ctx context.Context, uin uint32, info state.ICQMoreInfo) error {
  1156. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1157. return s.userUpdater.SetMoreInfo(ctx, screenName, info)
  1158. }
  1159. // UpdateInterests updates a user's interests in the database.
  1160. // Deprecated: Use SetInterests instead. This method is kept for backward compatibility.
  1161. func (s *ICQLegacyService) UpdateInterests(ctx context.Context, uin uint32, info state.ICQInterests) error {
  1162. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1163. return s.userUpdater.SetInterests(ctx, screenName, info)
  1164. }
  1165. // GetInterests retrieves the user's interests from the database.
  1166. // This is used by the V5 META_USER_FULLINFO response to return user interests.
  1167. // From iserverd v5_send_meta_interestsinfo() - returns user's interests.
  1168. //
  1169. // Parameters:
  1170. // - uin: The UIN of the user whose interests to retrieve
  1171. //
  1172. // Returns the ICQInterests struct and nil on success, or empty struct and error on failure.
  1173. func (s *ICQLegacyService) GetInterests(ctx context.Context, uin uint32) (*state.ICQInterests, error) {
  1174. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1175. user, err := s.userManager.User(ctx, screenName)
  1176. if err != nil {
  1177. s.logger.Debug("GetInterests: failed to get user", "uin", uin, "err", err)
  1178. return nil, fmt.Errorf("getting user: %w", err)
  1179. }
  1180. if user == nil {
  1181. s.logger.Debug("GetInterests: user not found", "uin", uin)
  1182. return &state.ICQInterests{}, nil
  1183. }
  1184. s.logger.Debug("GetInterests: retrieved interests",
  1185. "uin", uin,
  1186. "count", user.ICQInfo.Interests.Count,
  1187. )
  1188. return &user.ICQInfo.Interests, nil
  1189. }
  1190. // SetInterests saves the user's interests to the database.
  1191. // This is used by the V5 META_SET_INTERESTS (0x0410) command.
  1192. // From iserverd v5_set_interests_info() - updates user's interests.
  1193. //
  1194. // Parameters:
  1195. // - uin: The UIN of the user whose interests to update
  1196. // - interests: The new interests to save
  1197. //
  1198. // Returns nil on success, or an error if the update fails.
  1199. func (s *ICQLegacyService) SetInterests(ctx context.Context, uin uint32, interests state.ICQInterests) error {
  1200. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1201. if err := s.userUpdater.SetInterests(ctx, screenName, interests); err != nil {
  1202. s.logger.Error("SetInterests: failed to update interests",
  1203. "uin", uin,
  1204. "err", err,
  1205. )
  1206. return fmt.Errorf("updating interests: %w", err)
  1207. }
  1208. s.logger.Debug("SetInterests: interests updated",
  1209. "uin", uin,
  1210. "count", interests.Count,
  1211. )
  1212. return nil
  1213. }
  1214. // GetAffiliations retrieves the user's affiliations from the database.
  1215. // This is used by the V5 META_USER_FULLINFO response to return user affiliations.
  1216. // From iserverd v5_send_meta_affilationsinfo() - returns user's past and current affiliations.
  1217. //
  1218. // Parameters:
  1219. // - uin: The UIN of the user whose affiliations to retrieve
  1220. //
  1221. // Returns the ICQAffiliations struct and nil on success, or empty struct and error on failure.
  1222. func (s *ICQLegacyService) GetAffiliations(ctx context.Context, uin uint32) (*state.ICQAffiliations, error) {
  1223. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1224. user, err := s.userManager.User(ctx, screenName)
  1225. if err != nil {
  1226. s.logger.Debug("GetAffiliations: failed to get user", "uin", uin, "err", err)
  1227. return nil, fmt.Errorf("getting user: %w", err)
  1228. }
  1229. if user == nil {
  1230. s.logger.Debug("GetAffiliations: user not found", "uin", uin)
  1231. return &state.ICQAffiliations{}, nil
  1232. }
  1233. s.logger.Debug("GetAffiliations: retrieved affiliations",
  1234. "uin", uin,
  1235. "past_count", user.ICQInfo.Affiliations.PastCount,
  1236. "current_count", user.ICQInfo.Affiliations.CurrentCount,
  1237. )
  1238. return &user.ICQInfo.Affiliations, nil
  1239. }
  1240. // SetAffiliations saves the user's affiliations to the database.
  1241. // This is used by the V5 META_SET_AFFILIATIONS (0x041A) command.
  1242. // From iserverd v5_set_affilations_info() - updates user's past and current affiliations.
  1243. //
  1244. // Parameters:
  1245. // - uin: The UIN of the user whose affiliations to update
  1246. // - affiliations: The new affiliations to save
  1247. //
  1248. // Returns nil on success, or an error if the update fails.
  1249. func (s *ICQLegacyService) SetAffiliations(ctx context.Context, uin uint32, affiliations state.ICQAffiliations) error {
  1250. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1251. if err := s.userUpdater.SetAffiliations(ctx, screenName, affiliations); err != nil {
  1252. s.logger.Error("SetAffiliations: failed to update affiliations",
  1253. "uin", uin,
  1254. "err", err,
  1255. )
  1256. return fmt.Errorf("updating affiliations: %w", err)
  1257. }
  1258. s.logger.Debug("SetAffiliations: affiliations updated",
  1259. "uin", uin,
  1260. "past_count", affiliations.PastCount,
  1261. "current_count", affiliations.CurrentCount,
  1262. )
  1263. return nil
  1264. }
  1265. // UpdateAffiliations updates a user's affiliations
  1266. // Deprecated: Use SetAffiliations instead. This method is kept for backward compatibility.
  1267. func (s *ICQLegacyService) UpdateAffiliations(ctx context.Context, uin uint32, info state.ICQAffiliations) error {
  1268. return s.SetAffiliations(ctx, uin, info)
  1269. }
  1270. // UpdatePermissions updates a user's permission settings (auth required, web aware, etc.).
  1271. // This is used by V3/V5 handlers when processing permission-related commands.
  1272. func (s *ICQLegacyService) UpdatePermissions(ctx context.Context, uin uint32, info state.ICQPermissions) error {
  1273. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1274. return s.userUpdater.SetPermissions(ctx, screenName, info)
  1275. }
  1276. // DeleteUser removes a user account from the system.
  1277. // This is used by the V5 META_USER_UNREGISTER (0x04C4) command.
  1278. // The password must match the user's current password for the deletion to succeed.
  1279. // From iserverd v5_unregister_user() in meta_user.cpp
  1280. func (s *ICQLegacyService) DeleteUser(ctx context.Context, uin uint32, password string) error {
  1281. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1282. // First validate the password
  1283. valid, err := s.ValidateCredentials(ctx, uin, password)
  1284. if err != nil {
  1285. return fmt.Errorf("validating credentials: %w", err)
  1286. }
  1287. if !valid {
  1288. return errors.New("invalid password")
  1289. }
  1290. // Delete the user
  1291. if err := s.userManager.DeleteUser(ctx, screenName); err != nil {
  1292. return fmt.Errorf("deleting user: %w", err)
  1293. }
  1294. s.logger.Info("user account deleted (unregistered)",
  1295. "uin", uin,
  1296. )
  1297. return nil
  1298. }
  1299. // GetNotes retrieves the user's notes from the database.
  1300. // This is used by the V3 GET_NOTES (0x05AA) command.
  1301. // From iserverd v3_process_notes() - returns user's notes.
  1302. //
  1303. // Parameters:
  1304. // - uin: The UIN of the user whose notes to retrieve
  1305. //
  1306. // Returns the notes string and nil on success, or empty string and error on failure.
  1307. func (s *ICQLegacyService) GetNotes(ctx context.Context, uin uint32) (string, error) {
  1308. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1309. user, err := s.userManager.User(ctx, screenName)
  1310. if err != nil {
  1311. s.logger.Debug("GetNotes: failed to get user", "uin", uin, "err", err)
  1312. return "", fmt.Errorf("getting user: %w", err)
  1313. }
  1314. if user == nil {
  1315. s.logger.Debug("GetNotes: user not found", "uin", uin)
  1316. return "", nil
  1317. }
  1318. s.logger.Debug("GetNotes: retrieved notes",
  1319. "uin", uin,
  1320. "notes_len", len(user.ICQInfo.Notes.Notes),
  1321. )
  1322. return user.ICQInfo.Notes.Notes, nil
  1323. }
  1324. // SetNotes saves the user's notes to the database.
  1325. // This is used by the V3 SET_NOTES (0x0596) command.
  1326. // From iserverd v3_process_setnotes() - updates user's notes.
  1327. //
  1328. // Parameters:
  1329. // - uin: The UIN of the user whose notes to update
  1330. // - notes: The new notes content to save
  1331. //
  1332. // Returns nil on success, or an error if the update fails.
  1333. func (s *ICQLegacyService) SetNotes(ctx context.Context, uin uint32, notes string) error {
  1334. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1335. userNotes := state.ICQUserNotes{
  1336. Notes: notes,
  1337. }
  1338. if err := s.userUpdater.SetUserNotes(ctx, screenName, userNotes); err != nil {
  1339. s.logger.Error("SetNotes: failed to update notes",
  1340. "uin", uin,
  1341. "err", err,
  1342. )
  1343. return fmt.Errorf("updating notes: %w", err)
  1344. }
  1345. s.logger.Debug("SetNotes: notes updated",
  1346. "uin", uin,
  1347. "notes_len", len(notes),
  1348. )
  1349. return nil
  1350. }
  1351. // SetPassword changes the user's password.
  1352. // This is used by the V3 SET_PASSWORD (0x049C) command.
  1353. // From iserverd v3_process_setpass() - updates user's password.
  1354. //
  1355. // The iserverd implementation doesn't validate the old password - it just
  1356. // updates to the new password. However, for security, we optionally validate
  1357. // the old password if provided. If oldPassword is empty, we skip validation
  1358. // (matching iserverd behavior).
  1359. //
  1360. // Parameters:
  1361. // - uin: The UIN of the user whose password to change
  1362. // - oldPassword: The current password (empty to skip validation)
  1363. // - newPassword: The new password to set
  1364. //
  1365. // Returns nil on success, or an error if validation fails or update fails.
  1366. func (s *ICQLegacyService) SetPassword(ctx context.Context, uin uint32, oldPassword, newPassword string) error {
  1367. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1368. // If old password is provided, validate it first
  1369. if oldPassword != "" {
  1370. valid, err := s.ValidateCredentials(ctx, uin, oldPassword)
  1371. if err != nil {
  1372. s.logger.Error("SetPassword: failed to validate old password",
  1373. "uin", uin,
  1374. "err", err,
  1375. )
  1376. return fmt.Errorf("validating old password: %w", err)
  1377. }
  1378. if !valid {
  1379. s.logger.Info("SetPassword: old password validation failed",
  1380. "uin", uin,
  1381. )
  1382. return errors.New("invalid old password")
  1383. }
  1384. }
  1385. // Update the password using AccountManager
  1386. if err := s.accountManager.SetUserPassword(ctx, screenName, newPassword); err != nil {
  1387. s.logger.Error("SetPassword: failed to update password",
  1388. "uin", uin,
  1389. "err", err,
  1390. )
  1391. return fmt.Errorf("updating password: %w", err)
  1392. }
  1393. s.logger.Info("SetPassword: password updated successfully",
  1394. "uin", uin,
  1395. )
  1396. return nil
  1397. }
  1398. // SetAuthMode sets whether authorization is required to add the user to a contact list.
  1399. // This is used by the V3 SET_AUTH (0x0514) command.
  1400. // From iserverd v3_process_setauth() - updates user's auth mode in the database
  1401. // via db_users_setauthmode().
  1402. //
  1403. // When authRequired is true, other users must request authorization before adding
  1404. // this user to their contact list. The auth mode is stored in the user's
  1405. // ICQPermissions.AuthRequired field.
  1406. //
  1407. // Parameters:
  1408. // - uin: The UIN of the user whose auth mode to set
  1409. // - authRequired: true if authorization is required, false otherwise
  1410. //
  1411. // Returns nil on success, or an error if the update fails.
  1412. func (s *ICQLegacyService) SetAuthMode(ctx context.Context, uin uint32, authRequired bool) error {
  1413. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1414. // Get current user to preserve other permission settings
  1415. user, err := s.userManager.User(ctx, screenName)
  1416. if err != nil {
  1417. s.logger.Error("SetAuthMode: failed to get user",
  1418. "uin", uin,
  1419. "err", err,
  1420. )
  1421. return fmt.Errorf("getting user: %w", err)
  1422. }
  1423. if user == nil {
  1424. s.logger.Error("SetAuthMode: user not found",
  1425. "uin", uin,
  1426. )
  1427. return errors.New("user not found")
  1428. }
  1429. // Update the auth mode while preserving other permission settings
  1430. permissions := user.ICQInfo.Permissions
  1431. permissions.AuthRequired = authRequired
  1432. if err := s.userUpdater.SetPermissions(ctx, screenName, permissions); err != nil {
  1433. s.logger.Error("SetAuthMode: failed to update permissions",
  1434. "uin", uin,
  1435. "err", err,
  1436. )
  1437. return fmt.Errorf("updating permissions: %w", err)
  1438. }
  1439. s.logger.Debug("SetAuthMode: auth mode updated",
  1440. "uin", uin,
  1441. "auth_required", authRequired,
  1442. )
  1443. return nil
  1444. }
  1445. // GetHomepageCategory retrieves the user's homepage category from the database.
  1446. // This is used by the V5 META_USER_FULLINFO response to return user homepage category.
  1447. // From iserverd v5_send_meta_hpage_cat() - returns user's homepage category.
  1448. //
  1449. // Parameters:
  1450. // - uin: The UIN of the user whose homepage category to retrieve
  1451. //
  1452. // Returns the ICQHomepageCategory struct and nil on success, or empty struct and error on failure.
  1453. func (s *ICQLegacyService) GetHomepageCategory(ctx context.Context, uin uint32) (*state.ICQHomepageCategory, error) {
  1454. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1455. user, err := s.userManager.User(ctx, screenName)
  1456. if err != nil {
  1457. s.logger.Debug("GetHomepageCategory: failed to get user", "uin", uin, "err", err)
  1458. return nil, fmt.Errorf("getting user: %w", err)
  1459. }
  1460. if user == nil {
  1461. s.logger.Debug("GetHomepageCategory: user not found", "uin", uin)
  1462. return &state.ICQHomepageCategory{}, nil
  1463. }
  1464. s.logger.Debug("GetHomepageCategory: retrieved homepage category",
  1465. "uin", uin,
  1466. "enabled", user.ICQInfo.HomepageCategory.Enabled,
  1467. "index", user.ICQInfo.HomepageCategory.Index,
  1468. )
  1469. return &user.ICQInfo.HomepageCategory, nil
  1470. }
  1471. // SetHomepageCategory saves the user's homepage category to the database.
  1472. // This is used by the V5 META_SET_HPCAT (0x0442) command.
  1473. // From iserverd v5_set_hpcat_info() - updates user's homepage category.
  1474. //
  1475. // Parameters:
  1476. // - uin: The UIN of the user whose homepage category to update
  1477. // - hpcat: The new homepage category to save
  1478. //
  1479. // Returns nil on success, or an error if the update fails.
  1480. func (s *ICQLegacyService) SetHomepageCategory(ctx context.Context, uin uint32, hpcat state.ICQHomepageCategory) error {
  1481. screenName := state.NewIdentScreenName(strconv.FormatUint(uint64(uin), 10))
  1482. if err := s.userUpdater.SetHomepageCategory(ctx, screenName, hpcat); err != nil {
  1483. s.logger.Error("SetHomepageCategory: failed to update homepage category",
  1484. "uin", uin,
  1485. "err", err,
  1486. )
  1487. return fmt.Errorf("updating homepage category: %w", err)
  1488. }
  1489. s.logger.Debug("SetHomepageCategory: homepage category updated",
  1490. "uin", uin,
  1491. "enabled", hpcat.Enabled,
  1492. "index", hpcat.Index,
  1493. "description", hpcat.Description,
  1494. )
  1495. return nil
  1496. }
  1497. // userToSearchResult converts a state.User to a LegacyUserSearchResult,
  1498. // populating basic profile fields and checking online status.
  1499. func (s *ICQLegacyService) userToSearchResult(user state.User) *LegacyUserSearchResult {
  1500. uin, _ := strconv.Atoi(user.IdentScreenName.String())
  1501. nickname := user.ICQInfo.Basic.Nickname
  1502. // Use UIN as fallback nickname if not set
  1503. if nickname == "" {
  1504. nickname = user.IdentScreenName.String()
  1505. }
  1506. result := &LegacyUserSearchResult{
  1507. UIN: uint32(uin),
  1508. Nickname: nickname,
  1509. FirstName: user.ICQInfo.Basic.FirstName,
  1510. LastName: user.ICQInfo.Basic.LastName,
  1511. Email: user.ICQInfo.Basic.EmailAddress,
  1512. Gender: uint8(user.ICQInfo.More.Gender),
  1513. Age: uint8(user.Age(s.timeNow)),
  1514. }
  1515. // Check if user is online
  1516. session := s.sessionRetriever.RetrieveSession(user.IdentScreenName)
  1517. if session != nil {
  1518. result.Online = true
  1519. result.Status = ICQLegacyStatusOnline
  1520. }
  1521. return result
  1522. }
  1523. // mapLegacyStatusToOSCAR converts a legacy ICQ status value to the equivalent
  1524. // OSCAR status flags used by the AIM/OSCAR protocol.
  1525. func mapLegacyStatusToOSCAR(legacyStatus uint32) uint32 {
  1526. var oscarStatus uint32
  1527. // Map base status to OSCAR combined values that ICQ 2003b expects.
  1528. // Legacy V5 clients also send combined values:
  1529. // Available=0x00, FFC=0x20, Away=0x01, N/A=0x05, Occupied=0x11, DND=0x13
  1530. switch legacyStatus & 0xFF {
  1531. case 0x00: // Online
  1532. oscarStatus = wire.OServiceUserStatusAvailable // 0x00
  1533. case 0x01: // Away
  1534. oscarStatus = wire.OServiceUserStatusAway // 0x01
  1535. case 0x02: // DND (single bit)
  1536. oscarStatus = wire.OServiceUserStatusAway | wire.OServiceUserStatusDND | wire.OServiceUserStatusBusy
  1537. case 0x04: // N/A (single bit)
  1538. oscarStatus = wire.OServiceUserStatusAway | wire.OServiceUserStatusOut
  1539. case 0x05: // N/A (Away|Out combined from V5 client)
  1540. oscarStatus = wire.OServiceUserStatusAway | wire.OServiceUserStatusOut
  1541. case 0x10: // Occupied (single bit)
  1542. oscarStatus = wire.OServiceUserStatusAway | wire.OServiceUserStatusBusy
  1543. case 0x11: // Occupied (Away|Busy combined from V5 client)
  1544. oscarStatus = wire.OServiceUserStatusAway | wire.OServiceUserStatusBusy
  1545. case 0x13: // DND (Away|DND|Busy combined from V5 client)
  1546. oscarStatus = wire.OServiceUserStatusAway | wire.OServiceUserStatusDND | wire.OServiceUserStatusBusy
  1547. case 0x20: // Free for Chat
  1548. oscarStatus = wire.OServiceUserStatusChat // 0x20
  1549. default:
  1550. oscarStatus = wire.OServiceUserStatusAvailable
  1551. }
  1552. // Map flags (upper word)
  1553. if legacyStatus&ICQLegacyStatusInvisible != 0 {
  1554. oscarStatus |= wire.OServiceUserStatusInvisible
  1555. }
  1556. if legacyStatus&ICQLegacyStatusFlagWebAware != 0 {
  1557. oscarStatus |= wire.OServiceUserStatusWebAware
  1558. }
  1559. if legacyStatus&ICQLegacyStatusFlagBirthday != 0 {
  1560. oscarStatus |= wire.OServiceUserStatusBirthday
  1561. }
  1562. if legacyStatus&ICQLegacyStatusFlagDCAuth != 0 {
  1563. oscarStatus |= wire.OServiceUserStatusDirectRequireAuth
  1564. }
  1565. return oscarStatus
  1566. }
  1567. // mapOSCARStatusToLegacy converts OSCAR status flags to the equivalent
  1568. // legacy ICQ status value.
  1569. func mapOSCARStatusToLegacy(oscarStatus uint32) uint32 {
  1570. var legacyStatus uint32
  1571. // Map base status. ICQ 2003b sends combined status bits:
  1572. // Available=0x00, FFC=0x20, Away=0x01, N/A=0x05, Occupied=0x11, DND=0x13
  1573. statusByte := oscarStatus & 0xFF
  1574. switch statusByte {
  1575. case 0x00:
  1576. legacyStatus = ICQLegacyStatusOnline
  1577. case 0x01:
  1578. legacyStatus = ICQLegacyStatusAway
  1579. case 0x02:
  1580. legacyStatus = ICQLegacyStatusDND
  1581. case 0x04:
  1582. legacyStatus = ICQLegacyStatusNA
  1583. case 0x05: // Away|Out -> N/A
  1584. legacyStatus = ICQLegacyStatusNA
  1585. case 0x10:
  1586. legacyStatus = ICQLegacyStatusOccupied
  1587. case 0x11: // Away|Busy -> Occupied
  1588. legacyStatus = ICQLegacyStatusOccupied
  1589. case 0x13: // Away|DND|Busy -> DND
  1590. legacyStatus = ICQLegacyStatusDND
  1591. case 0x20:
  1592. legacyStatus = ICQLegacyStatusFFC
  1593. default:
  1594. switch {
  1595. case statusByte&0x20 != 0:
  1596. legacyStatus = ICQLegacyStatusFFC
  1597. case statusByte&0x02 != 0 && statusByte&0x10 != 0:
  1598. legacyStatus = ICQLegacyStatusDND
  1599. case statusByte&0x10 != 0:
  1600. legacyStatus = ICQLegacyStatusOccupied
  1601. case statusByte&0x04 != 0:
  1602. legacyStatus = ICQLegacyStatusNA
  1603. case statusByte&0x02 != 0:
  1604. legacyStatus = ICQLegacyStatusDND
  1605. case statusByte&0x01 != 0:
  1606. legacyStatus = ICQLegacyStatusAway
  1607. default:
  1608. legacyStatus = ICQLegacyStatusOnline
  1609. }
  1610. }
  1611. // Map flags (upper word)
  1612. if oscarStatus&wire.OServiceUserStatusInvisible != 0 {
  1613. legacyStatus |= ICQLegacyStatusInvisible
  1614. }
  1615. if oscarStatus&wire.OServiceUserStatusWebAware != 0 {
  1616. legacyStatus |= ICQLegacyStatusFlagWebAware
  1617. }
  1618. if oscarStatus&wire.OServiceUserStatusBirthday != 0 {
  1619. legacyStatus |= ICQLegacyStatusFlagBirthday
  1620. }
  1621. if oscarStatus&wire.OServiceUserStatusDirectRequireAuth != 0 {
  1622. legacyStatus |= ICQLegacyStatusFlagDCAuth
  1623. }
  1624. return legacyStatus
  1625. }