v3_handler.go 70 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181
  1. package icq_legacy
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/binary"
  6. "fmt"
  7. "log/slog"
  8. "net"
  9. "github.com/mk6i/open-oscar-server/state"
  10. )
  11. // V3Handler handles ICQ V3 protocol packets
  12. // V3 packet format (from iserverd source):
  13. // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + DATA...
  14. // Total header: 12 bytes (NO checksum in V3, unlike V5!)
  15. type V3Handler struct {
  16. sessions *LegacySessionManager
  17. service LegacyService
  18. sender PacketSender
  19. packetBuilder V3PacketBuilder // Packet builder for constructing V3 protocol packets
  20. dispatcher MessageDispatcher // Central dispatcher for cross-protocol messaging
  21. logger *slog.Logger
  22. }
  23. // NewV3Handler creates a new V3 protocol handler
  24. func NewV3Handler(
  25. sessions *LegacySessionManager,
  26. service LegacyService,
  27. sender PacketSender,
  28. packetBuilder V3PacketBuilder,
  29. logger *slog.Logger,
  30. ) *V3Handler {
  31. return &V3Handler{
  32. sessions: sessions,
  33. service: service,
  34. sender: sender,
  35. packetBuilder: packetBuilder,
  36. dispatcher: nil, // Set later via SetDispatcher to avoid circular dependency
  37. logger: logger,
  38. }
  39. }
  40. // SetSender sets the packet sender (for circular dependency resolution)
  41. func (h *V3Handler) SetSender(sender PacketSender) {
  42. h.sender = sender
  43. }
  44. // SetDispatcher sets the message dispatcher for cross-protocol messaging
  45. func (h *V3Handler) SetDispatcher(dispatcher MessageDispatcher) {
  46. h.dispatcher = dispatcher
  47. }
  48. // Handle processes a V3 protocol packet
  49. func (h *V3Handler) Handle(session *LegacySession, addr *net.UDPAddr, packet []byte) error {
  50. // V3 packet format (from iserverd source):
  51. // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + DATA...
  52. // Total header: 12 bytes (NO checksum in V3!)
  53. h.logger.Debug("raw V3 packet",
  54. "hex", fmt.Sprintf("%X", packet),
  55. "len", len(packet),
  56. )
  57. if len(packet) < 12 {
  58. return fmt.Errorf("V3 packet too short: %d bytes", len(packet))
  59. }
  60. // Parse V3 header directly (iserverd: pack >> vvers >> pcomm >> seq1 >> seq2 >> uin_num)
  61. version := binary.LittleEndian.Uint16(packet[0:2])
  62. command := binary.LittleEndian.Uint16(packet[2:4])
  63. seq1 := binary.LittleEndian.Uint16(packet[4:6])
  64. seq2 := binary.LittleEndian.Uint16(packet[6:8])
  65. uin := binary.LittleEndian.Uint32(packet[8:12])
  66. // Extract data after header
  67. var data []byte
  68. if len(packet) > 12 {
  69. data = packet[12:]
  70. }
  71. h.logger.Debug("V3 packet parsed",
  72. "version", version,
  73. "command", fmt.Sprintf("0x%04X", command),
  74. "seq1", seq1,
  75. "seq2", seq2,
  76. "uin", uin,
  77. "data_len", len(data),
  78. )
  79. // Update session activity if we have one
  80. if session != nil {
  81. session.UpdateActivity()
  82. session.SeqNumClient = seq1
  83. }
  84. // If no session exists and this is not a login/registration/ack command,
  85. // send NOT_CONNECTED to force the client to reconnect.
  86. if session == nil {
  87. switch command {
  88. case ICQLegacyCmdAck, ICQLegacyCmdFirstLogin,
  89. ICQLegacyCmdGetDeps, ICQLegacyCmdLogin:
  90. // Allow these through - they don't require a session
  91. default:
  92. h.logger.Info("V3 packet from unknown session, sending NOT_CONNECTED",
  93. "command", fmt.Sprintf("0x%04X", command),
  94. "uin", uin,
  95. "addr", addr.String(),
  96. )
  97. return h.sendNotConnected(addr, seq2, uin)
  98. }
  99. }
  100. // Handle V3 commands directly (matching iserverd's handle_v3_proto)
  101. switch command {
  102. case ICQLegacyCmdFirstLogin:
  103. return h.handleFirstLogin(addr, seq1, seq2, uin)
  104. case ICQLegacyCmdLogin:
  105. return h.handleLogin(session, addr, seq1, seq2, uin, data)
  106. case ICQLegacyCmdGetDeps:
  107. return h.handleGetDeps(addr, seq1, seq2, data)
  108. case ICQLegacyCmdUserGetInfo:
  109. return h.handleGetInfo(session, seq1, seq2, uin, data)
  110. case ICQLegacyCmdAck:
  111. h.logger.Debug("received ACK", "seq1", seq1, "seq2", seq2)
  112. return nil
  113. case ICQLegacyCmdKeepAlive, ICQLegacyCmdKeepAlive2:
  114. return h.handlePing(session, addr, seq1, seq2, uin)
  115. case ICQLegacyCmdLogoff:
  116. return h.handleLogoff(session, seq1, seq2, uin)
  117. case ICQLegacyCmdContactList:
  118. return h.handleContactList(session, seq1, seq2, uin, data)
  119. case ICQLegacyCmdSetStatus:
  120. return h.handleSetStatus(session, seq1, seq2, uin, data)
  121. case ICQLegacyCmdThruServer, ICQLegacyCmdAuthorize: // 0x010E and 0x0456 (authorize)
  122. return h.handleMessage(session, seq1, seq2, uin, data)
  123. case ICQLegacyCmdUserAdd:
  124. return h.handleUserAdd(session, seq1, seq2, uin, data)
  125. case ICQLegacyCmdSysMsgReq:
  126. return h.handleOfflineMsgReq(session, seq1, seq2, uin)
  127. case ICQLegacyCmdSetBasicInfo:
  128. return h.handleSetBasicInfo(session, seq1, seq2, uin, data)
  129. case 0x0582: // SetHomeInfo
  130. return h.handleSetHomeInfo(session, seq1, seq2, uin, data)
  131. case 0x058C: // SetHomeWeb
  132. return h.handleSetHomeWeb(session, seq1, seq2, uin, data)
  133. case 0x0578: // SetWorkInfo
  134. return h.handleSetWorkInfo(session, seq1, seq2, uin, data)
  135. case 0x05BE: // SetWorkWeb
  136. return h.handleSetWorkWeb(session, seq1, seq2, uin, data)
  137. case 0x0604: // ICQ_CMDxRCV_UKNOWN_DEP - unknown deps request (historical iserverd naming)
  138. return h.handleUnknownDep(session, seq1, seq2, uin, data)
  139. case 0x06AE: // ICQ_CMDxRCV_VISxLIST - visible list
  140. return h.handleVisibleList(session, seq1, seq2, uin, data)
  141. case 0x06A4: // ICQ_CMDxRCV_INVISxLIST - invisible list
  142. return h.handleInvisibleList(session, seq1, seq2, uin, data)
  143. case ICQLegacyCmdSearchStart: // 0x05C8 - Search user
  144. return h.handleSearchStart(session, seq1, seq2, uin, data)
  145. case ICQLegacyCmdGetDeps1: // 0x05F0 - Get deps list during session (pre-auth list, historical iserverd naming)
  146. return h.handleGetDeps1(session, seq1, seq2, uin, data)
  147. case 0x05AA: // ICQ_CMDxRCV_GETxNOTES - Get notes
  148. return h.handleGetNotes(session, seq1, seq2, uin, data)
  149. case 0x0596: // ICQ_CMDxRCV_SETxNOTES - Set notes
  150. return h.handleSetNotes(session, seq1, seq2, uin, data)
  151. case ICQLegacyCmdSetPassword: // 0x049C - Set password
  152. return h.handleSetPassword(session, seq1, seq2, uin, data)
  153. case ICQLegacyCmdSetAuth: // 0x0514 - Set auth mode
  154. return h.handleSetAuth(session, seq1, seq2, uin, data)
  155. case 0x0528: // ICQ_CMDxRCV_SETxSTATE - Set client state (not search!)
  156. return h.handleSetState(session, seq1, seq2, uin, data)
  157. case 0x0532: // ICQ_CMDxRCV_USAGExSTATS - Usage statistics
  158. return h.handleUsageStats(session, seq1, seq2, uin, data)
  159. case ICQLegacyCmdGetExternals: // 0x04C4 - Get externals
  160. return h.handleGetExternals(session, seq1, seq2, uin, data)
  161. case 0x0442: // ICQ_CMDxRCV_SYSxMSGxDONExACK - System message ack
  162. return h.handleSysAck(session, seq1, seq2, uin, data)
  163. case ICQLegacyCmdReconnect: // 0x015E - Reconnect/online info
  164. return h.handleOnlineInfo(session, seq1, seq2, uin, data)
  165. case 0x0460: // ICQ_CMDxRCV_USERxGETINFO1 - Get basic info only
  166. return h.handleGetInfo1(session, seq1, seq2, uin, data)
  167. default:
  168. h.logger.Debug("unhandled V3 command",
  169. "command", fmt.Sprintf("0x%04X", command),
  170. "uin", uin,
  171. )
  172. // Send ACK for unknown commands
  173. return h.sendAck(addr, seq1, seq2, uin)
  174. }
  175. }
  176. // handleFirstLogin processes the first login packet (0x04EC)
  177. // From iserverd: v3_process_firstlog() - just sends ACK
  178. func (h *V3Handler) handleFirstLogin(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
  179. h.logger.Debug("first login packet",
  180. "uin", uin,
  181. "addr", addr.String(),
  182. )
  183. return h.sendAck(addr, seq1, seq2, uin)
  184. }
  185. // handleGetDeps processes the pre-auth pseudo-login packet (0x03F2).
  186. // Historically called "get departments list" in iserverd (from its Users_Deps database table).
  187. // Data format: JUNK(4) + UIN(4) + PWD_LEN(2) + PASSWORD
  188. // The server validates credentials and sends the pre-auth response before actual login.
  189. //
  190. // Refactored to use service layer (AuthenticateUser) and packet builder.
  191. // Following the OSCAR pattern: unmarshal -> call service -> build response
  192. func (h *V3Handler) handleGetDeps(addr *net.UDPAddr, seq1, seq2 uint16, data []byte) error {
  193. ctx := context.Background()
  194. // 1. Unmarshal packet to typed struct
  195. if len(data) < 10 {
  196. h.logger.Debug("getdeps packet too short", "len", len(data))
  197. return nil
  198. }
  199. offset := 0
  200. // Skip junk (4 bytes)
  201. offset += 4
  202. // Read UIN (4 bytes)
  203. uin := binary.LittleEndian.Uint32(data[offset : offset+4])
  204. offset += 4
  205. // Read password length (2 bytes)
  206. pwdLen := binary.LittleEndian.Uint16(data[offset : offset+2])
  207. offset += 2
  208. if pwdLen > 20 {
  209. pwdLen = 20
  210. }
  211. // Read password
  212. if offset+int(pwdLen) > len(data) {
  213. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  214. }
  215. password := string(data[offset : offset+int(pwdLen)])
  216. if len(password) > 0 && password[len(password)-1] == 0 {
  217. password = password[:len(password)-1]
  218. }
  219. h.logger.Info("V3 getdeps (pseudo-login)",
  220. "uin", uin,
  221. "password_len", len(password),
  222. )
  223. // 2. Call service layer with typed request
  224. authReq := AuthRequest{
  225. UIN: uin,
  226. Password: password,
  227. Version: ICQLegacyVersionV3,
  228. }
  229. loginOK, err := h.service.ValidateCredentials(ctx, authReq.UIN, authReq.Password)
  230. if err != nil {
  231. h.logger.Error("getdeps authentication error", "err", err, "uin", uin)
  232. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  233. }
  234. if !loginOK {
  235. h.logger.Info("getdeps failed - invalid credentials", "uin", uin)
  236. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  237. }
  238. // 3. Build and send responses using packet builder
  239. // Send ACK
  240. _ = h.sender.SendPacket(addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
  241. // Send pre-auth response using packet builder
  242. return h.sender.SendPacket(addr, h.packetBuilder.BuildDeptsList(seq2, uin))
  243. }
  244. // handleLogin processes login packet (0x03E8)
  245. // From iserverd v3_process_login():
  246. // After header (12 bytes): TIMESTAMP(4) + TCP_PORT(4) + PWD_LEN(2) + PASSWORD + ...
  247. //
  248. // Refactored to use service layer (AuthenticateUser) and packet builder.
  249. // Following the OSCAR pattern: unmarshal -> call service -> build response
  250. func (h *V3Handler) handleLogin(session *LegacySession, addr *net.UDPAddr, seq1, seq2 uint16, uin uint32, data []byte) error {
  251. ctx := context.Background()
  252. // First send ACK (iserverd does this immediately)
  253. _ = h.sender.SendPacket(addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
  254. // 1. Unmarshal packet to typed struct
  255. // V3 login data format (from iserverd):
  256. // TIMESTAMP(4) + TCP_PORT(4) + PWD_LEN(2) + PASSWORD(pwdLen) + ...
  257. if len(data) < 10 {
  258. h.logger.Debug("login packet too short", "len", len(data))
  259. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  260. }
  261. offset := 0
  262. // Read timestamp (4 bytes) - skip it
  263. // timestamp := binary.LittleEndian.Uint32(data[offset : offset+4])
  264. offset += 4
  265. // Read TCP port (4 bytes)
  266. port := binary.LittleEndian.Uint32(data[offset : offset+4])
  267. offset += 4
  268. // Read password length (2 bytes)
  269. if offset+2 > len(data) {
  270. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  271. }
  272. pwdLen := binary.LittleEndian.Uint16(data[offset : offset+2])
  273. offset += 2
  274. // Sanity check password length
  275. if pwdLen > 20 {
  276. pwdLen = 20
  277. }
  278. // Read password
  279. if offset+int(pwdLen) > len(data) {
  280. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  281. }
  282. password := string(data[offset : offset+int(pwdLen)])
  283. // Remove null terminator if present
  284. if len(password) > 0 && password[len(password)-1] == 0 {
  285. password = password[:len(password)-1]
  286. }
  287. h.logger.Info("V3 login attempt",
  288. "uin", uin,
  289. "port", port,
  290. "password_len", len(password),
  291. )
  292. // 2. Call service layer with typed request
  293. authReq := AuthRequest{
  294. UIN: uin,
  295. Password: password,
  296. Status: ICQLegacyStatusOnline, // Default status for V3
  297. TCPPort: port,
  298. Version: ICQLegacyVersionV3,
  299. }
  300. authResult, err := h.service.AuthenticateUser(ctx, authReq)
  301. if err != nil {
  302. h.logger.Error("login authentication error", "err", err, "uin", uin)
  303. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  304. }
  305. if !authResult.Success {
  306. h.logger.Info("login failed - invalid credentials",
  307. "uin", uin,
  308. "error_code", authResult.ErrorCode,
  309. )
  310. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  311. }
  312. // 3. Create session (handler responsibility - session management)
  313. // TODO: parse and store direct connection info (IP, port, DC version, DC type)
  314. // from the V3 login packet so that presence notifications can include real
  315. // connection details when ICQ_LEGACY_DIRECT_CONNECTIONS includes V3.
  316. newSession, err := h.sessions.CreateSession(uin, addr, ICQLegacyVersionV3, authResult.oscarSession)
  317. if err != nil {
  318. h.logger.Error("failed to create session", "err", err, "uin", uin)
  319. return h.sender.SendPacket(addr, h.packetBuilder.BuildBadPassword(seq1, seq2, uin))
  320. }
  321. newSession.Password = password
  322. // 4. Build and send response using packet builder
  323. loginReplyPkt := h.packetBuilder.BuildLoginReply(newSession, seq1, seq2)
  324. if err := h.sender.SendToSession(newSession, loginReplyPkt); err != nil {
  325. return err
  326. }
  327. h.logger.Info("V3 login successful",
  328. "uin", uin,
  329. "session_id", newSession.SessionID,
  330. )
  331. if err := h.service.NotifyUserOnline(ctx, newSession.UIN, newSession.GetStatus()); err != nil {
  332. h.logger.Debug("V3 failed to notify OSCAR clients of online", "uin", newSession.UIN, "err", err)
  333. }
  334. return nil
  335. }
  336. // handlePing processes keep-alive packets
  337. func (h *V3Handler) handlePing(session *LegacySession, addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
  338. if session == nil {
  339. return h.sendNotConnected(addr, seq2, uin)
  340. }
  341. return h.sendAck(addr, seq1, seq2, uin)
  342. }
  343. // handleLogoff processes logout
  344. func (h *V3Handler) handleLogoff(session *LegacySession, seq1, seq2 uint16, uin uint32) error {
  345. if session == nil {
  346. return nil
  347. }
  348. h.logger.Info("V3 logout", "uin", uin)
  349. // Notify contacts that this user is going offline BEFORE removing session
  350. h.notifyContactsUserOffline(session)
  351. // Notify OSCAR clients that this user went offline
  352. ctx := context.Background()
  353. if err := h.service.NotifyUserOffline(ctx, uin); err != nil {
  354. h.logger.Debug("V3 failed to notify OSCAR clients of offline", "uin", uin, "err", err)
  355. }
  356. h.sessions.RemoveSession(uin)
  357. return nil
  358. }
  359. // notifyContactsUserOffline notifies all contacts who have this user in their list that we're offline
  360. func (h *V3Handler) notifyContactsUserOffline(session *LegacySession) {
  361. if h.dispatcher == nil {
  362. return
  363. }
  364. // Find all sessions that have this user in their contact list
  365. contactsToNotify := h.sessions.NotifyContactsOfStatus(session)
  366. h.logger.Debug("notifying contacts of user offline",
  367. "uin", session.UIN,
  368. "contacts_to_notify", contactsToNotify,
  369. )
  370. for _, contactUIN := range contactsToNotify {
  371. contactSession := h.sessions.GetSession(contactUIN)
  372. if contactSession != nil {
  373. // Use dispatcher to send offline notification in correct protocol format
  374. _ = h.dispatcher.SendUserOffline(contactSession, session.UIN)
  375. }
  376. }
  377. }
  378. // handleContactList processes contact list (0x0406)
  379. // From iserverd v3_process_contact_list(): processes contact list and sends online notifications
  380. //
  381. // Refactored to use service layer (ProcessContactList) and packet builder.
  382. // Following the OSCAR pattern: unmarshal -> call service -> send notifications
  383. func (h *V3Handler) handleContactList(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  384. if session == nil {
  385. return nil
  386. }
  387. // 1. Send ACK using packet builder
  388. _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
  389. // 2. Unmarshal packet to typed request struct
  390. req, err := h.parseContactListPacket(data, uin)
  391. if err != nil {
  392. h.logger.Debug("V3 contact list parse error", "uin", uin, "err", err)
  393. // Send contact list done even on parse error
  394. return h.sender.SendToSession(session, h.packetBuilder.BuildContactListDone(session.NextServerSeqNum(), seq2, session.UIN))
  395. }
  396. h.logger.Debug("V3 contact list",
  397. "uin", uin,
  398. "count", len(req.Contacts),
  399. )
  400. // Store contact list in session (handler responsibility - session management)
  401. session.SetContactList(req.Contacts)
  402. // 3. Call service layer with typed request
  403. ctx := context.Background()
  404. result, err := h.service.ProcessContactList(ctx, session.Instance, req)
  405. if err != nil {
  406. h.logger.Error("contact list processing failed", "uin", uin, "err", err)
  407. // Still send contact list done on error
  408. return h.sender.SendToSession(session, h.packetBuilder.BuildContactListDone(session.NextServerSeqNum(), seq2, session.UIN))
  409. }
  410. // 4. Send online notifications based on service result
  411. for _, contact := range result.OnlineContacts {
  412. if contact.Online {
  413. if h.dispatcher != nil {
  414. // Use central dispatcher - routes to correct protocol based on session's version
  415. _ = h.dispatcher.SendUserOnline(session, contact.UIN, contact.Status)
  416. } else {
  417. // Fallback to V3 format if dispatcher not set
  418. _ = h.sendUserOnline(session, contact.UIN, contact.Status)
  419. }
  420. }
  421. }
  422. h.notifyContactsUserOnline(session)
  423. // 5. Send contact list done using packet builder
  424. return h.sender.SendToSession(session, h.packetBuilder.BuildContactListDone(session.NextServerSeqNum(), seq2, session.UIN))
  425. }
  426. // parseContactListPacket parses a V3 contact list packet into a typed ContactListRequest struct.
  427. // Format: TIMESTAMP(4) + COUNT(1) + UIN(4)*COUNT
  428. func (h *V3Handler) parseContactListPacket(data []byte, ownerUIN uint32) (ContactListRequest, error) {
  429. req := ContactListRequest{
  430. UIN: ownerUIN,
  431. Contacts: make([]uint32, 0),
  432. }
  433. if len(data) < 5 {
  434. return req, fmt.Errorf("contact list packet too short: %d bytes", len(data))
  435. }
  436. // Skip timestamp (4 bytes)
  437. offset := 4
  438. // Contact count (1 byte)
  439. count := int(data[offset])
  440. offset++
  441. // Parse contact UINs
  442. for i := 0; i < count && offset+4 <= len(data); i++ {
  443. contactUIN := binary.LittleEndian.Uint32(data[offset : offset+4])
  444. req.Contacts = append(req.Contacts, contactUIN)
  445. offset += 4
  446. }
  447. return req, nil
  448. }
  449. // notifyContactsUserOnline notifies all contacts who have this user in their list that we're online
  450. // Following iserverd's pattern: when user comes online, notify everyone who cares
  451. func (h *V3Handler) notifyContactsUserOnline(session *LegacySession) {
  452. if h.dispatcher == nil {
  453. return
  454. }
  455. // Find all sessions that have this user in their contact list
  456. contactsToNotify := h.sessions.NotifyContactsOfStatus(session)
  457. h.logger.Debug("notifying contacts of user online",
  458. "uin", session.UIN,
  459. "contacts_to_notify", contactsToNotify,
  460. )
  461. for _, contactUIN := range contactsToNotify {
  462. contactSession := h.sessions.GetSession(contactUIN)
  463. if contactSession != nil {
  464. // Use dispatcher to send in correct protocol format for each contact
  465. _ = h.dispatcher.SendUserOnline(contactSession, session.UIN, session.GetStatus())
  466. }
  467. }
  468. }
  469. // handleSetStatus processes status change (0x04D8)
  470. // From iserverd v3_process_status(): when a user changes their status
  471. // (e.g., Online -> Away, Away -> DND), this handler updates the session
  472. // and notifies all contacts who have this user in their contact list.
  473. //
  474. // Refactored to use service layer (ProcessStatusChange) and packet builder.
  475. // Following the OSCAR pattern: unmarshal -> call service -> broadcast
  476. func (h *V3Handler) handleSetStatus(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  477. if session == nil {
  478. return nil
  479. }
  480. ctx := context.Background()
  481. // 1. Send ACK using packet builder
  482. _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
  483. // 2. Unmarshal packet to typed request struct
  484. // Parse status - format: TIMESTAMP(4) + STATUS(4)
  485. if len(data) < 8 {
  486. return nil
  487. }
  488. newStatus := binary.LittleEndian.Uint32(data[4:8])
  489. oldStatus := session.GetStatus()
  490. h.logger.Debug("V3 status change",
  491. "uin", uin,
  492. "old_status", fmt.Sprintf("0x%08X", oldStatus),
  493. "new_status", fmt.Sprintf("0x%08X", newStatus),
  494. )
  495. // Update session status (handler responsibility - session management)
  496. session.SetStatus(newStatus)
  497. // 3. Call service layer with typed request
  498. statusReq := StatusChangeRequest{
  499. UIN: uin,
  500. NewStatus: newStatus,
  501. OldStatus: oldStatus,
  502. }
  503. statusResult, err := h.service.ProcessStatusChange(ctx, statusReq)
  504. if err != nil {
  505. h.logger.Debug("failed to process status change", "err", err)
  506. return nil
  507. }
  508. h.logger.Debug("V3 status change processed",
  509. "uin", uin,
  510. "notify_count", len(statusResult.NotifyTargets),
  511. )
  512. // 4. Broadcast status change to contacts using packet builder
  513. // The service layer returns the list of users to notify
  514. for _, target := range statusResult.NotifyTargets {
  515. targetSession := h.sessions.GetSession(target.UIN)
  516. if targetSession != nil {
  517. if h.dispatcher != nil {
  518. // Use central dispatcher - routes to correct protocol based on target's version
  519. _ = h.dispatcher.SendStatusChange(targetSession, uin, newStatus)
  520. } else {
  521. // Fallback to V3 format if dispatcher not set
  522. statusPkt := h.packetBuilder.BuildUserStatus(
  523. targetSession.NextServerSeqNum(),
  524. uin,
  525. newStatus,
  526. )
  527. _ = h.sender.SendToSession(targetSession, statusPkt)
  528. }
  529. }
  530. }
  531. return nil
  532. }
  533. // handleMessage processes through-server messages (0x010E)
  534. // From iserverd v3_process_sysmsg(): forwards messages to target user
  535. // Format: TIMESTAMP(4) + TO_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
  536. //
  537. // Refactored to use service layer (ProcessMessage) and packet builder.
  538. // Following the OSCAR pattern: unmarshal -> call service -> route
  539. func (h *V3Handler) handleMessage(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  540. if session == nil {
  541. return nil
  542. }
  543. // 1. Send ACK using packet builder
  544. _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
  545. // 2. Unmarshal packet to typed request struct
  546. req, err := h.parseMessagePacket(data, uin)
  547. if err != nil {
  548. h.logger.Debug("V3 message parse error", "from", uin, "err", err)
  549. return nil
  550. }
  551. h.logger.Info("V3 message received",
  552. "from", req.FromUIN,
  553. "to", req.ToUIN,
  554. "type", fmt.Sprintf("0x%04X", req.MsgType),
  555. "msg_len", len(req.Message),
  556. )
  557. // 3. Call service layer with typed request
  558. ctx := context.Background()
  559. result, err := h.service.ProcessMessage(ctx, session, req)
  560. if err != nil {
  561. h.logger.Error("message processing failed",
  562. "from", req.FromUIN,
  563. "to", req.ToUIN,
  564. "err", err,
  565. )
  566. return nil
  567. }
  568. // 4. Route message based on service result
  569. if result.TargetOnline {
  570. // Target is online - forward message via dispatcher or direct send
  571. targetSession := h.sessions.GetSession(req.ToUIN)
  572. if targetSession != nil {
  573. if h.dispatcher != nil {
  574. // Use central dispatcher - routes to correct protocol based on target's version
  575. _ = h.dispatcher.SendOnlineMessage(targetSession, req.FromUIN, req.MsgType, req.Message)
  576. } else {
  577. // Fallback to V3 format if dispatcher not set
  578. _ = h.sendOnlineMessage(targetSession, req.FromUIN, req.MsgType, req.Message, seq2)
  579. }
  580. h.logger.Debug("V3 message forwarded",
  581. "from", req.FromUIN,
  582. "to", req.ToUIN,
  583. "target_version", targetSession.Version,
  584. )
  585. }
  586. } else if result.StoredOffline {
  587. // Message was stored for offline delivery by the service layer
  588. h.logger.Info("V3 message stored for offline delivery",
  589. "from", req.FromUIN,
  590. "to", req.ToUIN,
  591. "type", fmt.Sprintf("0x%04X", req.MsgType),
  592. )
  593. }
  594. return nil
  595. }
  596. // parseMessagePacket parses a V3 message packet into a typed MessageRequest struct.
  597. // Format: TIMESTAMP(4) + TO_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
  598. func (h *V3Handler) parseMessagePacket(data []byte, fromUIN uint32) (MessageRequest, error) {
  599. req := MessageRequest{
  600. FromUIN: fromUIN,
  601. }
  602. if len(data) < 12 {
  603. return req, fmt.Errorf("message packet too short: %d bytes", len(data))
  604. }
  605. offset := 0
  606. // Skip timestamp (4 bytes)
  607. offset += 4
  608. // Target UIN (4 bytes)
  609. req.ToUIN = binary.LittleEndian.Uint32(data[offset : offset+4])
  610. offset += 4
  611. // Message type (2 bytes)
  612. req.MsgType = binary.LittleEndian.Uint16(data[offset : offset+2])
  613. offset += 2
  614. // Message length (2 bytes)
  615. msgLen := binary.LittleEndian.Uint16(data[offset : offset+2])
  616. offset += 2
  617. // Message content
  618. if msgLen > 0 && offset+int(msgLen) <= len(data) {
  619. req.Message = string(data[offset : offset+int(msgLen)])
  620. // Remove null terminator if present
  621. if len(req.Message) > 0 && req.Message[len(req.Message)-1] == 0 {
  622. req.Message = req.Message[:len(req.Message)-1]
  623. }
  624. }
  625. return req, nil
  626. }
  627. // handleUserAdd processes user add to contact list (0x053C)
  628. // From iserverd v3_process_useradd(): adds user and sends "you were added" notification
  629. //
  630. // Refactored to use service layer (ProcessUserAdd) and packet builder.
  631. // Following the OSCAR pattern: unmarshal -> call service -> send notifications
  632. func (h *V3Handler) handleUserAdd(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  633. if session == nil {
  634. return nil
  635. }
  636. // 1. Send ACK using packet builder
  637. _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
  638. // 2. Unmarshal packet to typed request struct
  639. req, err := h.parseUserAddPacket(data, uin)
  640. if err != nil {
  641. h.logger.Debug("V3 user add parse error", "uin", uin, "err", err)
  642. return nil
  643. }
  644. h.logger.Debug("V3 user add",
  645. "from", req.FromUIN,
  646. "target", req.TargetUIN,
  647. )
  648. // Add to contact list (handler responsibility - session management)
  649. contacts := session.GetContactList()
  650. contacts = append(contacts, req.TargetUIN)
  651. session.SetContactList(contacts)
  652. // 3. Call service layer with typed request
  653. ctx := context.Background()
  654. result, err := h.service.ProcessUserAdd(ctx, session.Instance, req)
  655. if err != nil {
  656. h.logger.Error("user add processing failed", "from", req.FromUIN, "target", req.TargetUIN, "err", err)
  657. return nil
  658. }
  659. // 4. Send notifications based on service result
  660. if result.TargetOnline {
  661. // Send target's online status to the user who added them
  662. _ = h.sendUserOnline(session, req.TargetUIN, result.TargetStatus)
  663. // Send "you were added" notification to target user if service says to
  664. if result.SendYouWereAdded {
  665. targetSession := h.sessions.GetSession(req.TargetUIN)
  666. if targetSession != nil {
  667. // Message format: nick#FE#first#FE#last#FE#email#FE#auth
  668. // Using UIN as nick for now since we don't have user info
  669. youWereAddedMsg := fmt.Sprintf("%d\xFE\xFE\xFE\xFE0", req.FromUIN)
  670. if h.dispatcher != nil {
  671. // Use dispatcher for cross-protocol support
  672. _ = h.dispatcher.SendOnlineMessage(targetSession, req.FromUIN, ICQLegacyMsgAdded, youWereAddedMsg)
  673. } else {
  674. // Fallback to V3 format
  675. _ = h.sendOnlineMessage(targetSession, req.FromUIN, ICQLegacyMsgAdded, youWereAddedMsg, 0)
  676. }
  677. // Also send the adder's online status to the target.
  678. // The target receives "you were added" but won't see the
  679. // adder as online unless we explicitly tell them.
  680. if h.dispatcher != nil {
  681. _ = h.dispatcher.SendUserOnline(targetSession, req.FromUIN, session.GetStatus())
  682. } else {
  683. _ = h.sendUserOnline(targetSession, req.FromUIN, session.GetStatus())
  684. }
  685. h.logger.Debug("V3 sent 'you were added' notification",
  686. "from", req.FromUIN,
  687. "to", req.TargetUIN,
  688. )
  689. }
  690. }
  691. }
  692. return nil
  693. }
  694. // parseUserAddPacket parses a V3 user add packet into a typed UserAddRequest struct.
  695. // Format: TIMESTAMP(4) + TARGET_UIN(4)
  696. func (h *V3Handler) parseUserAddPacket(data []byte, fromUIN uint32) (UserAddRequest, error) {
  697. req := UserAddRequest{
  698. FromUIN: fromUIN,
  699. }
  700. if len(data) < 8 {
  701. return req, fmt.Errorf("user add packet too short: %d bytes", len(data))
  702. }
  703. // Skip timestamp (4 bytes)
  704. // Target UIN (4 bytes)
  705. req.TargetUIN = binary.LittleEndian.Uint32(data[4:8])
  706. return req, nil
  707. }
  708. // handleGetInfo processes user info request (0x05FA)
  709. // From iserverd v3_process_getinfo(): sends basic, home, home_web, work, work_web info
  710. //
  711. // Refactored to use service layer (GetUserInfoForProtocol) and packet builder.
  712. // Following the OSCAR pattern: unmarshal -> call service -> build response
  713. func (h *V3Handler) handleGetInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  714. if session == nil {
  715. return nil
  716. }
  717. // 1. Send ACK using packet builder
  718. _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
  719. // 2. Unmarshal packet to extract target UIN
  720. // Format: TIMESTAMP(4) + TARGET_UIN(4)
  721. if len(data) < 8 {
  722. return nil
  723. }
  724. targetUIN := binary.LittleEndian.Uint32(data[4:8])
  725. h.logger.Debug("V3 get info request",
  726. "from", uin,
  727. "target", targetUIN,
  728. )
  729. // 3. Call service layer to get user info
  730. ctx := context.Background()
  731. info, err := h.service.GetUserInfoForProtocol(ctx, targetUIN)
  732. if err != nil {
  733. h.logger.Error("failed to get user info", "target", targetUIN, "err", err)
  734. // Still send minimal info packets even on error
  735. info = &UserInfoResult{
  736. UIN: targetUIN,
  737. Nickname: fmt.Sprintf("%d", targetUIN),
  738. }
  739. }
  740. // 4. Build and send all user info packets using packet builder
  741. // (matching iserverd v3_process_getinfo which sends 5 packets)
  742. _ = h.sender.SendToSession(session, h.packetBuilder.BuildBasicInfo(session.NextServerSeqNum(), seq2, session.UIN, info))
  743. _ = h.sender.SendToSession(session, h.packetBuilder.BuildHomeInfo(session.NextServerSeqNum(), seq2, session.UIN, info))
  744. _ = h.sender.SendToSession(session, h.packetBuilder.BuildHomeWeb(session.NextServerSeqNum(), seq2, session.UIN, info))
  745. _ = h.sender.SendToSession(session, h.packetBuilder.BuildWorkInfo(session.NextServerSeqNum(), seq2, session.UIN, info))
  746. _ = h.sender.SendToSession(session, h.packetBuilder.BuildWorkWeb(session.NextServerSeqNum(), seq2, session.UIN, info))
  747. return nil
  748. }
  749. // V3 packet sending helpers
  750. // sendAck sends ACK packet (V3 format from iserverd v3_send_ack)
  751. func (h *V3Handler) sendAck(addr *net.UDPAddr, seq1, seq2 uint16, uin uint32) error {
  752. // V3 ACK format: VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + RESERVED(4)
  753. pkt := make([]byte, 16)
  754. binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
  755. binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvAck)
  756. binary.LittleEndian.PutUint16(pkt[4:6], seq1)
  757. binary.LittleEndian.PutUint16(pkt[6:8], seq2)
  758. binary.LittleEndian.PutUint32(pkt[8:12], uin)
  759. binary.LittleEndian.PutUint32(pkt[12:16], 0) // reserved
  760. return h.sender.SendPacket(addr, pkt)
  761. }
  762. // sendNotConnected sends not connected error
  763. func (h *V3Handler) sendNotConnected(addr *net.UDPAddr, seq2 uint16, uin uint32) error {
  764. pkt := make([]byte, 16)
  765. binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
  766. binary.LittleEndian.PutUint16(pkt[2:4], ICQLegacySrvNotConnected)
  767. binary.LittleEndian.PutUint16(pkt[4:6], 0)
  768. binary.LittleEndian.PutUint16(pkt[6:8], seq2)
  769. binary.LittleEndian.PutUint32(pkt[8:12], uin)
  770. binary.LittleEndian.PutUint32(pkt[12:16], 0)
  771. return h.sender.SendPacket(addr, pkt)
  772. }
  773. // sendUserOnline sends user online notification
  774. // From iserverd v3_send_user_online()
  775. func (h *V3Handler) sendUserOnline(session *LegacySession, uin uint32, status uint32) error {
  776. // V3 USER_ONLINE format from iserverd:
  777. // header(16) + UIN(4) + IP(4) + PORT(4) + INT_IP(4) + DC_TYPE(1) + STATUS(2) + ESTAT(2) + DCVER(2) + UNKNOWN(2)
  778. // Total: 16 + 4 + 4 + 4 + 4 + 1 + 2 + 2 + 2 + 2 = 41 bytes
  779. pkt := make([]byte, 41)
  780. offset := 0
  781. // Header
  782. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  783. offset += 2
  784. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserOnline)
  785. offset += 2
  786. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  787. offset += 2
  788. binary.LittleEndian.PutUint16(pkt[offset:], 0) // seq2
  789. offset += 2
  790. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  791. offset += 4
  792. binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
  793. offset += 4
  794. // UIN of the user who is online
  795. binary.LittleEndian.PutUint32(pkt[offset:], uin)
  796. offset += 4
  797. // IP address (0 for privacy - V3 clients crash on non-V3 client connect)
  798. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  799. offset += 4
  800. // TCP port (0 for privacy)
  801. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  802. offset += 4
  803. // Internal IP (0 for privacy)
  804. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  805. offset += 4
  806. // DC type (0)
  807. pkt[offset] = 0
  808. offset++
  809. // Status (low word)
  810. binary.LittleEndian.PutUint16(pkt[offset:], uint16(status&0xFFFF))
  811. offset += 2
  812. // Extended status (high word)
  813. binary.LittleEndian.PutUint16(pkt[offset:], uint16(status>>16))
  814. offset += 2
  815. // DC version
  816. binary.LittleEndian.PutUint16(pkt[offset:], 0)
  817. offset += 2
  818. // Unknown
  819. binary.LittleEndian.PutUint16(pkt[offset:], 0)
  820. offset += 2
  821. h.logger.Debug("sending V3 user online notification",
  822. "to", session.UIN,
  823. "online_uin", uin,
  824. "status", fmt.Sprintf("0x%08X", status),
  825. )
  826. return h.sender.SendToSession(session, pkt[:offset])
  827. }
  828. // handleOfflineMsgReq processes offline message request (0x044C)
  829. // From iserverd v3_process_sysmsg_req()
  830. func (h *V3Handler) handleOfflineMsgReq(session *LegacySession, seq1, seq2 uint16, uin uint32) error {
  831. if session == nil {
  832. return nil
  833. }
  834. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  835. ctx := context.Background()
  836. // Fetch offline messages from database
  837. messages, err := h.service.GetOfflineMessages(ctx, uin)
  838. if err != nil {
  839. h.logger.Error("failed to get offline messages", "uin", uin, "err", err)
  840. return h.sendOfflineMsgDone(session, seq2)
  841. }
  842. h.logger.Debug("V3 offline message request",
  843. "uin", uin,
  844. "message_count", len(messages),
  845. )
  846. // Send each offline message
  847. for _, msg := range messages {
  848. if err := h.sendOfflineMessage(session, seq2, &msg); err != nil {
  849. h.logger.Error("failed to send offline message",
  850. "uin", uin,
  851. "from", msg.FromUIN,
  852. "err", err,
  853. )
  854. }
  855. }
  856. // Send end of offline messages
  857. return h.sendOfflineMsgDone(session, seq2)
  858. }
  859. // handleSetBasicInfo processes set basic info (0x050A)
  860. // Format: TIMESTAMP(4) + NICK_LEN(2)+NICK + FIRST_LEN(2)+FIRST + LAST_LEN(2)+LAST + EMAIL_LEN(2)+EMAIL
  861. func (h *V3Handler) handleSetBasicInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  862. if session == nil {
  863. return nil
  864. }
  865. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  866. if len(data) < 6 {
  867. return h.sendReplyOK(session, seq2, 0x01E0)
  868. }
  869. r := bytes.NewReader(data[4:]) // skip timestamp
  870. nick, _ := ParseLegacyString(r, true)
  871. first, _ := ParseLegacyString(r, true)
  872. last, _ := ParseLegacyString(r, true)
  873. email, _ := ParseLegacyString(r, true)
  874. ctx := context.Background()
  875. info := state.ICQBasicInfo{
  876. Nickname: nick,
  877. FirstName: first,
  878. LastName: last,
  879. EmailAddress: email,
  880. }
  881. if err := h.service.UpdateBasicInfo(ctx, uin, info); err != nil {
  882. h.logger.Error("V3 set basic info failed", "uin", uin, "err", err)
  883. }
  884. return h.sendReplyOK(session, seq2, 0x01E0)
  885. }
  886. // handleSetHomeInfo processes set home info (0x0582)
  887. // Format: TIMESTAMP(4) + ADDR_LEN(2)+ADDR + CITY_LEN(2)+CITY + STATE_LEN(2)+STATE + COUNTRY(2) + PHONE_LEN(2)+PHONE + FAX_LEN(2)+FAX + CELL_LEN(2)+CELL + ZIP(4) + GMT(2) + AUTH(1) + WEBAWARE(1) + HIDEIP(1) + PUBLISH_EMAIL(1)
  888. func (h *V3Handler) handleSetHomeInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  889. if session == nil {
  890. return nil
  891. }
  892. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  893. if len(data) < 6 {
  894. return h.sendReplyOK(session, seq2, 0x0280)
  895. }
  896. r := bytes.NewReader(data[4:]) // skip timestamp
  897. address, _ := ParseLegacyString(r, true)
  898. city, _ := ParseLegacyString(r, true)
  899. st, _ := ParseLegacyString(r, true)
  900. var country uint16
  901. _ = binary.Read(r, binary.LittleEndian, &country)
  902. phone, _ := ParseLegacyString(r, true)
  903. fax, _ := ParseLegacyString(r, true)
  904. cell, _ := ParseLegacyString(r, true)
  905. var zipCode uint32
  906. _ = binary.Read(r, binary.LittleEndian, &zipCode)
  907. var gmt uint16
  908. _ = binary.Read(r, binary.LittleEndian, &gmt)
  909. var auth, webaware uint8
  910. _ = binary.Read(r, binary.LittleEndian, &auth)
  911. _ = binary.Read(r, binary.LittleEndian, &webaware)
  912. // Read existing basic info to avoid overwriting nick/first/last/email
  913. ctx := context.Background()
  914. existing, err := h.service.GetFullUserInfo(ctx, uin)
  915. if err == nil && existing != nil {
  916. existing.ICQInfo.Basic.Address = address
  917. existing.ICQInfo.Basic.City = city
  918. existing.ICQInfo.Basic.State = st
  919. existing.ICQInfo.Basic.CountryCode = country
  920. existing.ICQInfo.Basic.Phone = phone
  921. existing.ICQInfo.Basic.Fax = fax
  922. existing.ICQInfo.Basic.CellPhone = cell
  923. existing.ICQInfo.Basic.ZIPCode = fmt.Sprintf("%d", zipCode)
  924. existing.ICQInfo.Basic.GMTOffset = uint8(gmt)
  925. if err := h.service.UpdateBasicInfo(ctx, uin, existing.ICQInfo.Basic); err != nil {
  926. h.logger.Error("V3 set home info failed", "uin", uin, "err", err)
  927. }
  928. }
  929. if err := h.service.SetAuthMode(ctx, uin, auth == 0); err != nil {
  930. h.logger.Error("V3 set home auth failed", "uin", uin, "err", err)
  931. }
  932. return h.sendReplyOK(session, seq2, 0x0280)
  933. }
  934. // handleSetHomeWeb processes set home web (0x058C)
  935. // Format: TIMESTAMP(4) + AGE(2) + SEX(1) + HP_LEN(2)+HP + BIRTH_YEAR(2) + BIRTH_MONTH(1) + BIRTH_DAY(1) + LANG1(1) + LANG2(1) + LANG3(1)
  936. func (h *V3Handler) handleSetHomeWeb(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  937. if session == nil {
  938. return nil
  939. }
  940. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  941. if len(data) < 7 {
  942. return h.sendReplyOK(session, seq2, 0x0294)
  943. }
  944. r := bytes.NewReader(data[4:]) // skip timestamp
  945. var age uint16
  946. _ = binary.Read(r, binary.LittleEndian, &age)
  947. var sex uint8
  948. _ = binary.Read(r, binary.LittleEndian, &sex)
  949. hp, _ := ParseLegacyString(r, true)
  950. var birthYear uint16
  951. _ = binary.Read(r, binary.LittleEndian, &birthYear)
  952. var birthMonth, birthDay, lang1, lang2, lang3 uint8
  953. _ = binary.Read(r, binary.LittleEndian, &birthMonth)
  954. _ = binary.Read(r, binary.LittleEndian, &birthDay)
  955. _ = binary.Read(r, binary.LittleEndian, &lang1)
  956. _ = binary.Read(r, binary.LittleEndian, &lang2)
  957. _ = binary.Read(r, binary.LittleEndian, &lang3)
  958. ctx := context.Background()
  959. info := state.ICQMoreInfo{
  960. Gender: uint16(sex),
  961. HomePageAddr: hp,
  962. BirthYear: birthYear,
  963. BirthMonth: birthMonth,
  964. BirthDay: birthDay,
  965. Lang1: lang1,
  966. Lang2: lang2,
  967. Lang3: lang3,
  968. }
  969. if err := h.service.UpdateMoreInfo(ctx, uin, info); err != nil {
  970. h.logger.Error("V3 set home web failed", "uin", uin, "err", err)
  971. }
  972. return h.sendReplyOK(session, seq2, 0x0294)
  973. }
  974. // handleSetWorkInfo processes set work info (0x0578)
  975. // Format: TIMESTAMP(4) + ADDR_LEN(2)+ADDR + CITY_LEN(2)+CITY + STATE_LEN(2)+STATE + COUNTRY(2) + COMPANY_LEN(2)+COMPANY + TITLE_LEN(2)+TITLE + DEPT(2) + PHONE_LEN(2)+PHONE + FAX_LEN(2)+FAX + PAGER_LEN(2)+PAGER + ZIP(4)
  976. func (h *V3Handler) handleSetWorkInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  977. if session == nil {
  978. return nil
  979. }
  980. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  981. if len(data) < 6 {
  982. return h.sendReplyOK(session, seq2, 0x026C)
  983. }
  984. r := bytes.NewReader(data[4:]) // skip timestamp
  985. address, _ := ParseLegacyString(r, true)
  986. city, _ := ParseLegacyString(r, true)
  987. st, _ := ParseLegacyString(r, true)
  988. var country uint16
  989. _ = binary.Read(r, binary.LittleEndian, &country)
  990. company, _ := ParseLegacyString(r, true)
  991. position, _ := ParseLegacyString(r, true)
  992. var dept uint16
  993. _ = binary.Read(r, binary.LittleEndian, &dept)
  994. phone, _ := ParseLegacyString(r, true)
  995. fax, _ := ParseLegacyString(r, true)
  996. // pager field — not stored in ICQWorkInfo, skip
  997. _, _ = ParseLegacyString(r, true)
  998. var zipCode uint32
  999. _ = binary.Read(r, binary.LittleEndian, &zipCode)
  1000. // Read existing work info to preserve WebPage (set separately via 0x05BE)
  1001. ctx := context.Background()
  1002. existing, existErr := h.service.GetFullUserInfo(ctx, uin)
  1003. if existErr == nil && existing != nil {
  1004. existing.ICQInfo.Work.Address = address
  1005. existing.ICQInfo.Work.City = city
  1006. existing.ICQInfo.Work.State = st
  1007. existing.ICQInfo.Work.CountryCode = country
  1008. existing.ICQInfo.Work.Company = company
  1009. existing.ICQInfo.Work.Position = position
  1010. existing.ICQInfo.Work.Phone = phone
  1011. existing.ICQInfo.Work.Fax = fax
  1012. existing.ICQInfo.Work.ZIPCode = fmt.Sprintf("%d", zipCode)
  1013. if err := h.service.UpdateWorkInfo(ctx, uin, existing.ICQInfo.Work); err != nil {
  1014. h.logger.Error("V3 set work info failed", "uin", uin, "err", err)
  1015. }
  1016. }
  1017. return h.sendReplyOK(session, seq2, 0x026C)
  1018. }
  1019. // handleSetWorkWeb processes set work web (0x05BE)
  1020. // Format: TIMESTAMP(4) + WEB_LEN(2)+WEB
  1021. func (h *V3Handler) handleSetWorkWeb(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1022. if session == nil {
  1023. return nil
  1024. }
  1025. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1026. if len(data) < 6 {
  1027. return h.sendReplyOK(session, seq2, 0x0258)
  1028. }
  1029. r := bytes.NewReader(data[4:]) // skip timestamp
  1030. web, _ := ParseLegacyString(r, true)
  1031. // Read existing work info to avoid overwriting other fields
  1032. ctx := context.Background()
  1033. existing, err := h.service.GetFullUserInfo(ctx, uin)
  1034. if err == nil && existing != nil {
  1035. existing.ICQInfo.Work.WebPage = web
  1036. if err := h.service.UpdateWorkInfo(ctx, uin, existing.ICQInfo.Work); err != nil {
  1037. h.logger.Error("V3 set work web failed", "uin", uin, "err", err)
  1038. }
  1039. }
  1040. return h.sendReplyOK(session, seq2, 0x0258)
  1041. }
  1042. // handleVisibleList processes visible list (0x06AE)
  1043. func (h *V3Handler) handleVisibleList(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1044. if session == nil {
  1045. return nil
  1046. }
  1047. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1048. h.logger.Debug("V3 visible list", "uin", uin)
  1049. return nil
  1050. }
  1051. // handleInvisibleList processes invisible list (0x06A4)
  1052. func (h *V3Handler) handleInvisibleList(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1053. if session == nil {
  1054. return nil
  1055. }
  1056. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1057. h.logger.Debug("V3 invisible list", "uin", uin)
  1058. return nil
  1059. }
  1060. // handleUnknownDep processes unknown pre-auth related request (0x0604)
  1061. func (h *V3Handler) handleUnknownDep(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1062. if session == nil {
  1063. return nil
  1064. }
  1065. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1066. h.logger.Debug("V3 unknown dep request", "uin", uin)
  1067. return nil
  1068. }
  1069. // handleSearchStart processes search user request (0x05C8)
  1070. // From V3 protocol: TIMESTAMP(4) + TYPE(2) + COMP(2) + LEN(2) + STRING
  1071. // Type: 0x00FF=UIN, 0x01FF=Nick, 0x02FF=First, 0x03FF=Last, etc.
  1072. // Comp: 0x0000=contains, 0x0001=doesn't contain, 0x0002=is, etc.
  1073. func (h *V3Handler) handleSearchStart(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1074. ctx := context.Background()
  1075. if session == nil {
  1076. return nil
  1077. }
  1078. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1079. // Parse search parameters - format: TIMESTAMP(4) + TYPE(2) + COMP(2) + LEN(2) + STRING
  1080. if len(data) < 10 {
  1081. h.logger.Debug("V3 search start - packet too short", "uin", uin, "len", len(data))
  1082. return h.sendSearchDone(session, seq2)
  1083. }
  1084. offset := 4 // Skip timestamp
  1085. searchType := binary.LittleEndian.Uint16(data[offset : offset+2])
  1086. offset += 2
  1087. compType := binary.LittleEndian.Uint16(data[offset : offset+2])
  1088. offset += 2
  1089. strLen := binary.LittleEndian.Uint16(data[offset : offset+2])
  1090. offset += 2
  1091. var searchStr string
  1092. if len(data) >= offset+int(strLen) && strLen > 0 {
  1093. searchStr = string(data[offset : offset+int(strLen)])
  1094. // Remove null terminator if present
  1095. if len(searchStr) > 0 && searchStr[len(searchStr)-1] == 0 {
  1096. searchStr = searchStr[:len(searchStr)-1]
  1097. }
  1098. }
  1099. h.logger.Debug("V3 search start",
  1100. "uin", uin,
  1101. "search_type", fmt.Sprintf("0x%04X", searchType),
  1102. "comp_type", compType,
  1103. "search_str", searchStr,
  1104. )
  1105. // Handle UIN search (type 0x00FF)
  1106. if searchType == 0x00FF && searchStr != "" {
  1107. // Try to parse as UIN
  1108. var targetUIN uint64
  1109. _, err := fmt.Sscanf(searchStr, "%d", &targetUIN)
  1110. if err == nil && targetUIN > 0 {
  1111. // Search in database first (includes both online and offline users)
  1112. result, err := h.service.SearchByUIN(ctx, uint32(targetUIN))
  1113. if err == nil && result != nil {
  1114. // User found in database - truncate fields for legacy client safety
  1115. nick := truncateField(result.Nickname, 20, h.logger, "nickname", result.UIN)
  1116. first := truncateField(result.FirstName, 64, h.logger, "first_name", result.UIN)
  1117. last := truncateField(result.LastName, 64, h.logger, "last_name", result.UIN)
  1118. email := truncateField(result.Email, 64, h.logger, "email", result.UIN)
  1119. _ = h.sendSearchFound(session, seq2, result.UIN, nick, first, last, email, 0)
  1120. h.logger.Debug("V3 search found user in database",
  1121. "searcher", uin,
  1122. "found", result.UIN,
  1123. "nick", result.Nickname,
  1124. )
  1125. } else {
  1126. h.logger.Debug("V3 search - user not found",
  1127. "searcher", uin,
  1128. "target", targetUIN,
  1129. "err", err,
  1130. )
  1131. }
  1132. }
  1133. }
  1134. // Send search done
  1135. return h.sendSearchDone(session, seq2)
  1136. }
  1137. // handleGetDeps1 processes in-session pre-auth list request (0x05F0).
  1138. // Historically called "get departments list" in iserverd. Used by the client
  1139. // to replicate the pre-auth response during a normal session.
  1140. // From iserverd v3_process_getdeps1() - sends ICQ_CMDxSND_USERxDEPS_LIST1 (0x0082)
  1141. func (h *V3Handler) handleGetDeps1(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1142. if session == nil {
  1143. return nil
  1144. }
  1145. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1146. h.logger.Debug("V3 get deps1 request", "uin", uin)
  1147. // Send deps list using 0x0082 (during session, not login)
  1148. return h.sendDeptsList1(session, seq2)
  1149. }
  1150. // sendDeptsList1 sends the in-session pre-auth list response (0x0082).
  1151. // Historically called "departments list" in iserverd (from its Users_Deps database table).
  1152. // Different from login depslist (0x0032).
  1153. func (h *V3Handler) sendDeptsList1(session *LegacySession, seq2 uint16) error {
  1154. // V3 DEPS_LIST1 format from iserverd:
  1155. // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + RESERVED(4) +
  1156. // UNKNOWN(2) + DEPLIST_VERSION(4) + COUNT(4) + [deps...] + TRAILER(4)
  1157. // For now, send empty list with version 1
  1158. pkt := make([]byte, 32)
  1159. offset := 0
  1160. // Header
  1161. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  1162. offset += 2
  1163. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserDepsList1) // 0x0082 - ICQ_CMDxSND_USERxDEPS_LIST1
  1164. offset += 2
  1165. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  1166. offset += 2
  1167. binary.LittleEndian.PutUint16(pkt[offset:], seq2)
  1168. offset += 2
  1169. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  1170. offset += 4
  1171. binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
  1172. offset += 4
  1173. // Unknown field (0x003C from iserverd)
  1174. binary.LittleEndian.PutUint16(pkt[offset:], 0x003C)
  1175. offset += 2
  1176. // Deplist version
  1177. binary.LittleEndian.PutUint32(pkt[offset:], 1)
  1178. offset += 4
  1179. // Count = 0 (empty deps list)
  1180. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  1181. offset += 4
  1182. // Trailer from iserverd
  1183. binary.LittleEndian.PutUint16(pkt[offset:], 0x0002)
  1184. offset += 2
  1185. binary.LittleEndian.PutUint16(pkt[offset:], 0x002a)
  1186. offset += 2
  1187. return h.sender.SendToSession(session, pkt[:offset])
  1188. }
  1189. // sendOfflineMessage sends an offline message to a V3 client
  1190. // From iserverd v3_send_offline_message()
  1191. // Format: header(16) + FROM_UIN(4) + YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MINUTE(1) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
  1192. func (h *V3Handler) sendOfflineMessage(session *LegacySession, seq2 uint16, msg *LegacyOfflineMessage) error {
  1193. // Build message data
  1194. msgBytes := []byte(msg.Message)
  1195. pktLen := 16 + 4 + 2 + 1 + 1 + 1 + 1 + 2 + 2 + len(msgBytes) + 1
  1196. pkt := make([]byte, pktLen)
  1197. offset := 0
  1198. // Header
  1199. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  1200. offset += 2
  1201. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvSysMsgOffline) // 0x00DC
  1202. offset += 2
  1203. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  1204. offset += 2
  1205. binary.LittleEndian.PutUint16(pkt[offset:], seq2)
  1206. offset += 2
  1207. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  1208. offset += 4
  1209. binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
  1210. offset += 4
  1211. // From UIN
  1212. binary.LittleEndian.PutUint32(pkt[offset:], msg.FromUIN)
  1213. offset += 4
  1214. // Timestamp: YEAR(2) + MONTH(1) + DAY(1) + HOUR(1) + MINUTE(1)
  1215. binary.LittleEndian.PutUint16(pkt[offset:], uint16(msg.Timestamp.Year()))
  1216. offset += 2
  1217. pkt[offset] = byte(msg.Timestamp.Month())
  1218. offset++
  1219. pkt[offset] = byte(msg.Timestamp.Day())
  1220. offset++
  1221. pkt[offset] = byte(msg.Timestamp.Hour())
  1222. offset++
  1223. pkt[offset] = byte(msg.Timestamp.Minute())
  1224. offset++
  1225. // Message type
  1226. binary.LittleEndian.PutUint16(pkt[offset:], msg.MsgType)
  1227. offset += 2
  1228. // Message length (including null terminator)
  1229. binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(msgBytes)+1))
  1230. offset += 2
  1231. // Message content
  1232. copy(pkt[offset:], msgBytes)
  1233. offset += len(msgBytes)
  1234. pkt[offset] = 0 // null terminator
  1235. h.logger.Debug("sending V3 offline message",
  1236. "to", session.UIN,
  1237. "from", msg.FromUIN,
  1238. "type", fmt.Sprintf("0x%04X", msg.MsgType),
  1239. "timestamp", msg.Timestamp,
  1240. )
  1241. return h.sender.SendToSession(session, pkt)
  1242. }
  1243. // sendOfflineMsgDone sends end of offline messages
  1244. func (h *V3Handler) sendOfflineMsgDone(session *LegacySession, seq2 uint16) error {
  1245. pkt := make([]byte, 16)
  1246. binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
  1247. binary.LittleEndian.PutUint16(pkt[2:4], 0x00E6) // ICQ_CMDxSND_SYSxMSGxDONE
  1248. binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
  1249. binary.LittleEndian.PutUint16(pkt[6:8], seq2)
  1250. binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
  1251. binary.LittleEndian.PutUint32(pkt[12:16], 0)
  1252. return h.sender.SendToSession(session, pkt)
  1253. }
  1254. // sendReplyOK sends a generic OK response
  1255. func (h *V3Handler) sendReplyOK(session *LegacySession, seq2 uint16, command uint16) error {
  1256. pkt := make([]byte, 16)
  1257. binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
  1258. binary.LittleEndian.PutUint16(pkt[2:4], command)
  1259. binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
  1260. binary.LittleEndian.PutUint16(pkt[6:8], seq2)
  1261. binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
  1262. binary.LittleEndian.PutUint32(pkt[12:16], 0)
  1263. return h.sender.SendToSession(session, pkt)
  1264. }
  1265. // sendSearchDone sends search complete response
  1266. // From iserverd v3_send_search_finished()
  1267. // Packet format (17 bytes total):
  1268. //
  1269. // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + RESERVED(4) + MORE(1)
  1270. //
  1271. // The 'more' parameter indicates if there are more search results available:
  1272. //
  1273. // 0 = no more results, 1 = more results available (pagination)
  1274. func (h *V3Handler) sendSearchDone(session *LegacySession, seq2 uint16, more ...bool) error {
  1275. pkt := make([]byte, 17)
  1276. binary.LittleEndian.PutUint16(pkt[0:2], ICQLegacyVersionV3)
  1277. binary.LittleEndian.PutUint16(pkt[2:4], 0x00A0) // ICQ_CMDxSND_SEARCHxDONE
  1278. binary.LittleEndian.PutUint16(pkt[4:6], session.NextServerSeqNum())
  1279. binary.LittleEndian.PutUint16(pkt[6:8], seq2)
  1280. binary.LittleEndian.PutUint32(pkt[8:12], session.UIN)
  1281. binary.LittleEndian.PutUint32(pkt[12:16], 0) // reserved
  1282. // Set 'more' flag - indicates if more search results are available
  1283. if len(more) > 0 && more[0] {
  1284. pkt[16] = 1 // more results available
  1285. } else {
  1286. pkt[16] = 0 // no more results
  1287. }
  1288. return h.sender.SendToSession(session, pkt)
  1289. }
  1290. // sendSearchFound sends a search result
  1291. // From iserverd v3_send_found_info()
  1292. // Format: header(16) + UIN(4) + NICK_LEN(2) + NICK + FIRST_LEN(2) + FIRST +
  1293. //
  1294. // LAST_LEN(2) + LAST + EMAIL_LEN(2) + EMAIL + AUTH(1)
  1295. func (h *V3Handler) sendSearchFound(session *LegacySession, seq2 uint16, foundUIN uint32, nick, first, last, email string, auth uint8) error {
  1296. // Calculate packet size
  1297. pktSize := 16 + 4 + 2 + len(nick) + 1 + 2 + len(first) + 1 + 2 + len(last) + 1 + 2 + len(email) + 1 + 1
  1298. pkt := make([]byte, pktSize)
  1299. offset := 0
  1300. // Header
  1301. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  1302. offset += 2
  1303. binary.LittleEndian.PutUint16(pkt[offset:], 0x008C) // ICQ_CMDxSND_SEARCHxFOUND
  1304. offset += 2
  1305. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  1306. offset += 2
  1307. binary.LittleEndian.PutUint16(pkt[offset:], seq2)
  1308. offset += 2
  1309. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  1310. offset += 4
  1311. binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
  1312. offset += 4
  1313. // Found user UIN
  1314. binary.LittleEndian.PutUint32(pkt[offset:], foundUIN)
  1315. offset += 4
  1316. // Nick (length-prefixed with null terminator)
  1317. binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(nick)+1))
  1318. offset += 2
  1319. copy(pkt[offset:], nick)
  1320. offset += len(nick)
  1321. pkt[offset] = 0
  1322. offset++
  1323. // First name
  1324. binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(first)+1))
  1325. offset += 2
  1326. copy(pkt[offset:], first)
  1327. offset += len(first)
  1328. pkt[offset] = 0
  1329. offset++
  1330. // Last name
  1331. binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(last)+1))
  1332. offset += 2
  1333. copy(pkt[offset:], last)
  1334. offset += len(last)
  1335. pkt[offset] = 0
  1336. offset++
  1337. // Email
  1338. binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(email)+1))
  1339. offset += 2
  1340. copy(pkt[offset:], email)
  1341. offset += len(email)
  1342. pkt[offset] = 0
  1343. offset++
  1344. // Auth flag
  1345. pkt[offset] = auth
  1346. offset++
  1347. h.logger.Debug("V3 sending search found",
  1348. "to", session.UIN,
  1349. "found_uin", foundUIN,
  1350. "nick", nick,
  1351. "packet_hex", fmt.Sprintf("%X", pkt[:offset]),
  1352. "packet_len", offset,
  1353. )
  1354. return h.sender.SendToSession(session, pkt[:offset])
  1355. }
  1356. // sendOnlineMessage sends an online system message to a V3/V4 client
  1357. // From iserverd v3_send_user_message()
  1358. // Format: header(16) + FROM_UIN(4) + MSG_TYPE(2) + MSG_LEN(2) + MESSAGE
  1359. // Note: V3 packets do NOT have a checkcode - they have a reserved field (always 0)
  1360. func (h *V3Handler) sendOnlineMessage(session *LegacySession, fromUIN uint32, msgType uint16, message string, seq2 uint16) error {
  1361. msgBytes := []byte(message)
  1362. h.logger.Debug("V3 sending online message",
  1363. "to", session.UIN,
  1364. "from", fromUIN,
  1365. "type", fmt.Sprintf("0x%04X", msgType),
  1366. "msg_len", len(msgBytes),
  1367. )
  1368. // Send V3 format packet
  1369. pktSize := 16 + 4 + 2 + 2 + len(msgBytes) + 1
  1370. pkt := make([]byte, pktSize)
  1371. offset := 0
  1372. // Header
  1373. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  1374. offset += 2
  1375. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvSysMsgOnline) // 0x0104
  1376. offset += 2
  1377. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  1378. offset += 2
  1379. binary.LittleEndian.PutUint16(pkt[offset:], seq2)
  1380. offset += 2
  1381. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  1382. offset += 4
  1383. binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved (V3 has NO checkcode!)
  1384. offset += 4
  1385. // From UIN
  1386. binary.LittleEndian.PutUint32(pkt[offset:], fromUIN)
  1387. offset += 4
  1388. // Message type (mask high byte for V3 clients)
  1389. binary.LittleEndian.PutUint16(pkt[offset:], msgType&0x00FF)
  1390. offset += 2
  1391. // Message length (including null terminator)
  1392. binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(msgBytes)+1))
  1393. offset += 2
  1394. // Message content
  1395. copy(pkt[offset:], msgBytes)
  1396. offset += len(msgBytes)
  1397. pkt[offset] = 0 // null terminator
  1398. offset++
  1399. h.logger.Debug("V3 sending online message packet",
  1400. "to", session.UIN,
  1401. "from", fromUIN,
  1402. "packet_hex", fmt.Sprintf("%X", pkt[:offset]),
  1403. )
  1404. return h.sender.SendToSession(session, pkt[:offset])
  1405. }
  1406. // sendUserOffline sends user offline notification
  1407. // From iserverd v3_send_user_offline()
  1408. func (h *V3Handler) sendUserOffline(session *LegacySession, uin uint32) error {
  1409. // V3 USER_OFFLINE format: header(16) + UIN(4)
  1410. pkt := make([]byte, 20)
  1411. offset := 0
  1412. // Header
  1413. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  1414. offset += 2
  1415. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserOffline)
  1416. offset += 2
  1417. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  1418. offset += 2
  1419. binary.LittleEndian.PutUint16(pkt[offset:], 0) // seq2
  1420. offset += 2
  1421. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  1422. offset += 4
  1423. binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
  1424. offset += 4
  1425. // UIN of the user who went offline
  1426. binary.LittleEndian.PutUint32(pkt[offset:], uin)
  1427. offset += 4
  1428. h.logger.Debug("sending V3 user offline notification",
  1429. "to", session.UIN,
  1430. "offline_uin", uin,
  1431. )
  1432. return h.sender.SendToSession(session, pkt[:offset])
  1433. }
  1434. // sendUserStatus sends user status change notification
  1435. // From iserverd v3_send_user_status() in make_packet.cpp
  1436. // This is used when a user changes status while already online
  1437. // (e.g., Away -> Online, Online -> DND)
  1438. // Format: header(16) + UIN(4) + STATUS(2) + ESTAT(2)
  1439. func (h *V3Handler) sendUserStatus(session *LegacySession, uin uint32, status uint32) error {
  1440. // V3 USER_STATUS format from iserverd:
  1441. // VERSION(2) + COMMAND(2) + SEQ1(2) + SEQ2(2) + UIN(4) + RESERVED(4) +
  1442. // TARGET_UIN(4) + STATUS(2) + ESTAT(2)
  1443. pkt := make([]byte, 24)
  1444. offset := 0
  1445. // Header
  1446. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  1447. offset += 2
  1448. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacySrvUserStatus) // 0x01A4
  1449. offset += 2
  1450. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  1451. offset += 2
  1452. binary.LittleEndian.PutUint16(pkt[offset:], 0) // seq2
  1453. offset += 2
  1454. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  1455. offset += 4
  1456. binary.LittleEndian.PutUint32(pkt[offset:], 0) // reserved
  1457. offset += 4
  1458. // UIN of the user whose status changed
  1459. binary.LittleEndian.PutUint32(pkt[offset:], uin)
  1460. offset += 4
  1461. // Status (low word)
  1462. binary.LittleEndian.PutUint16(pkt[offset:], uint16(status&0xFFFF))
  1463. offset += 2
  1464. // Extended status (high word)
  1465. binary.LittleEndian.PutUint16(pkt[offset:], uint16(status>>16))
  1466. offset += 2
  1467. h.logger.Debug("sending V3 user status change notification",
  1468. "to", session.UIN,
  1469. "changed_uin", uin,
  1470. "status", fmt.Sprintf("0x%08X", status),
  1471. )
  1472. return h.sender.SendToSession(session, pkt[:offset])
  1473. }
  1474. // handleGetNotes processes get notes request (0x05AA)
  1475. // From iserverd v3_process_notes() - returns user's notes
  1476. func (h *V3Handler) handleGetNotes(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1477. if session == nil {
  1478. return nil
  1479. }
  1480. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1481. // Parse: TIMESTAMP(4) + TARGET_UIN(4)
  1482. if len(data) < 8 {
  1483. return nil
  1484. }
  1485. targetUIN := binary.LittleEndian.Uint32(data[4:8])
  1486. h.logger.Debug("V3 get notes", "uin", uin, "target", targetUIN)
  1487. // Retrieve notes from database via service layer
  1488. ctx := context.Background()
  1489. notes, err := h.service.GetNotes(ctx, targetUIN)
  1490. if err != nil {
  1491. h.logger.Debug("V3 get notes failed", "uin", uin, "target", targetUIN, "err", err)
  1492. // Send empty notes on error
  1493. notes = ""
  1494. }
  1495. // Send notes response
  1496. return h.sendNotes(session, seq2, targetUIN, notes)
  1497. }
  1498. // handleSetNotes processes set notes request (0x0596)
  1499. // From iserverd v3_process_setnotes()
  1500. // Data format: TIMESTAMP(4) + NOTES_LEN(2) + NOTES
  1501. func (h *V3Handler) handleSetNotes(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1502. if session == nil {
  1503. return nil
  1504. }
  1505. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1506. // Parse notes from packet
  1507. // Format: TIMESTAMP(4) + NOTES_LEN(2) + NOTES
  1508. if len(data) < 6 {
  1509. h.logger.Debug("V3 set notes - data too short", "uin", uin, "len", len(data))
  1510. return h.sendReplyOK(session, seq2, 0x01CC)
  1511. }
  1512. offset := 4 // Skip timestamp
  1513. // Read notes length (2 bytes)
  1514. notesLen := binary.LittleEndian.Uint16(data[offset : offset+2])
  1515. offset += 2
  1516. // Read notes content
  1517. var notes string
  1518. if notesLen > 0 && offset+int(notesLen) <= len(data) {
  1519. notes = string(data[offset : offset+int(notesLen)])
  1520. // Remove null terminator if present
  1521. if len(notes) > 0 && notes[len(notes)-1] == 0 {
  1522. notes = notes[:len(notes)-1]
  1523. }
  1524. }
  1525. h.logger.Debug("V3 set notes", "uin", uin, "notes_len", len(notes))
  1526. // Save notes to database via service layer
  1527. ctx := context.Background()
  1528. if err := h.service.SetNotes(ctx, uin, notes); err != nil {
  1529. h.logger.Error("V3 set notes failed", "uin", uin, "err", err)
  1530. // Still send OK response to client (matching iserverd behavior)
  1531. }
  1532. // Send OK response (0x01CC = ICQ_CMDxSND_USERxSET_NOTES_OK)
  1533. return h.sendReplyOK(session, seq2, 0x01CC)
  1534. }
  1535. // handleSetPassword processes set password request (0x049C)
  1536. // From iserverd v3_process_setpass()
  1537. // Data format: TIMESTAMP(4) + PASSWORD_LEN(2) + NEW_PASSWORD
  1538. // Note: iserverd doesn't validate old password, just updates to new password
  1539. func (h *V3Handler) handleSetPassword(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1540. if session == nil {
  1541. return nil
  1542. }
  1543. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1544. // Parse password from data
  1545. // Format: TIMESTAMP(4) + PASSWORD_LEN(2) + NEW_PASSWORD
  1546. if len(data) < 6 {
  1547. h.logger.Debug("V3 set password - data too short", "uin", uin, "len", len(data))
  1548. return h.sendReplyOK(session, seq2, 0x0140)
  1549. }
  1550. offset := 0
  1551. // Skip timestamp (4 bytes)
  1552. offset += 4
  1553. // Read password length (2 bytes)
  1554. pwdLen := binary.LittleEndian.Uint16(data[offset : offset+2])
  1555. offset += 2
  1556. // Sanity check password length
  1557. if pwdLen > 32 {
  1558. pwdLen = 32
  1559. }
  1560. // Read new password
  1561. if offset+int(pwdLen) > len(data) {
  1562. h.logger.Debug("V3 set password - password truncated", "uin", uin)
  1563. return h.sendReplyOK(session, seq2, 0x0140)
  1564. }
  1565. newPassword := string(data[offset : offset+int(pwdLen)])
  1566. // Remove null terminator if present
  1567. if len(newPassword) > 0 && newPassword[len(newPassword)-1] == 0 {
  1568. newPassword = newPassword[:len(newPassword)-1]
  1569. }
  1570. h.logger.Info("V3 set password request",
  1571. "uin", uin,
  1572. "new_password_len", len(newPassword),
  1573. )
  1574. // Update password using service
  1575. // Note: iserverd doesn't validate old password, so we pass empty string
  1576. ctx := context.Background()
  1577. if err := h.service.SetPassword(ctx, uin, "", newPassword); err != nil {
  1578. h.logger.Error("V3 set password failed",
  1579. "uin", uin,
  1580. "err", err,
  1581. )
  1582. // Still send OK response to match iserverd behavior
  1583. // (iserverd doesn't have error handling for this)
  1584. } else {
  1585. h.logger.Info("V3 password updated successfully", "uin", uin)
  1586. }
  1587. // Send OK response (0x0140 = ICQ_CMDxSND_USERxSET_PASSWD_OK)
  1588. return h.sendReplyOK(session, seq2, 0x0140)
  1589. }
  1590. // handleSetAuth processes set auth mode request (0x0514)
  1591. // From iserverd v3_process_setauth()
  1592. // Data format: TIMESTAMP(4) + AUTH_MODE(1)
  1593. // - AUTH_MODE: 0 = no auth required, 1 = auth required
  1594. func (h *V3Handler) handleSetAuth(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1595. if session == nil {
  1596. return nil
  1597. }
  1598. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1599. // Parse auth mode - format: TIMESTAMP(4) + AUTH_MODE(1)
  1600. // From iserverd v3_process_setauth():
  1601. // int_pack >> ... >> temp_stamp;
  1602. // auth_mode = 0;
  1603. // int_pack >> auth_mode;
  1604. authRequired := false
  1605. if len(data) >= 5 {
  1606. // Skip timestamp (4 bytes), read auth_mode (1 byte)
  1607. authMode := data[4]
  1608. authRequired = authMode != 0
  1609. }
  1610. h.logger.Debug("V3 set auth",
  1611. "uin", uin,
  1612. "auth_required", authRequired,
  1613. )
  1614. // Update auth mode in database
  1615. ctx := context.Background()
  1616. if err := h.service.SetAuthMode(ctx, uin, authRequired); err != nil {
  1617. h.logger.Error("V3 set auth failed",
  1618. "uin", uin,
  1619. "err", err,
  1620. )
  1621. // Still send OK response to match iserverd behavior
  1622. }
  1623. // Send OK response (0x01F4 = ICQ_CMDxSND_USERxSET_AUTH_OK)
  1624. return h.sendReplyOK(session, seq2, 0x01F4)
  1625. }
  1626. // handleSetState processes set client state request (0x0528)
  1627. // From iserverd v3_process_state() - client state/mode changes
  1628. // This is the V3 status change command - when a user changes their status
  1629. // (e.g., Online -> Away, Away -> DND), this handler updates the session
  1630. // and broadcasts the status change to all contacts.
  1631. //
  1632. // Data format: TIMESTAMP(4) + STATUS(4) + ESTATUS(4)
  1633. // - STATUS: Low 16 bits of status (Online=0, Away=1, DND=2, NA=4, etc.)
  1634. // - ESTATUS: High 16 bits of status (extended status flags)
  1635. func (h *V3Handler) handleSetState(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1636. if session == nil {
  1637. return nil
  1638. }
  1639. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1640. // Parse status - format: TIMESTAMP(4) + STATUS(4) + ESTATUS(4)
  1641. // From iserverd v3_process_status():
  1642. // int_pack >> ... >> new_status >> new_estatus;
  1643. if len(data) < 12 {
  1644. h.logger.Debug("V3 set state - data too short", "uin", uin, "len", len(data))
  1645. return nil
  1646. }
  1647. // Skip timestamp (4 bytes)
  1648. offset := 4
  1649. // Read status (4 bytes) - low word is status, high word is extended status
  1650. newStatus := binary.LittleEndian.Uint32(data[offset : offset+4])
  1651. offset += 4
  1652. // Read extended status (4 bytes) - some clients send this separately
  1653. newEStatus := binary.LittleEndian.Uint32(data[offset : offset+4])
  1654. // Combine status and extended status (iserverd: user.status = new_status, user.estat = new_estatus)
  1655. // The full status is stored as: status | (estatus << 16)
  1656. fullStatus := newStatus | (newEStatus << 16)
  1657. oldStatus := session.GetStatus()
  1658. session.SetStatus(fullStatus)
  1659. h.logger.Info("V3 status change",
  1660. "uin", uin,
  1661. "old_status", fmt.Sprintf("0x%08X", oldStatus),
  1662. "new_status", fmt.Sprintf("0x%08X", fullStatus),
  1663. )
  1664. // Broadcast status change to all contacts who have this user in their list
  1665. // From iserverd v3_process_status(): broadcast_status(user, old_status)
  1666. h.broadcastStatusChange(session, fullStatus)
  1667. return nil
  1668. }
  1669. // broadcastStatusChange notifies all contacts who have this user in their list
  1670. // that the user's status has changed.
  1671. // From iserverd broadcast_status() in broadcast.cpp - this function finds all
  1672. // online users who have the target user in their contact list and sends them
  1673. // a status update notification.
  1674. func (h *V3Handler) broadcastStatusChange(session *LegacySession, newStatus uint32) {
  1675. if h.dispatcher == nil {
  1676. return
  1677. }
  1678. // Find all sessions that have this user in their contact list
  1679. // This is the same logic used for online/offline notifications
  1680. contactsToNotify := h.sessions.NotifyContactsOfStatus(session)
  1681. h.logger.Debug("broadcasting status change to contacts",
  1682. "uin", session.UIN,
  1683. "new_status", fmt.Sprintf("0x%08X", newStatus),
  1684. "contacts_to_notify", contactsToNotify,
  1685. )
  1686. for _, contactUIN := range contactsToNotify {
  1687. contactSession := h.sessions.GetSession(contactUIN)
  1688. if contactSession != nil {
  1689. // Use dispatcher to send status change in correct protocol format
  1690. // This routes to V3 or V5 handler based on the contact's protocol version
  1691. _ = h.dispatcher.SendStatusChange(contactSession, session.UIN, newStatus)
  1692. }
  1693. }
  1694. }
  1695. // handleUsageStats processes usage statistics (0x0532)
  1696. // From iserverd v3_process_usagestats() - client sends usage data
  1697. func (h *V3Handler) handleUsageStats(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1698. if session == nil {
  1699. return nil
  1700. }
  1701. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1702. h.logger.Debug("V3 usage stats", "uin", uin)
  1703. return nil
  1704. }
  1705. // handleGetExternals processes get externals request (0x04C4)
  1706. // From iserverd v3_process_getext() - returns external services list
  1707. func (h *V3Handler) handleGetExternals(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1708. if session == nil {
  1709. return nil
  1710. }
  1711. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1712. h.logger.Debug("V3 get externals", "uin", uin)
  1713. // Send externals response (empty list)
  1714. return h.sendExternals(session, seq2)
  1715. }
  1716. // handleSysAck processes system message acknowledgment (0x0442)
  1717. // From iserverd v3_process_sysack() - client acknowledges receipt of offline messages
  1718. // After receiving this, we delete the offline messages from the database
  1719. func (h *V3Handler) handleSysAck(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1720. if session == nil {
  1721. return nil
  1722. }
  1723. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1724. ctx := context.Background()
  1725. // Delete offline messages for this user
  1726. if err := h.service.AckOfflineMessages(ctx, uin); err != nil {
  1727. h.logger.Error("failed to delete offline messages", "uin", uin, "err", err)
  1728. } else {
  1729. h.logger.Debug("V3 offline messages deleted", "uin", uin)
  1730. }
  1731. return nil
  1732. }
  1733. // handleOnlineInfo processes reconnect/online info request (0x015E)
  1734. // From iserverd v3_process_onlineinfo()
  1735. func (h *V3Handler) handleOnlineInfo(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1736. if session == nil {
  1737. return nil
  1738. }
  1739. _ = h.sendAck(session.Addr, seq1, seq2, uin)
  1740. h.logger.Debug("V3 online info", "uin", uin)
  1741. return nil
  1742. }
  1743. // handleGetInfo1 processes get basic info only request (0x0460)
  1744. // From iserverd v3_process_getinfo1() - sends only basic info (not full 5-packet set)
  1745. //
  1746. // Refactored to use service layer (GetUserInfoForProtocol) and packet builder.
  1747. // Following the OSCAR pattern: unmarshal -> call service -> build response
  1748. func (h *V3Handler) handleGetInfo1(session *LegacySession, seq1, seq2 uint16, uin uint32, data []byte) error {
  1749. if session == nil {
  1750. return nil
  1751. }
  1752. // 1. Send ACK using packet builder
  1753. _ = h.sender.SendPacket(session.Addr, h.packetBuilder.BuildAck(seq1, seq2, uin))
  1754. // 2. Unmarshal packet to extract target UIN
  1755. // Format: TIMESTAMP(4) + TARGET_UIN(4)
  1756. if len(data) < 8 {
  1757. return nil
  1758. }
  1759. targetUIN := binary.LittleEndian.Uint32(data[4:8])
  1760. h.logger.Debug("V3 get info1", "uin", uin, "target", targetUIN)
  1761. // 3. Call service layer to get user info
  1762. ctx := context.Background()
  1763. info, err := h.service.GetUserInfoForProtocol(ctx, targetUIN)
  1764. if err != nil {
  1765. h.logger.Error("failed to get user info", "target", targetUIN, "err", err)
  1766. // Still send minimal info packet even on error
  1767. info = &UserInfoResult{
  1768. UIN: targetUIN,
  1769. Nickname: fmt.Sprintf("%d", targetUIN),
  1770. }
  1771. }
  1772. // 4. Build and send only basic info using packet builder
  1773. // (unlike handleGetInfo which sends all 5 packets)
  1774. return h.sender.SendToSession(session, h.packetBuilder.BuildBasicInfo(session.NextServerSeqNum(), seq2, session.UIN, info))
  1775. }
  1776. // sendNotes sends notes response (0x0352)
  1777. // From iserverd v3_send_notes()
  1778. func (h *V3Handler) sendNotes(session *LegacySession, seq2 uint16, targetUIN uint32, notes string) error {
  1779. pktSize := 16 + 2 + len(notes) + 1 + 12 // header + notes + timestamps
  1780. pkt := make([]byte, pktSize)
  1781. offset := 0
  1782. // Header
  1783. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  1784. offset += 2
  1785. binary.LittleEndian.PutUint16(pkt[offset:], 0x0352) // ICQ_CMDxSND_USERxNOTES
  1786. offset += 2
  1787. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  1788. offset += 2
  1789. binary.LittleEndian.PutUint16(pkt[offset:], seq2)
  1790. offset += 2
  1791. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  1792. offset += 4
  1793. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  1794. offset += 4
  1795. // Notes (length-prefixed)
  1796. binary.LittleEndian.PutUint16(pkt[offset:], uint16(len(notes)+1))
  1797. offset += 2
  1798. copy(pkt[offset:], notes)
  1799. offset += len(notes)
  1800. pkt[offset] = 0
  1801. offset++
  1802. // Timestamps: nupdate(4) + lastlogin(4) + ip_addr(4)
  1803. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  1804. offset += 4
  1805. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  1806. offset += 4
  1807. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  1808. offset += 4
  1809. return h.sender.SendToSession(session, pkt[:offset])
  1810. }
  1811. // sendExternals sends externals list response (0x00B4)
  1812. // From iserverd v3_send_externals()
  1813. func (h *V3Handler) sendExternals(session *LegacySession, seq2 uint16) error {
  1814. pkt := make([]byte, 20)
  1815. offset := 0
  1816. // Header
  1817. binary.LittleEndian.PutUint16(pkt[offset:], ICQLegacyVersionV3)
  1818. offset += 2
  1819. binary.LittleEndian.PutUint16(pkt[offset:], 0x00B4) // ICQ_CMDxSND_USERxEXTERNALS
  1820. offset += 2
  1821. binary.LittleEndian.PutUint16(pkt[offset:], session.NextServerSeqNum())
  1822. offset += 2
  1823. binary.LittleEndian.PutUint16(pkt[offset:], seq2)
  1824. offset += 2
  1825. binary.LittleEndian.PutUint32(pkt[offset:], session.UIN)
  1826. offset += 4
  1827. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  1828. offset += 4
  1829. // Count = 0 (no externals)
  1830. binary.LittleEndian.PutUint32(pkt[offset:], 0)
  1831. offset += 4
  1832. return h.sender.SendToSession(session, pkt[:offset])
  1833. }