service.go 64 KB

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