cmd_client.go 86 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715
  1. package toc
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/base64"
  6. "encoding/csv"
  7. "encoding/hex"
  8. "errors"
  9. "fmt"
  10. "log/slog"
  11. "net/url"
  12. "sort"
  13. "strconv"
  14. "strings"
  15. "sync"
  16. "time"
  17. "github.com/google/uuid"
  18. "github.com/mk6i/open-oscar-server/config"
  19. "github.com/mk6i/open-oscar-server/state"
  20. "github.com/mk6i/open-oscar-server/wire"
  21. )
  22. // NewChatRegistry creates a new ChatRegistry instance.
  23. func NewChatRegistry() *ChatRegistry {
  24. chatRegistry := &ChatRegistry{
  25. lookup: make(map[int]wire.ICBMRoomInfo),
  26. sessions: make(map[int]*state.SessionInstance),
  27. m: sync.RWMutex{},
  28. }
  29. return chatRegistry
  30. }
  31. // ChatRegistry manages the chat rooms that a user is connected to during a TOC
  32. // session. It maintains mappings between chat room identifiers, metadata, and
  33. // active chat sessions.
  34. //
  35. // This struct provides thread-safe operations for adding, retrieving, and managing
  36. // chat room metadata and associated sessions.
  37. type ChatRegistry struct {
  38. lookup map[int]wire.ICBMRoomInfo // Maps chat room IDs to their metadata.
  39. sessions map[int]*state.SessionInstance // Tracks active chat sessions by chat room ID.
  40. nextID int // Incremental identifier for newly added chat rooms.
  41. m sync.RWMutex // Synchronization primitive for concurrent access.
  42. }
  43. // Add registers metadata for a newly joined chat room and returns a unique
  44. // identifier for it. If the room is already registered, it returns the existing ID.
  45. func (c *ChatRegistry) Add(room wire.ICBMRoomInfo) int {
  46. c.m.Lock()
  47. defer c.m.Unlock()
  48. for chatID, r := range c.lookup {
  49. if r == room {
  50. return chatID
  51. }
  52. }
  53. id := c.nextID
  54. c.lookup[id] = room
  55. c.nextID++
  56. return id
  57. }
  58. // LookupRoom retrieves metadata for the chat room registered with chatID.
  59. // It returns the room metadata and a boolean indicating whether the chat ID
  60. // was found.
  61. func (c *ChatRegistry) LookupRoom(chatID int) (wire.ICBMRoomInfo, bool) {
  62. c.m.RLock()
  63. defer c.m.RUnlock()
  64. room, found := c.lookup[chatID]
  65. return room, found
  66. }
  67. // RegisterSess associates a chat session with a chat room. If a session is
  68. // already registered for the given chat ID, it will be overwritten.
  69. func (c *ChatRegistry) RegisterSess(chatID int, instance *state.SessionInstance) {
  70. c.m.Lock()
  71. defer c.m.Unlock()
  72. c.sessions[chatID] = instance
  73. }
  74. // RetrieveSess retrieves the chat session associated with the given chat ID.
  75. // If no session is registered for the chat ID, it returns nil.
  76. func (c *ChatRegistry) RetrieveSess(chatID int) *state.SessionInstance {
  77. c.m.RLock()
  78. defer c.m.RUnlock()
  79. return c.sessions[chatID]
  80. }
  81. // RemoveSess removes a chat session.
  82. func (c *ChatRegistry) RemoveSess(chatID int) {
  83. c.m.Lock()
  84. defer c.m.Unlock()
  85. delete(c.sessions, chatID)
  86. }
  87. // Sessions retrieves all the chat sessions.
  88. func (c *ChatRegistry) Sessions() []*state.SessionInstance {
  89. c.m.RLock()
  90. defer c.m.RUnlock()
  91. sessions := make([]*state.SessionInstance, 0, len(c.sessions))
  92. for _, s := range c.sessions {
  93. sessions = append(sessions, s)
  94. }
  95. return sessions
  96. }
  97. // OSCARProxy acts as a bridge between TOC clients and the OSCAR server,
  98. // translating protocol messages between the two.
  99. //
  100. // It performs the following functions:
  101. // - Receives TOC messages from the client, converts them into SNAC messages,
  102. // and forwards them to the OSCAR server. The SNAC response is then converted
  103. // back into a TOC response for the client.
  104. // - Receives incoming messages from the OSCAR server and translates them into
  105. // TOC responses for the client.
  106. type OSCARProxy struct {
  107. AdminService AdminService
  108. AuthService AuthService
  109. BuddyListRegistry BuddyListRegistry
  110. BuddyService BuddyService
  111. ChatNavService ChatNavService
  112. ChatService ChatService
  113. ChatSessionManager ChatSessionManager
  114. CookieBaker CookieBaker
  115. DirSearchService DirSearchService
  116. ICBMService ICBMService
  117. LocateService LocateService
  118. Logger *slog.Logger
  119. OServiceService OServiceService
  120. PermitDenyService PermitDenyService
  121. TOCConfigStore TOCConfigStore
  122. SessionRetriever SessionRetriever
  123. FeedbagService FeedbagService
  124. FeedbagManager FeedbagManager
  125. SNACRateLimits wire.SNACRateLimits
  126. HTTPIPRateLimiter *IPRateLimiter
  127. // RandIntn is the source for feedbag item ID generation.
  128. // Inject a deterministic func in tests to assert exact feedbag item slices.
  129. RandIntn func(n int) int
  130. }
  131. // RecvClientCmd processes a client TOC command and returns a server reply.
  132. //
  133. // * sessBOS is the current user's session.
  134. // * chatRegistry manages the current user's chat sessions
  135. // * payload is the command + arguments
  136. // * toCh is the channel that transports messages to client
  137. // * doAsync performs async tasks, is auto-cleaned up by caller
  138. //
  139. // It returns true if the server can continue processing commands.
  140. func (s OSCARProxy) RecvClientCmd(
  141. ctx context.Context,
  142. sessBOS *state.SessionInstance,
  143. chatRegistry *ChatRegistry,
  144. payload []byte,
  145. toCh chan<- []string,
  146. doAsync func(f func() error),
  147. ) (replies []string) {
  148. cmd := payload
  149. var args []byte
  150. if idx := bytes.IndexByte(payload, ' '); idx > -1 {
  151. cmd, args = payload[:idx], payload[idx:]
  152. }
  153. if s.Logger.Enabled(ctx, slog.LevelDebug) {
  154. s.Logger.DebugContext(ctx, "client request", "command", payload)
  155. } else {
  156. s.Logger.InfoContext(ctx, "client request", "command", cmd)
  157. }
  158. switch string(cmd) {
  159. case "toc_send_im", "toc2_send_im":
  160. return s.SendIM(ctx, sessBOS, args)
  161. case "toc_init_done":
  162. return s.InitDone(ctx, sessBOS)
  163. case "toc_add_buddy":
  164. return s.AddBuddy(ctx, sessBOS, args)
  165. case "toc_get_status":
  166. return s.GetStatus(ctx, sessBOS, args)
  167. case "toc_remove_buddy":
  168. return s.RemoveBuddy(ctx, sessBOS, args)
  169. case "toc_add_permit":
  170. return s.AddPermit(ctx, sessBOS, args)
  171. case "toc_add_deny":
  172. return s.AddDeny(ctx, sessBOS, args)
  173. case "toc_set_away":
  174. return s.SetAway(ctx, sessBOS, args)
  175. case "toc_set_caps":
  176. return s.SetCaps(ctx, sessBOS, args)
  177. case "toc_evil":
  178. return s.Evil(ctx, sessBOS, args)
  179. case "toc_get_info":
  180. return s.GetInfoURL(ctx, sessBOS, args)
  181. case "toc_change_passwd":
  182. return s.ChangePassword(ctx, sessBOS, args)
  183. case "toc_format_nickname":
  184. return s.FormatNickname(ctx, sessBOS, args)
  185. case "toc_chat_join", "toc_chat_accept":
  186. var chatID int
  187. var msg []string
  188. if string(cmd) == "toc_chat_join" {
  189. chatID, msg = s.ChatJoin(ctx, sessBOS, chatRegistry, args)
  190. } else {
  191. chatID, msg = s.ChatAccept(ctx, sessBOS, chatRegistry, args)
  192. }
  193. if len(msg) > 0 && msg[0] == cmdInternalSvcErr {
  194. return msg
  195. }
  196. doAsync(func() error {
  197. sess := chatRegistry.RetrieveSess(chatID)
  198. s.RecvChat(ctx, sess, chatID, toCh)
  199. return nil
  200. })
  201. return msg
  202. case "toc_chat_send":
  203. return s.ChatSend(ctx, chatRegistry, args)
  204. case "toc_chat_whisper":
  205. return s.ChatWhisper(ctx, chatRegistry, args)
  206. case "toc_chat_leave":
  207. return s.ChatLeave(ctx, chatRegistry, args)
  208. case "toc_set_info":
  209. return s.SetInfo(ctx, sessBOS, args)
  210. case "toc_set_dir":
  211. return s.SetDir(ctx, sessBOS, args)
  212. case "toc_set_idle":
  213. return s.SetIdle(ctx, sessBOS, args)
  214. case "toc_set_config":
  215. return s.SetConfig(ctx, sessBOS, args)
  216. case "toc_chat_invite":
  217. return s.ChatInvite(ctx, sessBOS, chatRegistry, args)
  218. case "toc_dir_search":
  219. return s.GetDirSearchURL(ctx, sessBOS, args)
  220. case "toc_get_dir":
  221. return s.GetDirURL(ctx, sessBOS, args)
  222. case "toc_rvous_accept":
  223. return s.RvousAccept(ctx, sessBOS, args)
  224. case "toc_rvous_cancel":
  225. return s.RvousCancel(ctx, sessBOS, args)
  226. case "toc2_set_pdmode":
  227. return s.SetPDMode(ctx, sessBOS, args)
  228. case "toc2_send_im_enc":
  229. return s.SendIMEnc(ctx, sessBOS, args)
  230. case "toc2_remove_buddy":
  231. return s.RemoveBuddy2(ctx, sessBOS, args)
  232. case "toc2_new_group":
  233. return s.NewGroup(ctx, sessBOS, args)
  234. case "toc2_del_group":
  235. return s.DelGroup(ctx, sessBOS, args)
  236. case "toc2_new_buddies":
  237. return s.NewBuddies(ctx, sessBOS, args)
  238. case "toc2_add_permit":
  239. return s.AddPermit2(ctx, sessBOS, args)
  240. case "toc2_remove_permit":
  241. return s.RemovePermit2(ctx, sessBOS, args)
  242. case "toc2_add_deny":
  243. return s.AddDeny2(ctx, sessBOS, args)
  244. case "toc2_remove_deny":
  245. return s.RemoveDeny2(ctx, sessBOS, args)
  246. case "toc2_client_event":
  247. return s.SendClientEvent(ctx, sessBOS, args)
  248. }
  249. s.Logger.ErrorContext(ctx, fmt.Sprintf("unsupported TOC command %s", cmd))
  250. return []string{cmdInternalSvcErr}
  251. }
  252. // AddBuddy handles the toc_add_buddy TOC command.
  253. //
  254. // From the TiK documentation:
  255. //
  256. // Add buddies to your buddy list. This does not change your saved config.
  257. //
  258. // Command syntax: toc_add_buddy <Buddy User 1> [<Buddy User2> [<Buddy User 3> [...]]]
  259. func (s OSCARProxy) AddBuddy(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  260. if msg, isLimited := s.checkRateLimit(ctx, me, wire.Buddy, wire.BuddyAddBuddies); isLimited {
  261. return msg
  262. }
  263. users, err := parseArgs(args)
  264. if err != nil {
  265. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  266. }
  267. snac := wire.SNAC_0x03_0x04_BuddyAddBuddies{}
  268. for _, sn := range users {
  269. snac.Buddies = append(snac.Buddies, struct {
  270. ScreenName string `oscar:"len_prefix=uint8"`
  271. }{ScreenName: sn})
  272. }
  273. if err := s.BuddyService.AddBuddies(ctx, me, snac); err != nil {
  274. return s.runtimeErr(ctx, fmt.Errorf("BuddyService.AddBuddies: %w", err))
  275. }
  276. return []string{}
  277. }
  278. // AddPermit handles the toc_add_permit TOC command.
  279. //
  280. // From the TiK documentation:
  281. //
  282. // ADD the following people to your permit mode. If you are in deny mode it
  283. // will switch you to permit mode first. With no arguments and in deny mode
  284. // this will switch you to permit none. If already in permit mode, no
  285. // arguments does nothing and your permit list remains the same.
  286. //
  287. // Command syntax: toc_add_permit [ <User 1> [<User 2> [...]]]
  288. func (s OSCARProxy) AddPermit(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  289. if msg, isLimited := s.checkRateLimit(ctx, me, wire.PermitDeny, wire.PermitDenyAddDenyListEntries); isLimited {
  290. return msg
  291. }
  292. users, err := parseArgs(args)
  293. if err != nil {
  294. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  295. }
  296. snac := wire.SNAC_0x09_0x05_PermitDenyAddPermListEntries{}
  297. for _, sn := range users {
  298. snac.Users = append(snac.Users, struct {
  299. ScreenName string `oscar:"len_prefix=uint8"`
  300. }{ScreenName: sn})
  301. }
  302. if err := s.PermitDenyService.AddPermListEntries(ctx, me, snac); err != nil {
  303. return s.runtimeErr(ctx, fmt.Errorf("PermitDenyService.AddPermListEntries: %w", err))
  304. }
  305. return []string{}
  306. }
  307. // AddDeny handles the toc_add_deny TOC command.
  308. //
  309. // From the TiK documentation:
  310. //
  311. // ADD the following people to your deny mode. If you are in permit mode it
  312. // will switch you to deny mode first. With no arguments and in permit mode,
  313. // this will switch you to deny none. If already in deny mode, no arguments
  314. // does nothing and your deny list remains unchanged.
  315. //
  316. // Command syntax: toc_add_deny [ <User 1> [<User 2> [...]]]
  317. func (s OSCARProxy) AddDeny(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  318. if msg, isLimited := s.checkRateLimit(ctx, me, wire.PermitDeny, wire.PermitDenyAddDenyListEntries); isLimited {
  319. return msg
  320. }
  321. users, err := parseArgs(args)
  322. if err != nil {
  323. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  324. }
  325. snac := wire.SNAC_0x09_0x07_PermitDenyAddDenyListEntries{}
  326. for _, sn := range users {
  327. snac.Users = append(snac.Users, struct {
  328. ScreenName string `oscar:"len_prefix=uint8"`
  329. }{ScreenName: sn})
  330. }
  331. if err := s.PermitDenyService.AddDenyListEntries(ctx, me, snac); err != nil {
  332. return s.runtimeErr(ctx, fmt.Errorf("PermitDenyService.AddDenyListEntries: %w", err))
  333. }
  334. return []string{}
  335. }
  336. // ChangePassword handles the toc_change_passwd TOC command.
  337. //
  338. // From the TiK documentation:
  339. //
  340. // Change a user's password. An ADMIN_PASSWD_STATUS or ERROR message will be
  341. // sent back to the client.
  342. //
  343. // Command syntax: toc_change_passwd <existing_passwd> <new_passwd>
  344. func (s OSCARProxy) ChangePassword(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  345. if msg, isLimited := s.checkRateLimit(ctx, me, wire.Admin, wire.AdminInfoChangeRequest); isLimited {
  346. return msg
  347. }
  348. var oldPass, newPass string
  349. if _, err := parseArgs(args, &oldPass, &newPass); err != nil {
  350. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  351. }
  352. oldPass = unescape(oldPass)
  353. newPass = unescape(newPass)
  354. reqSNAC := wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  355. TLVRestBlock: wire.TLVRestBlock{
  356. TLVList: wire.TLVList{
  357. wire.NewTLVBE(wire.AdminTLVOldPassword, oldPass),
  358. wire.NewTLVBE(wire.AdminTLVNewPassword, newPass),
  359. },
  360. },
  361. }
  362. reply, err := s.AdminService.InfoChangeRequest(ctx, me, wire.SNACFrame{}, reqSNAC)
  363. if err != nil {
  364. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: %w", err))
  365. }
  366. replyBody, ok := reply.Body.(wire.SNAC_0x07_0x05_AdminChangeReply)
  367. if !ok {
  368. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: unexpected response type %v", replyBody))
  369. }
  370. code, ok := replyBody.Uint16BE(wire.AdminTLVErrorCode)
  371. if ok {
  372. switch code {
  373. case wire.AdminInfoErrorInvalidPasswordLength:
  374. return []string{"ERROR:" + wire.TOCErrorAdminInvalidInput}
  375. case wire.AdminInfoErrorValidatePassword:
  376. return []string{"ERROR:" + wire.TOCErrorAuthIncorrectNickOrPassword}
  377. default:
  378. return []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest}
  379. }
  380. }
  381. return []string{"ADMIN_PASSWD_STATUS:0"}
  382. }
  383. // ChatAccept handles the toc_chat_accept TOC command.
  384. //
  385. // From the TiK documentation:
  386. //
  387. // Accept a CHAT_INVITE message from TOC. The server will send a CHAT_JOIN in
  388. // response.
  389. //
  390. // Command syntax: toc_chat_accept <Chat Room ID>
  391. func (s OSCARProxy) ChatAccept(
  392. ctx context.Context,
  393. me *state.SessionInstance,
  394. chatRegistry *ChatRegistry,
  395. args []byte,
  396. ) (int, []string) {
  397. var chatIDStr string
  398. if _, err := parseArgs(args, &chatIDStr); err != nil {
  399. return 0, s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  400. }
  401. chatID, err := strconv.Atoi(chatIDStr)
  402. if err != nil {
  403. return 0, s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  404. }
  405. chatInfo, found := chatRegistry.LookupRoom(chatID)
  406. if !found {
  407. return 0, s.runtimeErr(ctx, fmt.Errorf("chatRegistry.LookupRoom: no chat found for ID %d", chatID))
  408. }
  409. if msg, isLimited := s.checkRateLimit(ctx, me, wire.ChatNav, wire.ChatNavRequestRoomInfo); isLimited {
  410. return 0, msg
  411. }
  412. reqRoomSNAC := wire.SNAC_0x0D_0x04_ChatNavRequestRoomInfo{
  413. Cookie: chatInfo.Cookie,
  414. Exchange: chatInfo.Exchange,
  415. InstanceNumber: chatInfo.Instance,
  416. }
  417. reqRoomReply, err := s.ChatNavService.RequestRoomInfo(ctx, wire.SNACFrame{}, reqRoomSNAC)
  418. if err != nil {
  419. return 0, s.runtimeErr(ctx, fmt.Errorf("ChatNavService.RequestRoomInfo: %w", err))
  420. }
  421. reqRoomReplyBody, ok := reqRoomReply.Body.(wire.SNAC_0x0D_0x09_ChatNavNavInfo)
  422. if !ok {
  423. return 0, s.runtimeErr(
  424. ctx,
  425. fmt.Errorf("chatNavService.RequestRoomInfo: unexpected response type %v", reqRoomReplyBody),
  426. )
  427. }
  428. b, hasInfo := reqRoomReplyBody.Bytes(wire.ChatNavTLVRoomInfo)
  429. if !hasInfo {
  430. return 0, s.runtimeErr(ctx, errors.New("reqRoomReplyBody.Bytes: missing wire.ChatNavTLVRoomInfo"))
  431. }
  432. roomInfo := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
  433. if err := wire.UnmarshalBE(&roomInfo, bytes.NewReader(b)); err != nil {
  434. return 0, s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  435. }
  436. roomName, hasName := roomInfo.Bytes(wire.ChatRoomTLVRoomName)
  437. if !hasName {
  438. return 0, s.runtimeErr(ctx, errors.New("roomInfo.Bytes: missing wire.ChatRoomTLVRoomName"))
  439. }
  440. if msg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceServiceRequest); isLimited {
  441. return 0, msg
  442. }
  443. svcReqSNAC := wire.SNAC_0x01_0x04_OServiceServiceRequest{
  444. FoodGroup: wire.Chat,
  445. TLVRestBlock: wire.TLVRestBlock{
  446. TLVList: wire.TLVList{
  447. wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
  448. Cookie: chatInfo.Cookie,
  449. }),
  450. },
  451. },
  452. }
  453. svcReqReply, err := s.OServiceService.ServiceRequest(ctx, wire.BOS, me, wire.SNACFrame{}, svcReqSNAC, config.Listener{})
  454. if err != nil {
  455. return 0, s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.ServiceRequest: %w", err))
  456. }
  457. svcReqReplyBody, ok := svcReqReply.Body.(wire.SNAC_0x01_0x05_OServiceServiceResponse)
  458. if !ok {
  459. return 0, s.runtimeErr(
  460. ctx,
  461. fmt.Errorf("OServiceServiceBOS.ServiceRequest: unexpected response type %v", svcReqReplyBody),
  462. )
  463. }
  464. loginCookie, hasCookie := svcReqReplyBody.Bytes(wire.OServiceTLVTagsLoginCookie)
  465. if !hasCookie {
  466. return 0, s.runtimeErr(ctx, errors.New("missing wire.OServiceTLVTagsLoginCookie"))
  467. }
  468. // todo: naming for cookie: login cookie, server cookie, or auth cookie?
  469. serverCookie, err := s.AuthService.CrackCookie(loginCookie)
  470. if err != nil {
  471. return 0, s.runtimeErr(ctx, fmt.Errorf("AuthService.CrackCookie: %w", err))
  472. }
  473. sessCfg := func(sess *state.Session) {
  474. sess.OnSessionClose(func() {
  475. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
  476. defer cancel()
  477. s.AuthService.SignoutChat(ctx, sess)
  478. })
  479. }
  480. chatSess, err := s.AuthService.RegisterChatSession(ctx, serverCookie, sessCfg)
  481. if err != nil {
  482. return 0, s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterChatSession: %w", err))
  483. }
  484. if msg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceClientOnline); isLimited {
  485. return 0, msg
  486. }
  487. if err := s.OServiceService.ClientOnline(ctx, wire.Chat, wire.SNAC_0x01_0x02_OServiceClientOnline{}, chatSess); err != nil {
  488. return 0, s.runtimeErr(ctx, fmt.Errorf("OServiceServiceChat.ClientOnline: %w", err))
  489. }
  490. chatRegistry.RegisterSess(chatID, chatSess)
  491. if me.IsTOC2() {
  492. chatSess.SetTOC2(me.SupportsTOC2MsgEnc())
  493. }
  494. return chatID, []string{fmt.Sprintf("CHAT_JOIN:%d:%s", chatID, roomName)}
  495. }
  496. // ChatInvite handles the toc_chat_invite TOC command.
  497. //
  498. // From the TiK documentation:
  499. //
  500. // Once you are inside a chat room you can invite other people into that room.
  501. // Remember to quote and encode the invite message.
  502. //
  503. // Command syntax: toc_chat_invite <Chat Room ID> <Invite Msg> <buddy1> [<buddy2> [<buddy3> [...]]]
  504. func (s OSCARProxy) ChatInvite(ctx context.Context, me *state.SessionInstance, chatRegistry *ChatRegistry, args []byte) []string {
  505. var chatRoomIDStr, msg string
  506. users, err := parseArgs(args, &chatRoomIDStr, &msg)
  507. if err != nil {
  508. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  509. }
  510. msg = unescape(msg)
  511. chatID, err := strconv.Atoi(chatRoomIDStr)
  512. if err != nil {
  513. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  514. }
  515. roomInfo, found := chatRegistry.LookupRoom(chatID)
  516. if !found {
  517. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.LookupRoom: chat ID `%d` not found", chatID))
  518. }
  519. for _, guest := range users {
  520. if msg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  521. return msg
  522. }
  523. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  524. ChannelID: wire.ICBMChannelRendezvous,
  525. ScreenName: guest,
  526. TLVRestBlock: wire.TLVRestBlock{
  527. TLVList: wire.TLVList{
  528. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  529. Type: wire.ICBMRdvMessagePropose,
  530. Capability: wire.CapChat,
  531. TLVRestBlock: wire.TLVRestBlock{
  532. TLVList: wire.TLVList{
  533. wire.NewTLVBE(wire.ICBMRdvTLVTagsSeqNum, uint16(1)),
  534. wire.NewTLVBE(wire.ICBMRdvTLVTagsInvitation, msg),
  535. wire.NewTLVBE(wire.ICBMRdvTLVTagsInviteMIMECharset, "us-ascii"),
  536. wire.NewTLVBE(wire.ICBMRdvTLVTagsInviteMIMELang, "en"),
  537. wire.NewTLVBE(wire.ICBMRdvTLVTagsSvcData, roomInfo),
  538. },
  539. },
  540. }),
  541. },
  542. },
  543. }
  544. if _, err := s.ICBMService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
  545. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
  546. }
  547. }
  548. return []string{}
  549. }
  550. // ChatJoin handles the toc_chat_join TOC command.
  551. //
  552. // From the TiK documentation:
  553. //
  554. // Join a chat room in the given exchange. Exchange is an integer that
  555. // represents a group of chat rooms. Different exchanges have different
  556. // properties. For example some exchanges might have room replication (ie a
  557. // room never fills up, there are just multiple instances.) and some exchanges
  558. // might have navigational information. Currently, exchange should always be
  559. // 4, however this may change in the future. You will either receive an ERROR
  560. // if the room couldn't be joined or a CHAT_JOIN message. The Chat Room Name
  561. // is case-insensitive and consecutive spaces are removed.
  562. //
  563. // Command syntax: toc_chat_join <Exchange> <Chat Room Name>
  564. func (s OSCARProxy) ChatJoin(
  565. ctx context.Context,
  566. me *state.SessionInstance,
  567. chatRegistry *ChatRegistry,
  568. args []byte,
  569. ) (int, []string) {
  570. var exchangeStr, roomName string
  571. if _, err := parseArgs(args, &exchangeStr, &roomName); err != nil {
  572. return 0, s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  573. }
  574. roomName = unescape(roomName)
  575. // create room or retrieve the room if it already exists
  576. exchange, err := strconv.Atoi(exchangeStr)
  577. if err != nil {
  578. return 0, s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  579. }
  580. if msg, isLimited := s.checkRateLimit(ctx, me, wire.Chat, wire.ChatRoomInfoUpdate); isLimited {
  581. return 0, msg
  582. }
  583. mkRoomReq := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{
  584. Exchange: uint16(exchange),
  585. Cookie: "create",
  586. TLVBlock: wire.TLVBlock{
  587. TLVList: wire.TLVList{
  588. wire.NewTLVBE(wire.ChatRoomTLVRoomName, roomName),
  589. },
  590. },
  591. }
  592. mkRoomReply, err := s.ChatNavService.CreateRoom(ctx, me, wire.SNACFrame{}, mkRoomReq)
  593. if err != nil {
  594. return 0, s.runtimeErr(ctx, fmt.Errorf("ChatNavService.CreateRoom: %w", err))
  595. }
  596. mkRoomReplyBody, ok := mkRoomReply.Body.(wire.SNAC_0x0D_0x09_ChatNavNavInfo)
  597. if !ok {
  598. return 0, s.runtimeErr(
  599. ctx,
  600. fmt.Errorf("chatNavService.CreateRoom: unexpected response type %v", mkRoomReplyBody),
  601. )
  602. }
  603. buf, ok := mkRoomReplyBody.Bytes(wire.ChatNavTLVRoomInfo)
  604. if !ok {
  605. return 0, s.runtimeErr(ctx, errors.New("mkRoomReplyBody.Bytes: missing wire.ChatNavTLVRoomInfo"))
  606. }
  607. inBody := wire.SNAC_0x0E_0x02_ChatRoomInfoUpdate{}
  608. if err := wire.UnmarshalBE(&inBody, bytes.NewReader(buf)); err != nil {
  609. return 0, s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  610. }
  611. if msg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceServiceRequest); isLimited {
  612. return 0, msg
  613. }
  614. svcReqSNAC := wire.SNAC_0x01_0x04_OServiceServiceRequest{
  615. FoodGroup: wire.Chat,
  616. TLVRestBlock: wire.TLVRestBlock{
  617. TLVList: wire.TLVList{
  618. wire.NewTLVBE(0x01, wire.SNAC_0x01_0x04_TLVRoomInfo{
  619. Cookie: inBody.Cookie,
  620. }),
  621. },
  622. },
  623. }
  624. svcReqReply, err := s.OServiceService.ServiceRequest(ctx, wire.BOS, me, wire.SNACFrame{}, svcReqSNAC, config.Listener{})
  625. if err != nil {
  626. return 0, s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.ServiceRequest: %w", err))
  627. }
  628. svcReqReplyBody, ok := svcReqReply.Body.(wire.SNAC_0x01_0x05_OServiceServiceResponse)
  629. if !ok {
  630. return 0, s.runtimeErr(
  631. ctx,
  632. fmt.Errorf("OServiceServiceBOS.ServiceRequest: unexpected response type %v", svcReqReplyBody),
  633. )
  634. }
  635. loginCookie, hasCookie := svcReqReplyBody.Bytes(wire.OServiceTLVTagsLoginCookie)
  636. if !hasCookie {
  637. return 0, s.runtimeErr(ctx, errors.New("svcReqReplyBody.Bytes: missing wire.OServiceTLVTagsLoginCookie"))
  638. }
  639. // todo: naming for cookie: login cookie, server cookie, or auth cookie?
  640. serverCookie, err := s.AuthService.CrackCookie(loginCookie)
  641. if err != nil {
  642. return 0, s.runtimeErr(ctx, fmt.Errorf("AuthService.CrackCookie: %w", err))
  643. }
  644. sessCfg := func(sess *state.Session) {
  645. sess.OnSessionClose(func() {
  646. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
  647. defer cancel()
  648. s.AuthService.SignoutChat(ctx, sess)
  649. })
  650. }
  651. chatSess, err := s.AuthService.RegisterChatSession(ctx, serverCookie, sessCfg)
  652. if err != nil {
  653. return 0, s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterChatSession: %w", err))
  654. }
  655. if msg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceClientOnline); isLimited {
  656. return 0, msg
  657. }
  658. if err := s.OServiceService.ClientOnline(ctx, wire.Chat, wire.SNAC_0x01_0x02_OServiceClientOnline{}, chatSess); err != nil {
  659. return 0, s.runtimeErr(ctx, fmt.Errorf("OServiceServiceChat.ClientOnline: %w", err))
  660. }
  661. roomInfo := wire.ICBMRoomInfo{
  662. Exchange: inBody.Exchange,
  663. Cookie: inBody.Cookie,
  664. Instance: inBody.InstanceNumber,
  665. }
  666. chatID := chatRegistry.Add(roomInfo)
  667. chatRegistry.RegisterSess(chatID, chatSess)
  668. if me.IsTOC2() {
  669. chatSess.SetTOC2(me.SupportsTOC2MsgEnc())
  670. }
  671. return chatID, []string{fmt.Sprintf("CHAT_JOIN:%d:%s", chatID, roomName)}
  672. }
  673. // ChatLeave handles the toc_chat_leave TOC command.
  674. //
  675. // From the TiK documentation:
  676. //
  677. // Leave the chat room.
  678. //
  679. // Command syntax: toc_chat_leave <Chat Room ID>
  680. func (s OSCARProxy) ChatLeave(ctx context.Context, chatRegistry *ChatRegistry, args []byte) []string {
  681. var chatIDStr string
  682. if _, err := parseArgs(args, &chatIDStr); err != nil {
  683. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  684. }
  685. chatID, err := strconv.Atoi(chatIDStr)
  686. if err != nil {
  687. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  688. }
  689. me := chatRegistry.RetrieveSess(chatID)
  690. if me == nil {
  691. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveSess: chat session `%d` not found", chatID))
  692. }
  693. me.CloseInstance() // stop async server SNAC reply handler for this chat room
  694. chatRegistry.RemoveSess(chatID)
  695. return []string{fmt.Sprintf("CHAT_LEFT:%d", chatID)}
  696. }
  697. // ChatSend handles the toc_chat_send TOC command.
  698. //
  699. // From the TiK documentation:
  700. //
  701. // Send a message in a chat room using the chat room id from CHAT_JOIN. Since
  702. // reflection is always on in TOC, you do not need to add the message to your
  703. // chat UI, since you will get a CHAT_IN with the message. Remember to quote
  704. // and encode the message.
  705. //
  706. // Command syntax: toc_chat_send <Chat Room ID> <Message>
  707. func (s OSCARProxy) ChatSend(ctx context.Context, chatRegistry *ChatRegistry, args []byte) []string {
  708. var chatIDStr, msg string
  709. if _, err := parseArgs(args, &chatIDStr, &msg); err != nil {
  710. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  711. }
  712. msg = unescape(msg)
  713. chatID, err := strconv.Atoi(chatIDStr)
  714. if err != nil {
  715. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  716. }
  717. me := chatRegistry.RetrieveSess(chatID)
  718. if me == nil {
  719. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveSess: session for chat ID `%d` not found", chatID))
  720. }
  721. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Chat, wire.ChatChannelMsgToHost); isLimited {
  722. return errMsg
  723. }
  724. block := wire.TLVRestBlock{}
  725. // the order of these TLVs matters for AIM 2.x. if out of order, screen
  726. // names do not appear with each chat message.
  727. block.Append(wire.NewTLVBE(wire.ChatTLVEnableReflectionFlag, uint8(1)))
  728. block.Append(wire.NewTLVBE(wire.ChatTLVSenderInformation, me.Session().TLVUserInfo()))
  729. block.Append(wire.NewTLVBE(wire.ChatTLVPublicWhisperFlag, []byte{}))
  730. block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  731. TLVList: wire.TLVList{
  732. wire.NewTLVBE(wire.ChatTLVMessageInfoText, msg),
  733. },
  734. }))
  735. snac := wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  736. Channel: wire.ICBMChannelMIME,
  737. TLVRestBlock: block,
  738. }
  739. reply, err := s.ChatService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac)
  740. if err != nil {
  741. return s.runtimeErr(ctx, fmt.Errorf("ChatService.ChannelMsgToHost: %w", err))
  742. }
  743. if reply == nil {
  744. return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: missing response "))
  745. }
  746. switch v := reply.Body.(type) {
  747. case wire.SNAC_0x0E_0x06_ChatChannelMsgToClient:
  748. msgInfo, ok := v.Bytes(wire.ChatTLVMessageInfo)
  749. if !ok {
  750. return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: missing wire.ChatTLVMessageInfo"))
  751. }
  752. reflectMsg, err := wire.UnmarshalChatMessageText(msgInfo)
  753. if err != nil {
  754. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalChatMessageText: %w", err))
  755. }
  756. senderInfo, ok := v.Bytes(wire.ChatTLVSenderInformation)
  757. if !ok {
  758. return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: missing wire.ChatTLVSenderInformation"))
  759. }
  760. var userInfo wire.TLVUserInfo
  761. if err := wire.UnmarshalBE(&userInfo, bytes.NewReader(senderInfo)); err != nil {
  762. return s.runtimeErr(ctx, fmt.Errorf("wire.UnmarshalBE: %w", err))
  763. }
  764. if me.SupportsTOC2MsgEnc() {
  765. return []string{fmt.Sprintf("CHAT_IN_ENC:%d:%s:F:A:en:%s", chatID, userInfo.ScreenName, reflectMsg)}
  766. }
  767. return []string{fmt.Sprintf("CHAT_IN:%d:%s:F:%s", chatID, userInfo.ScreenName, reflectMsg)}
  768. default:
  769. return s.runtimeErr(ctx, errors.New("ChatService.ChannelMsgToHost: unexpected response"))
  770. }
  771. }
  772. // ChatWhisper handles the toc_chat_whisper TOC command.
  773. //
  774. // From the TiK documentation:
  775. //
  776. // Send a message in a chat room using the chat room id from CHAT_JOIN.
  777. // This message is directed at only one person. (Currently you DO need to add
  778. // this to your UI.) Remember to quote and encode the message. Chat whispering
  779. // is different from IMs since it is linked to a chat room, and should usually
  780. // be displayed in the chat room UI.
  781. //
  782. // Command syntax: toc_chat_whisper <Chat Room ID> <dst_user> <Message>
  783. func (s OSCARProxy) ChatWhisper(ctx context.Context, chatRegistry *ChatRegistry, args []byte) []string {
  784. var chatIDStr, recip, msg string
  785. if _, err := parseArgs(args, &chatIDStr, &recip, &msg); err != nil {
  786. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  787. }
  788. msg = unescape(msg)
  789. chatID, err := strconv.Atoi(chatIDStr)
  790. if err != nil {
  791. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  792. }
  793. me := chatRegistry.RetrieveSess(chatID)
  794. if me == nil {
  795. return s.runtimeErr(ctx, fmt.Errorf("chatRegistry.RetrieveSess: session for chat ID `%d` not found", chatID))
  796. }
  797. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Chat, wire.ChatChannelMsgToHost); isLimited {
  798. return errMsg
  799. }
  800. block := wire.TLVRestBlock{}
  801. block.Append(wire.NewTLVBE(wire.ChatTLVSenderInformation, me.Session().TLVUserInfo()))
  802. block.Append(wire.NewTLVBE(wire.ChatTLVWhisperToUser, recip))
  803. block.Append(wire.NewTLVBE(wire.ChatTLVMessageInfo, wire.TLVRestBlock{
  804. TLVList: wire.TLVList{
  805. wire.NewTLVBE(wire.ChatTLVMessageInfoText, msg),
  806. },
  807. }))
  808. snac := wire.SNAC_0x0E_0x05_ChatChannelMsgToHost{
  809. Channel: wire.ICBMChannelMIME,
  810. TLVRestBlock: block,
  811. }
  812. if _, err = s.ChatService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
  813. return s.runtimeErr(ctx, fmt.Errorf("ChatService.ChannelMsgToHost: %w", err))
  814. }
  815. return []string{}
  816. }
  817. // Evil handles the toc_evil TOC command.
  818. //
  819. // From the TiK documentation:
  820. //
  821. // Evil/Warn someone else. The 2nd argument is either the string "norm" for a
  822. // normal warning, or "anon" for an anonymous warning. You can only evil
  823. // people who have recently sent you ims. The higher someones evil level, the
  824. // slower they can send message.
  825. //
  826. // An ERROR message will be sent back to the client if the warning was unsuccessful.
  827. //
  828. // Command syntax: toc_evil <User> <norm|anon>
  829. func (s OSCARProxy) Evil(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  830. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMEvilRequest); isLimited {
  831. return errMsg
  832. }
  833. var user, scope string
  834. if _, err := parseArgs(args, &user, &scope); err != nil {
  835. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  836. }
  837. snac := wire.SNAC_0x04_0x08_ICBMEvilRequest{
  838. ScreenName: user,
  839. }
  840. switch scope {
  841. case "anon":
  842. snac.SendAs = 1
  843. case "norm":
  844. snac.SendAs = 0
  845. default:
  846. return s.runtimeErr(ctx, fmt.Errorf("incorrect warning type `%s`. allowed values: anon, norm", scope))
  847. }
  848. response, err := s.ICBMService.EvilRequest(ctx, me, wire.SNACFrame{}, snac)
  849. if err != nil {
  850. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.EvilRequest: %w", err))
  851. }
  852. switch v := response.Body.(type) {
  853. case wire.SNAC_0x04_0x09_ICBMEvilReply:
  854. return []string{}
  855. case wire.SNACError:
  856. s.Logger.InfoContext(ctx, "unable to warn user", "code", v.Code)
  857. return []string{"ERROR:" + wire.TOCErrorGeneralWarningUserNotAvailable}
  858. default:
  859. return s.runtimeErr(ctx, errors.New("unexpected response"))
  860. }
  861. }
  862. // FormatNickname handles the toc_format_nickname TOC command.
  863. //
  864. // From the TiK documentation:
  865. //
  866. // Reformat a user's nickname. An ADMIN_NICK_STATUS or ERROR message will be
  867. // sent back to the client.
  868. //
  869. // From the BizTOCSock documentation:
  870. //
  871. // [NICK is also sent with ADMIN_NICK_STATUS. This gets called [...] whenever
  872. // you do a format change.
  873. //
  874. // Command syntax: toc_format_nickname <new_format>
  875. func (s OSCARProxy) FormatNickname(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  876. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Admin, wire.AdminInfoChangeRequest); isLimited {
  877. return errMsg
  878. }
  879. var newFormat string
  880. if _, err := parseArgs(args, &newFormat); err != nil {
  881. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  882. }
  883. // remove curly braces added by TiK
  884. newFormat = strings.Trim(newFormat, "{}")
  885. reqSNAC := wire.SNAC_0x07_0x04_AdminInfoChangeRequest{
  886. TLVRestBlock: wire.TLVRestBlock{
  887. TLVList: wire.TLVList{
  888. wire.NewTLVBE(wire.AdminTLVScreenNameFormatted, newFormat),
  889. },
  890. },
  891. }
  892. reply, err := s.AdminService.InfoChangeRequest(ctx, me, wire.SNACFrame{}, reqSNAC)
  893. if err != nil {
  894. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: %w", err))
  895. }
  896. replyBody, ok := reply.Body.(wire.SNAC_0x07_0x05_AdminChangeReply)
  897. if !ok {
  898. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: unexpected response type %v", replyBody))
  899. }
  900. code, ok := replyBody.Uint16BE(wire.AdminTLVErrorCode)
  901. if ok {
  902. switch code {
  903. case wire.AdminInfoErrorInvalidNickNameLength, wire.AdminInfoErrorInvalidNickName:
  904. return []string{"ERROR:" + wire.TOCErrorAdminInvalidInput}
  905. default:
  906. return []string{"ERROR:" + wire.TOCErrorAdminProcessingRequest}
  907. }
  908. }
  909. val, hasVal := replyBody.TLVBlock.String(wire.AdminTLVScreenNameFormatted)
  910. if !hasVal {
  911. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: missing AdminTLVScreenNameFormatted %v", replyBody))
  912. }
  913. return []string{"ADMIN_NICK_STATUS:0", "NICK:" + val}
  914. }
  915. // GetDirSearchURL handles the toc_dir_search TOC command.
  916. //
  917. // From the TiK documentation:
  918. //
  919. // Perform a search of the Oscar Directory, using colon separated fields as in:
  920. //
  921. // "first name":"middle name":"last name":"maiden name":"city":"state":"country":"email"
  922. //
  923. // You can search by keyword by setting search terms in the 11th position (this
  924. // feature is not in the TiK docs but is present in the code):
  925. //
  926. // ::::::::::"search kw"
  927. //
  928. // Returns either a GOTO_URL or ERROR msg.
  929. //
  930. // Command syntax: toc_dir_search <info information>
  931. func (s OSCARProxy) GetDirSearchURL(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  932. if status := me.Session().EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
  933. return []string{rateLimitExceededErr}
  934. }
  935. var info string
  936. if _, err := parseArgs(args, &info); err != nil {
  937. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  938. }
  939. info = unescape(info)
  940. params := strings.Split(info, ":")
  941. labels := []string{
  942. "first_name",
  943. "middle_name",
  944. "last_name",
  945. "maiden_name",
  946. "city",
  947. "state",
  948. "country",
  949. "email",
  950. "nop", // unused placeholder
  951. "nop",
  952. "keyword",
  953. }
  954. // map labels to param values at their corresponding positions
  955. p := url.Values{}
  956. for i, param := range params {
  957. if i >= len(labels) {
  958. break
  959. }
  960. if param != "" {
  961. p.Add(labels[i], strings.Trim(param, "\""))
  962. }
  963. }
  964. if len(p) == 0 {
  965. return s.runtimeErr(ctx, errors.New("no search fields found"))
  966. }
  967. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  968. if err != nil {
  969. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  970. }
  971. p.Add("cookie", cookie)
  972. return []string{fmt.Sprintf("GOTO_URL:search results:dir_search?%s", p.Encode())}
  973. }
  974. // GetDirURL handles the toc_get_dir TOC command.
  975. //
  976. // From the TiK documentation:
  977. //
  978. // Gets a user's dir info a GOTO_URL or ERROR message will be sent back to the client.
  979. //
  980. // Command syntax: toc_get_dir <username>
  981. func (s OSCARProxy) GetDirURL(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  982. if status := me.Session().EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
  983. return []string{rateLimitExceededErr}
  984. }
  985. var user string
  986. if _, err := parseArgs(args, &user); err != nil {
  987. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  988. }
  989. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  990. if err != nil {
  991. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  992. }
  993. p := url.Values{}
  994. p.Add("cookie", cookie)
  995. p.Add("user", user)
  996. return []string{fmt.Sprintf("GOTO_URL:directory info:dir_info?%s", p.Encode())}
  997. }
  998. // GetInfoURL handles the toc_get_info TOC command.
  999. //
  1000. // From the TiK documentation:
  1001. //
  1002. // Gets a user's info a GOTO_URL or ERROR message will be sent back to the client.
  1003. //
  1004. // Command syntax: toc_get_info <username>
  1005. func (s OSCARProxy) GetInfoURL(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1006. if status := me.Session().EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
  1007. return []string{rateLimitExceededErr}
  1008. }
  1009. var user string
  1010. if _, err := parseArgs(args, &user); err != nil {
  1011. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1012. }
  1013. cookie, err := s.newHTTPAuthToken(me.IdentScreenName())
  1014. if err != nil {
  1015. return s.runtimeErr(ctx, fmt.Errorf("newHTTPAuthToken: %w", err))
  1016. }
  1017. p := url.Values{}
  1018. p.Add("cookie", cookie)
  1019. p.Add("from", me.IdentScreenName().String())
  1020. p.Add("user", user)
  1021. return []string{fmt.Sprintf("GOTO_URL:profile:info?%s", p.Encode())}
  1022. }
  1023. // GetStatus handles the toc_get_status TOC command.
  1024. //
  1025. // From the BlueTOC documentation:
  1026. //
  1027. // This useful command wasn't ever really documented. It returns either an
  1028. // UPDATE_BUDDY message or an ERROR message depending on whether or not the
  1029. // guy appears to be online. The command is used in both TOC1 and TOC2 with
  1030. // the same syntax.
  1031. //
  1032. // Command syntax: toc_get_status <screenname>
  1033. func (s OSCARProxy) GetStatus(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1034. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateUserInfoQuery); isLimited {
  1035. return errMsg
  1036. }
  1037. var them string
  1038. if _, err := parseArgs(args, &them); err != nil {
  1039. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1040. }
  1041. inBody := wire.SNAC_0x02_0x05_LocateUserInfoQuery{
  1042. ScreenName: them,
  1043. }
  1044. info, err := s.LocateService.UserInfoQuery(ctx, me, wire.SNACFrame{}, inBody)
  1045. if err != nil {
  1046. return s.runtimeErr(ctx, fmt.Errorf("LocateService.UserInfoQuery: %w", err))
  1047. }
  1048. switch v := info.Body.(type) {
  1049. case wire.SNACError:
  1050. if v.Code == wire.ErrorCodeNotLoggedOn {
  1051. return []string{fmt.Sprintf("ERROR:%s:%s", wire.TOCErrorGeneralUserNotAvailable, them)}
  1052. } else {
  1053. return s.runtimeErr(ctx, fmt.Errorf("LocateService.UserInfoQuery error code: %d", v.Code))
  1054. }
  1055. case wire.SNAC_0x02_0x06_LocateUserInfoReply:
  1056. return []string{userInfoToUpdateBuddy(v.TLVUserInfo, me)}
  1057. default:
  1058. return s.runtimeErr(ctx, fmt.Errorf("AdminService.InfoChangeRequest: unexpected response type %v", v))
  1059. }
  1060. }
  1061. // InitDone handles the toc_init_done TOC command.
  1062. //
  1063. // From the TiK documentation:
  1064. //
  1065. // Tells TOC that we are ready to go online. TOC clients should first send TOC
  1066. // the buddy list and any permit/deny lists. However, toc_init_done must be
  1067. // called within 30 seconds after toc_signon, or the connection will be
  1068. // dropped. Remember, it can't be called until after the SIGN_ON message is
  1069. // received. Calling this before or multiple times after a SIGN_ON will cause
  1070. // the connection to be dropped.
  1071. //
  1072. // Note: The business logic described in the last 3 sentences are not yet
  1073. // implemented.
  1074. //
  1075. // Command syntax: toc_init_done
  1076. func (s OSCARProxy) InitDone(ctx context.Context, instance *state.SessionInstance) []string {
  1077. err := s.FeedbagManager.UseFeedbag(ctx, instance.IdentScreenName())
  1078. if err != nil {
  1079. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.UseFeedbag: %w", err))
  1080. }
  1081. if errMsg, isLimited := s.checkRateLimit(ctx, instance, wire.OService, wire.OServiceClientOnline); isLimited {
  1082. return errMsg
  1083. }
  1084. if err := s.OServiceService.ClientOnline(ctx, wire.BOS, wire.SNAC_0x01_0x02_OServiceClientOnline{}, instance); err != nil {
  1085. return s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.ClientOnliney: %w", err))
  1086. }
  1087. return []string{}
  1088. }
  1089. // RemoveBuddy handles the toc_remove_buddy TOC command.
  1090. //
  1091. // From the TiK documentation:
  1092. //
  1093. // Remove buddies from your buddy list. This does not change your saved config.
  1094. //
  1095. // Command syntax: toc_remove_buddy <Buddy User 1> [<Buddy User2> [<Buddy User 3> [...]]]
  1096. func (s OSCARProxy) RemoveBuddy(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1097. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Buddy, wire.BuddyDelBuddies); isLimited {
  1098. return errMsg
  1099. }
  1100. users, err := parseArgs(args)
  1101. if err != nil {
  1102. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1103. }
  1104. snac := wire.SNAC_0x03_0x05_BuddyDelBuddies{}
  1105. for _, sn := range users {
  1106. snac.Buddies = append(snac.Buddies, struct {
  1107. ScreenName string `oscar:"len_prefix=uint8"`
  1108. }{ScreenName: sn})
  1109. }
  1110. if err := s.BuddyService.DelBuddies(ctx, me, snac); err != nil {
  1111. return s.runtimeErr(ctx, fmt.Errorf("BuddyService.DelBuddies: %w", err))
  1112. }
  1113. return []string{}
  1114. }
  1115. // RvousAccept handles the toc_rvous_accept TOC command.
  1116. //
  1117. // From the TiK documentation:
  1118. //
  1119. // Accept a rendezvous proposal from the user <nick>. <cookie> is the cookie
  1120. // from the RVOUS_PROPOSE message. <service> is the UUID the proposal was for.
  1121. // <tlvlist> contains a list of tlv tags followed by base64 encoded values.
  1122. //
  1123. // Note: This method does not actually process the TLV list param, as it's not
  1124. // passed in the TiK client, the reference implementation.
  1125. //
  1126. // Command syntax: toc_rvous_accept <nick> <cookie> <service>
  1127. func (s OSCARProxy) RvousAccept(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1128. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  1129. return errMsg
  1130. }
  1131. var nick, cookie, service string
  1132. if _, err := parseArgs(args, &nick, &cookie, &service); err != nil {
  1133. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1134. }
  1135. cbytes, err := base64.StdEncoding.DecodeString(cookie)
  1136. if err != nil {
  1137. return s.runtimeErr(ctx, fmt.Errorf("base64.Decode: %w", err))
  1138. }
  1139. var arr [8]byte
  1140. copy(arr[:], cbytes) // copy slice into array
  1141. svcUUID, err := uuid.Parse(service)
  1142. if err != nil {
  1143. return s.runtimeErr(ctx, fmt.Errorf("uuid.Parse: %w", err))
  1144. }
  1145. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  1146. ChannelID: wire.ICBMChannelRendezvous,
  1147. ScreenName: nick,
  1148. TLVRestBlock: wire.TLVRestBlock{
  1149. TLVList: wire.TLVList{
  1150. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  1151. Type: wire.ICBMRdvMessageAccept,
  1152. Cookie: arr,
  1153. Capability: svcUUID,
  1154. }),
  1155. },
  1156. },
  1157. }
  1158. if _, err = s.ICBMService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
  1159. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
  1160. }
  1161. return []string{}
  1162. }
  1163. // RvousCancel handles the toc_rvous_cancel TOC command.
  1164. //
  1165. // From the TiK documentation:
  1166. //
  1167. // Cancel a rendezvous proposal from the user <nick>. <cookie> is the cookie
  1168. // from the RVOUS_PROPOSE message. <service> is the UUID the proposal was for.
  1169. // <tlvlist> contains a list of tlv tags followed by base64 encoded values.
  1170. //
  1171. // Note: This method does not actually process the TLV list param, as it's not
  1172. // passed in the TiK client, the reference implementation.
  1173. //
  1174. // Command syntax: toc_rvous_cancel <nick> <cookie> <service>
  1175. func (s OSCARProxy) RvousCancel(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1176. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  1177. return errMsg
  1178. }
  1179. var nick, cookie, service string
  1180. if _, err := parseArgs(args, &nick, &cookie, &service); err != nil {
  1181. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1182. }
  1183. cbytes, err := base64.StdEncoding.DecodeString(cookie)
  1184. if err != nil {
  1185. return s.runtimeErr(ctx, fmt.Errorf("base64.Decode: %w", err))
  1186. }
  1187. var arr [8]byte
  1188. copy(arr[:], cbytes) // copy slice into array
  1189. svcUUID, err := uuid.Parse(service)
  1190. if err != nil {
  1191. return s.runtimeErr(ctx, fmt.Errorf("uuid.Parse: %w", err))
  1192. }
  1193. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  1194. ChannelID: wire.ICBMChannelRendezvous,
  1195. ScreenName: nick,
  1196. TLVRestBlock: wire.TLVRestBlock{
  1197. TLVList: wire.TLVList{
  1198. wire.NewTLVBE(wire.ICBMTLVData, wire.ICBMCh2Fragment{
  1199. Type: wire.ICBMRdvMessageCancel,
  1200. Cookie: arr,
  1201. Capability: svcUUID,
  1202. TLVRestBlock: wire.TLVRestBlock{
  1203. TLVList: wire.TLVList{
  1204. wire.NewTLVBE(wire.ICBMRdvTLVTagsCancelReason, wire.ICBMRdvCancelReasonsUserCancel),
  1205. },
  1206. },
  1207. }),
  1208. },
  1209. },
  1210. }
  1211. if _, err = s.ICBMService.ChannelMsgToHost(ctx, me, wire.SNACFrame{}, snac); err != nil {
  1212. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
  1213. }
  1214. return []string{}
  1215. }
  1216. // SetPDMode handles the toc2_set_pdmode TOC2 command.
  1217. //
  1218. // From the BlueTOC documentation:
  1219. //
  1220. // Set permit/deny mode. Value: 1 = Allow all (default), 2 = Block all,
  1221. // 3 = Allow "permit group" only, 4 = Block "deny group" only, 5 = Allow buddy list only.
  1222. //
  1223. // Command syntax: toc2_set_pdmode <mode>
  1224. func (s OSCARProxy) SetPDMode(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1225. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.FeedbagInsertItem); isLimited {
  1226. return errMsg
  1227. }
  1228. var pdModeStr string
  1229. if _, err := parseArgs(args, &pdModeStr); err != nil {
  1230. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1231. }
  1232. mode, err := strconv.Atoi(pdModeStr)
  1233. if err != nil {
  1234. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  1235. }
  1236. if mode < 1 || mode > 5 {
  1237. return s.runtimeErr(ctx, errors.New("invalid pd mode specified"))
  1238. }
  1239. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1240. if err != nil {
  1241. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1242. }
  1243. fl := newFeedbagList(fb, s.RandIntn)
  1244. fl.SetMode(uint8(mode))
  1245. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1246. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  1247. if _, err := s.FeedbagService.UpsertItem(ctx, me, frame, pending); err != nil {
  1248. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.UpsertItem: %w", err))
  1249. }
  1250. }
  1251. return []string{}
  1252. }
  1253. // NewGroup handles the toc2_new_group TOC2 command.
  1254. //
  1255. // From the TOC2 docs by Jeffrey Rosen:
  1256. //
  1257. // This is an entirely new command that allows you to add groups. These should be
  1258. // quoted and you can't add more than one per command.
  1259. //
  1260. // Command syntax: toc2_new_group <group>
  1261. func (s OSCARProxy) NewGroup(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1262. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Feedbag, wire.FeedbagInsertItem); isLimited {
  1263. return errMsg
  1264. }
  1265. var groupName string
  1266. if _, err := parseArgs(args, &groupName); err != nil {
  1267. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1268. }
  1269. groupName = unescape(groupName)
  1270. if groupName == "" {
  1271. return s.runtimeErr(ctx, fmt.Errorf("empty group name"))
  1272. }
  1273. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1274. if err != nil {
  1275. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1276. }
  1277. fl := newFeedbagList(fb, s.RandIntn)
  1278. fl.AddGroup(groupName)
  1279. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1280. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  1281. if _, err := s.FeedbagService.UpsertItem(ctx, me, frame, pending); err != nil {
  1282. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.UpsertItem: %w", err))
  1283. }
  1284. }
  1285. return []string{}
  1286. }
  1287. // DelGroup handles the toc2_del_group TOC2 command.
  1288. //
  1289. // From the TOC2 docs by Jeffrey Rosen:
  1290. //
  1291. // Delete a group.
  1292. //
  1293. // Command syntax: toc2_del_group <group>
  1294. func (s OSCARProxy) DelGroup(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1295. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Feedbag, wire.FeedbagDeleteItem); isLimited {
  1296. return errMsg
  1297. }
  1298. var groupName string
  1299. if _, err := parseArgs(args, &groupName); err != nil {
  1300. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1301. }
  1302. groupName = unescape(groupName)
  1303. if groupName == "" {
  1304. return s.runtimeErr(ctx, fmt.Errorf("empty group name"))
  1305. }
  1306. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1307. if err != nil {
  1308. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1309. }
  1310. fl := newFeedbagList(fb, s.RandIntn)
  1311. fl.DeleteGroup(groupName)
  1312. if pending := fl.PendingDeletes(); len(pending) > 0 {
  1313. deleteItem := wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1314. Items: pending,
  1315. }
  1316. if _, err := s.FeedbagService.DeleteItem(ctx, me, wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem}, deleteItem); err != nil {
  1317. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.DeleteItem: %w", err))
  1318. }
  1319. } else {
  1320. return s.runtimeErr(ctx, fmt.Errorf("group not found: %s", groupName))
  1321. }
  1322. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1323. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  1324. if _, err := s.FeedbagService.UpsertItem(ctx, me, frame, pending); err != nil {
  1325. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.UpsertItem: %w", err))
  1326. }
  1327. }
  1328. return []string{}
  1329. }
  1330. // NewBuddies handles the toc2_new_buddies TOC2 command.
  1331. //
  1332. // From the TOC2 docs by Jeffrey Rosen:
  1333. //
  1334. // In TOC2.0, you must add buddies in "config format". If you sent that with the
  1335. // toc2_new_buddies command, you would add the three buddies (buddytest,
  1336. // buddytest2, and buddytest3) into the group "test". Note that if the group
  1337. // doesn't already exist, it will be created.
  1338. //
  1339. // Config format: {g:group<lf>b:buddy1<lf>b:buddy2<lf>}
  1340. // Where <lf> is a linefeed character (ASCII 10).
  1341. //
  1342. // Command syntax: toc2_new_buddies <config format>
  1343. func (s OSCARProxy) NewBuddies(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1344. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Feedbag, wire.FeedbagInsertItem); isLimited {
  1345. return errMsg
  1346. }
  1347. // Parse config string (handle quotes, unescape)
  1348. configStr := string(args)
  1349. configStr = strings.Trim(configStr, "'\" ")
  1350. configStr = unescape(configStr)
  1351. if configStr == "" {
  1352. return s.runtimeErr(ctx, fmt.Errorf("empty config"))
  1353. }
  1354. // Parse config format
  1355. groups, err := parseTOC2Config(configStr)
  1356. if err != nil {
  1357. return s.runtimeErr(ctx, fmt.Errorf("parseTOC2Config: %w", err))
  1358. }
  1359. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1360. if err != nil {
  1361. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1362. }
  1363. fl := newFeedbagList(fb, s.RandIntn)
  1364. var replies []string
  1365. // Process each group in config (deterministic order so tests and root Order are stable)
  1366. groupNames := make([]string, 0, len(groups))
  1367. for k := range groups {
  1368. groupNames = append(groupNames, k)
  1369. }
  1370. sort.Strings(groupNames)
  1371. for _, groupName := range groupNames {
  1372. buddies := groups[groupName]
  1373. fl.AddGroup(groupName)
  1374. for _, buddy := range buddies {
  1375. // Parse buddy entry (may contain alias/note: b:buddy:alias:::::note)
  1376. parts := strings.Split(buddy, ":")
  1377. buddyName := parts[0]
  1378. if buddyName == "" {
  1379. continue
  1380. }
  1381. var alias, note string
  1382. if len(parts) > 1 {
  1383. alias = parts[1]
  1384. }
  1385. if len(parts) > 6 {
  1386. note = parts[6]
  1387. }
  1388. inserted, err := fl.AddBuddy(groupName, buddyName, alias, note)
  1389. if err != nil {
  1390. return s.runtimeErr(ctx, fmt.Errorf("fl.AddBuddy: %w", err))
  1391. }
  1392. if inserted {
  1393. replies = append(replies, fmt.Sprintf("NEW_BUDDY_REPLY2:%s:added", buddyName))
  1394. }
  1395. }
  1396. }
  1397. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1398. buddyItems := make(map[uint16][]wire.FeedbagItem)
  1399. for _, item := range pending {
  1400. if item.ClassID == wire.FeedbagClassIdBuddy {
  1401. if _, ok := buddyItems[item.GroupID]; !ok {
  1402. buddyItems[item.GroupID] = nil
  1403. }
  1404. buddyItems[item.GroupID] = append(buddyItems[item.GroupID], item)
  1405. }
  1406. }
  1407. if len(buddyItems) == 0 {
  1408. return replies
  1409. }
  1410. for _, item := range pending {
  1411. if item.ClassID == wire.FeedbagClassIdGroup {
  1412. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem}
  1413. if _, err := s.FeedbagService.UpsertItem(ctx, me, frame, []wire.FeedbagItem{item}); err != nil {
  1414. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.UpsertItem: %w", err))
  1415. }
  1416. }
  1417. }
  1418. for _, buddies := range buddyItems {
  1419. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  1420. if _, err := s.FeedbagService.UpsertItem(ctx, me, frame, buddies); err != nil {
  1421. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.UpsertItem: %w", err))
  1422. }
  1423. }
  1424. }
  1425. return replies
  1426. }
  1427. // RemoveBuddy2 handles the toc2_remove_buddy command.
  1428. //
  1429. // From the TOC2 docs by Jeffrey Rosen:
  1430. //
  1431. // You can remove multiple names in the same group using the syntax <screenname> <screenname> <group>.
  1432. //
  1433. // Command syntax: toc2_remove_buddy <screenname> [screenname] ... [screenname] <group>
  1434. func (s OSCARProxy) RemoveBuddy2(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1435. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Feedbag, wire.FeedbagDeleteItem); isLimited {
  1436. return errMsg
  1437. }
  1438. params, err := parseArgs(args)
  1439. if err != nil {
  1440. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1441. }
  1442. if len(params) < 2 {
  1443. return s.runtimeErr(ctx, fmt.Errorf("missing params: need at least one screenname and group"))
  1444. }
  1445. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1446. if err != nil {
  1447. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1448. }
  1449. fl := newFeedbagList(fb, s.RandIntn)
  1450. groupName := params[len(params)-1]
  1451. screenNames := params[:len(params)-1]
  1452. for _, buddyName := range screenNames {
  1453. if err := fl.DeleteBuddy(groupName, buddyName); err != nil {
  1454. return s.runtimeErr(ctx, err)
  1455. }
  1456. }
  1457. // ensure to delete buddies before groups to ensure they correctly propagate
  1458. // to concurrent sessions
  1459. if pending := fl.PendingDeletes(); len(pending) > 0 {
  1460. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem}
  1461. deleteItem := wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1462. Items: pending,
  1463. }
  1464. if _, err := s.FeedbagService.DeleteItem(ctx, me, frame, deleteItem); err != nil {
  1465. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.DeleteItem: %w", err))
  1466. }
  1467. }
  1468. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1469. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagUpdateItem}
  1470. if _, err := s.FeedbagService.UpsertItem(ctx, me, frame, pending); err != nil {
  1471. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.UpsertItem: %w", err))
  1472. }
  1473. }
  1474. return []string{}
  1475. }
  1476. // AddPermit2 handles the toc2_add_permit TOC2 command.
  1477. //
  1478. // From the TOC2 docs by Jeffrey Rosen:
  1479. //
  1480. // Add user(s) to permit list. <screenname> should be normalized and you can add
  1481. // multiple people at a time by separating the screennames with a space.
  1482. //
  1483. // Command syntax: toc2_add_permit <screenname> [screenname] ...
  1484. func (s OSCARProxy) AddPermit2(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1485. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Feedbag, wire.FeedbagInsertItem); isLimited {
  1486. return errMsg
  1487. }
  1488. screenNames, err := parseArgs(args)
  1489. if err != nil {
  1490. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1491. }
  1492. if len(screenNames) == 0 {
  1493. return s.runtimeErr(ctx, fmt.Errorf("no screennames provided"))
  1494. }
  1495. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1496. if err != nil {
  1497. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1498. }
  1499. fl := newFeedbagList(fb, s.RandIntn)
  1500. for _, sn := range screenNames {
  1501. fl.PermitUser(sn)
  1502. }
  1503. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1504. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  1505. if _, err := s.FeedbagService.UpsertItem(ctx, me, frame, pending); err != nil {
  1506. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.UpsertItem: %w", err))
  1507. }
  1508. }
  1509. return []string{}
  1510. }
  1511. // RemovePermit2 handles the toc2_remove_permit TOC2 command.
  1512. //
  1513. // From the TOC2 docs by Jeffrey Rosen:
  1514. //
  1515. // Remove user(s) from permit list. <screenname> should be normalized and you can
  1516. // remove multiple people at a time by separating the screennames with a space.
  1517. //
  1518. // Command syntax: toc2_remove_permit <screenname> [screenname] ...
  1519. func (s OSCARProxy) RemovePermit2(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1520. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Feedbag, wire.FeedbagDeleteItem); isLimited {
  1521. return errMsg
  1522. }
  1523. screenNames, err := parseArgs(args)
  1524. if err != nil {
  1525. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1526. }
  1527. if len(screenNames) == 0 {
  1528. return s.runtimeErr(ctx, fmt.Errorf("no screennames provided"))
  1529. }
  1530. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1531. if err != nil {
  1532. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1533. }
  1534. fl := newFeedbagList(fb, s.RandIntn)
  1535. for _, sn := range screenNames {
  1536. fl.DeletePermit(sn)
  1537. }
  1538. if pending := fl.PendingDeletes(); len(pending) > 0 {
  1539. deleteItem := wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1540. Items: pending,
  1541. }
  1542. if _, err := s.FeedbagService.DeleteItem(ctx, me, wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem}, deleteItem); err != nil {
  1543. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.DeleteItem: %w", err))
  1544. }
  1545. }
  1546. return []string{}
  1547. }
  1548. // AddDeny2 handles the toc2_add_deny TOC2 command.
  1549. //
  1550. // From the TOC2 docs by Jeffrey Rosen:
  1551. //
  1552. // Add user(s) to deny list. <screenname> should be normalized and you can add
  1553. // multiple people at a time by separating the screennames with a space.
  1554. //
  1555. // Command syntax: toc2_add_deny <screenname> [screenname] ...
  1556. func (s OSCARProxy) AddDeny2(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1557. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Feedbag, wire.FeedbagInsertItem); isLimited {
  1558. return errMsg
  1559. }
  1560. screenNames, err := parseArgs(args)
  1561. if err != nil {
  1562. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1563. }
  1564. if len(screenNames) == 0 {
  1565. return s.runtimeErr(ctx, fmt.Errorf("no screennames provided"))
  1566. }
  1567. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1568. if err != nil {
  1569. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1570. }
  1571. fl := newFeedbagList(fb, s.RandIntn)
  1572. for _, sn := range screenNames {
  1573. fl.DenyUser(sn)
  1574. }
  1575. if pending := fl.PendingUpdates(); len(pending) > 0 {
  1576. frame := wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagInsertItem}
  1577. if _, err := s.FeedbagService.UpsertItem(ctx, me, frame, pending); err != nil {
  1578. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.UpsertItem: %w", err))
  1579. }
  1580. }
  1581. return []string{}
  1582. }
  1583. // RemoveDeny2 handles the toc2_remove_deny TOC2 command.
  1584. //
  1585. // From the TOC2 docs by Jeffrey Rosen:
  1586. //
  1587. // Remove user(s) from deny list. <screenname> should be normalized and you can
  1588. // remove multiple people at a time by separating the screennames with a space.
  1589. //
  1590. // Command syntax: toc2_remove_deny <screenname> [screenname] ...
  1591. func (s OSCARProxy) RemoveDeny2(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1592. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Feedbag, wire.FeedbagDeleteItem); isLimited {
  1593. return errMsg
  1594. }
  1595. screenNames, err := parseArgs(args)
  1596. if err != nil {
  1597. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1598. }
  1599. if len(screenNames) == 0 {
  1600. return s.runtimeErr(ctx, fmt.Errorf("no screennames provided"))
  1601. }
  1602. fb, err := s.FeedbagManager.Feedbag(ctx, me.IdentScreenName())
  1603. if err != nil {
  1604. return s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  1605. }
  1606. fl := newFeedbagList(fb, s.RandIntn)
  1607. for _, sn := range screenNames {
  1608. fl.DeleteDeny(sn)
  1609. }
  1610. if pending := fl.PendingDeletes(); len(pending) > 0 {
  1611. deleteItem := wire.SNAC_0x13_0x0A_FeedbagDeleteItem{
  1612. Items: pending,
  1613. }
  1614. if _, err := s.FeedbagService.DeleteItem(ctx, me, wire.SNACFrame{FoodGroup: wire.Feedbag, SubGroup: wire.FeedbagDeleteItem}, deleteItem); err != nil {
  1615. return s.runtimeErr(ctx, fmt.Errorf("FeedbagService.DeleteItem: %w", err))
  1616. }
  1617. }
  1618. return []string{}
  1619. }
  1620. // SendIMEnc handles the toc2_send_im_enc TOC2 command.
  1621. //
  1622. // From the BlueTOC source code:
  1623. //
  1624. // Sends an encoded instant message to a user
  1625. // Internally, this uses the "TOC3" encoded message format
  1626. // This encoded message version supports a few more variables as well as encoding
  1627. //
  1628. // Command syntax: toc2_send_im_enc <Destination user> "F" <Encoding> <Language> <Message> [auto]
  1629. func (s OSCARProxy) SendIMEnc(ctx context.Context, sender *state.SessionInstance, args []byte) []string {
  1630. if msg, isLimited := s.checkRateLimit(ctx, sender, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  1631. return msg
  1632. }
  1633. var recip, unknown, enc, lang, msg string
  1634. autoReply, err := parseArgs(args, &recip, &unknown, &enc, &lang, &msg)
  1635. if err != nil {
  1636. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1637. }
  1638. return s.sendIm(ctx, sender, msg, recip, autoReply)
  1639. }
  1640. // SendIM handles the toc_send_im and toc2_send_im TOC commands.
  1641. //
  1642. // From the TiK documentation (TOC1):
  1643. //
  1644. // Send a message to a remote user. Remember to quote and encode the message.
  1645. // If the optional string "auto" is the last argument, then the auto response
  1646. // flag will be turned on for the IM.
  1647. //
  1648. // TOC2 uses the same syntax as TOC1 (BlueTOC documentation).
  1649. //
  1650. // Command syntax: toc_send_im <Destination User> <Message> [auto]
  1651. // Command syntax: toc2_send_im <user> <message> [auto]
  1652. func (s OSCARProxy) SendIM(ctx context.Context, sender *state.SessionInstance, args []byte) []string {
  1653. if msg, isLimited := s.checkRateLimit(ctx, sender, wire.ICBM, wire.ICBMChannelMsgToHost); isLimited {
  1654. return msg
  1655. }
  1656. var recip, msg string
  1657. autoReply, err := parseArgs(args, &recip, &msg)
  1658. if err != nil {
  1659. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1660. }
  1661. return s.sendIm(ctx, sender, msg, recip, autoReply)
  1662. }
  1663. func (s OSCARProxy) sendIm(ctx context.Context, sender *state.SessionInstance, msg string, recip string, autoReply []string) []string {
  1664. frags, err := wire.ICBMFragmentList(unescape(msg))
  1665. if err != nil {
  1666. return s.runtimeErr(ctx, fmt.Errorf("wire.ICBMFragmentList: %w", err))
  1667. }
  1668. snac := wire.SNAC_0x04_0x06_ICBMChannelMsgToHost{
  1669. ChannelID: wire.ICBMChannelIM,
  1670. ScreenName: recip,
  1671. TLVRestBlock: wire.TLVRestBlock{
  1672. TLVList: wire.TLVList{
  1673. wire.NewTLVBE(wire.ICBMTLVAOLIMData, frags),
  1674. },
  1675. },
  1676. }
  1677. if len(autoReply) > 0 && autoReply[0] == "auto" {
  1678. snac.Append(wire.NewTLVBE(wire.ICBMTLVAutoResponse, []byte{}))
  1679. }
  1680. // send message and ignore response since there is no TOC error code to
  1681. // handle errors such as "user is offline", etc.
  1682. _, err = s.ICBMService.ChannelMsgToHost(ctx, sender, wire.SNACFrame{}, snac)
  1683. if err != nil {
  1684. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ChannelMsgToHost: %w", err))
  1685. }
  1686. return []string{}
  1687. }
  1688. // SendClientEvent handles the toc2_client_event TOC2 command.
  1689. //
  1690. // From the BlueTOC documentation:
  1691. //
  1692. // This is used to send a typing notification. Typing status: 0 for no activity,
  1693. // 1 for typing paused, 2 for currently typing.
  1694. //
  1695. // Command syntax: toc2_client_event <user> <typing status>
  1696. func (s OSCARProxy) SendClientEvent(ctx context.Context, sender *state.SessionInstance, args []byte) []string {
  1697. if msg, isLimited := s.checkRateLimit(ctx, sender, wire.ICBM, wire.ICBMClientEvent); isLimited {
  1698. return msg
  1699. }
  1700. var user, statusStr string
  1701. if _, err := parseArgs(args, &user, &statusStr); err != nil {
  1702. return s.runtimeErr(ctx, fmt.Errorf("SendClientEvent: invalid args: %w", err))
  1703. }
  1704. status, err := strconv.ParseUint(statusStr, 10, 16)
  1705. if err != nil || status > 2 {
  1706. return s.runtimeErr(ctx, fmt.Errorf("SendClientEvent: typing status must be 0, 1, or 2, got %q", statusStr))
  1707. }
  1708. inBody := wire.SNAC_0x04_0x14_ICBMClientEvent{
  1709. Cookie: 0,
  1710. ChannelID: wire.ICBMChannelIM,
  1711. ScreenName: user,
  1712. Event: uint16(status),
  1713. }
  1714. if err := s.ICBMService.ClientEvent(ctx, sender, wire.SNACFrame{}, inBody); err != nil {
  1715. return s.runtimeErr(ctx, fmt.Errorf("ICBMService.ClientEvent: %w", err))
  1716. }
  1717. return []string{}
  1718. }
  1719. // SetAway handles the toc_set_away TOC command.
  1720. //
  1721. // From the TiK documentation:
  1722. //
  1723. // If the away message is present, then the unavailable status flag is set for
  1724. // the user. If the away message is not present, then the unavailable status
  1725. // flag is unset. The away message is basic HTML, remember to encode the
  1726. // information.
  1727. //
  1728. // Command syntax: toc_set_away [<away message>]
  1729. func (s OSCARProxy) SetAway(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1730. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
  1731. return errMsg
  1732. }
  1733. maybeMsg, err := parseArgs(args)
  1734. if err != nil {
  1735. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1736. }
  1737. var msg string
  1738. if len(maybeMsg) > 0 {
  1739. msg = unescape(maybeMsg[0])
  1740. }
  1741. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1742. TLVRestBlock: wire.TLVRestBlock{
  1743. TLVList: wire.TLVList{
  1744. wire.NewTLVBE(wire.LocateTLVTagsInfoUnavailableData, msg),
  1745. },
  1746. },
  1747. }
  1748. if err := s.LocateService.SetInfo(ctx, me, snac); err != nil {
  1749. return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
  1750. }
  1751. return []string{}
  1752. }
  1753. // SetCaps handles the toc_set_caps TOC command.
  1754. //
  1755. // From the TiK documentation:
  1756. //
  1757. // Set my capabilities. All capabilities that we support need to be sent at
  1758. // the same time. Capabilities are represented by UUIDs.
  1759. //
  1760. // This method automatically adds the "chat" capability since it doesn't seem
  1761. // to be sent explicitly by the official clients, even though they support
  1762. // chat.
  1763. //
  1764. // Command syntax: toc_set_caps [ <Capability 1> [<Capability 2> [...]]]
  1765. func (s OSCARProxy) SetCaps(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1766. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
  1767. return errMsg
  1768. }
  1769. params, err := parseArgs(args)
  1770. if err != nil {
  1771. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1772. }
  1773. // Clients may send capabilities as space-separated UUIDs or as a single
  1774. // comma-separated string (e.g. TameClone: "UUID,1348,134B,..."). Split each
  1775. // param on comma. Accept full UUIDs and OSCAR short caps (1–4 hex digits
  1776. // expanded to 0946XXYY-4C7F-11D1-8222-444553540000); ignore unknown tokens.
  1777. caps := make([]uuid.UUID, 0, 16)
  1778. for _, param := range params {
  1779. for _, token := range strings.Split(param, ",") {
  1780. token = strings.TrimSpace(token)
  1781. if token == "" {
  1782. continue
  1783. }
  1784. uid, err := uuid.Parse(token)
  1785. if err != nil {
  1786. uid, ok := wire.ShortCapHexToUUID(token)
  1787. if !ok {
  1788. continue
  1789. }
  1790. caps = append(caps, uid)
  1791. continue
  1792. }
  1793. caps = append(caps, uid)
  1794. }
  1795. }
  1796. // assume client supports chat, although we may want to do this according
  1797. // to client ID
  1798. caps = append(caps, wire.CapChat)
  1799. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1800. TLVRestBlock: wire.TLVRestBlock{
  1801. TLVList: wire.TLVList{
  1802. wire.NewTLVBE(wire.LocateTLVTagsInfoCapabilities, caps),
  1803. },
  1804. },
  1805. }
  1806. if err := s.LocateService.SetInfo(ctx, me, snac); err != nil {
  1807. return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
  1808. }
  1809. return []string{}
  1810. }
  1811. // SetConfig handles the toc_set_config TOC command.
  1812. //
  1813. // From the TiK documentation:
  1814. //
  1815. // Set the config information for this user. The config information is line
  1816. // oriented with the first character being the item type, followed by a space,
  1817. // with the rest of the line being the item value. Only letters, numbers, and
  1818. // spaces should be used. Remember you will have to enclose the entire config
  1819. // in quotes.
  1820. //
  1821. // Item Types:
  1822. // - g - Buddy Group (All Buddies until the next g or the end of config are in this group.)
  1823. // - b - A Buddy
  1824. // - p - Person on permit list
  1825. // - d - Person on deny list
  1826. // - m - Permit/Deny Mode. Possible values are
  1827. // - 1 - Permit All
  1828. // - 2 - Deny All
  1829. // - 3 - Permit Some
  1830. // - 4 - Deny Some
  1831. //
  1832. // This method doesn't attempt to validate any of the configuration--it saves
  1833. // the config as received from the client.
  1834. //
  1835. // Command syntax: toc_set_config <Config Info>
  1836. func (s OSCARProxy) SetConfig(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1837. if status := me.Session().EvaluateRateLimit(time.Now(), 1); status == wire.RateLimitStatusLimited {
  1838. return []string{rateLimitExceededErr}
  1839. }
  1840. // most TOC clients don't quote the config info argument, despite what the
  1841. // documentation specifies. this makes the argument payload incompatible
  1842. // for CSV parsing. since this command takes a single argument, we can get
  1843. // away with trimming quotes and spaces from the byte slice before passing
  1844. // it to the config store.
  1845. args = bytes.Trim(args, "'\" ")
  1846. cfg := string(args)
  1847. if cfg == "" {
  1848. return s.runtimeErr(ctx, fmt.Errorf("empty config"))
  1849. }
  1850. if err := s.TOCConfigStore.SetTOCConfig(ctx, me.IdentScreenName(), cfg); err != nil {
  1851. return s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.SaveTOCConfig: %w", err))
  1852. }
  1853. return []string{}
  1854. }
  1855. // SetDir handles the toc_set_dir TOC command.
  1856. //
  1857. // From the TiK documentation:
  1858. //
  1859. // Set the DIR user information. This is a colon separated fields as in:
  1860. //
  1861. // "first name":"middle name":"last name":"maiden name":"city":"state":"country":"email":"allow web searches".
  1862. //
  1863. // Should return a DIR_STATUS msg. Having anything in the "allow web searches"
  1864. // field allows people to use web-searches to find your directory info.
  1865. // Otherwise, they'd have to use the client.
  1866. //
  1867. // The fields "email" and "allow web searches" are ignored by this method.
  1868. //
  1869. // Command syntax: toc_set_dir <info information>
  1870. func (s OSCARProxy) SetDir(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1871. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetDirInfo); isLimited {
  1872. return errMsg
  1873. }
  1874. var info string
  1875. if _, err := parseArgs(args, &info); err != nil {
  1876. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1877. }
  1878. info = unescape(info)
  1879. rawFields := strings.Split(info, ":")
  1880. var finalFields [9]string
  1881. if len(rawFields) > len(finalFields) {
  1882. return s.runtimeErr(ctx, fmt.Errorf("expected at most %d params, got %d", len(finalFields), len(rawFields)))
  1883. }
  1884. for i, a := range rawFields {
  1885. finalFields[i] = strings.Trim(a, "\"")
  1886. }
  1887. snac := wire.SNAC_0x02_0x09_LocateSetDirInfo{
  1888. TLVRestBlock: wire.TLVRestBlock{
  1889. TLVList: wire.TLVList{
  1890. wire.NewTLVBE(wire.ODirTLVFirstName, finalFields[0]),
  1891. wire.NewTLVBE(wire.ODirTLVMiddleName, finalFields[1]),
  1892. wire.NewTLVBE(wire.ODirTLVLastName, finalFields[2]),
  1893. wire.NewTLVBE(wire.ODirTLVMaidenName, finalFields[3]),
  1894. wire.NewTLVBE(wire.ODirTLVCountry, finalFields[6]),
  1895. wire.NewTLVBE(wire.ODirTLVState, finalFields[5]),
  1896. wire.NewTLVBE(wire.ODirTLVCity, finalFields[4]),
  1897. },
  1898. },
  1899. }
  1900. if _, err := s.LocateService.SetDirInfo(ctx, me, wire.SNACFrame{}, snac); err != nil {
  1901. return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetDirInfo: %w", err))
  1902. }
  1903. return []string{}
  1904. }
  1905. // SetIdle handles the toc_set_idle TOC command.
  1906. //
  1907. // From the TiK documentation:
  1908. //
  1909. // Set idle information. If <idle secs> is 0 then the user isn't idle at all.
  1910. // If <idle secs> is greater than 0 then the user has already been idle for
  1911. // <idle secs> number of seconds. The server will automatically keep
  1912. // incrementing this number, so do not repeatedly call with new idle times.
  1913. //
  1914. // Command syntax: toc_set_idle <idle secs>
  1915. func (s OSCARProxy) SetIdle(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1916. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.OService, wire.OServiceIdleNotification); isLimited {
  1917. return errMsg
  1918. }
  1919. var idleTimeStr string
  1920. if _, err := parseArgs(args, &idleTimeStr); err != nil {
  1921. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1922. }
  1923. t, err := strconv.Atoi(idleTimeStr)
  1924. if err != nil {
  1925. return s.runtimeErr(ctx, fmt.Errorf("strconv.Atoi: %w", err))
  1926. }
  1927. snac := wire.SNAC_0x01_0x11_OServiceIdleNotification{
  1928. IdleTime: uint32(t),
  1929. }
  1930. if err := s.OServiceService.IdleNotification(ctx, me, snac); err != nil {
  1931. return s.runtimeErr(ctx, fmt.Errorf("OServiceServiceBOS.IdleNotification: %w", err))
  1932. }
  1933. return []string{}
  1934. }
  1935. // SetInfo handles the toc_set_info TOC command.
  1936. //
  1937. // From the TiK documentation:
  1938. //
  1939. // Set the LOCATE user information. This is basic HTML. Remember to encode the info.
  1940. //
  1941. // Command syntax: toc_set_info <info information>
  1942. func (s OSCARProxy) SetInfo(ctx context.Context, me *state.SessionInstance, args []byte) []string {
  1943. if errMsg, isLimited := s.checkRateLimit(ctx, me, wire.Locate, wire.LocateSetInfo); isLimited {
  1944. return errMsg
  1945. }
  1946. var info string
  1947. if _, err := parseArgs(args, &info); err != nil {
  1948. return s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1949. }
  1950. info = unescape(info)
  1951. snac := wire.SNAC_0x02_0x04_LocateSetInfo{
  1952. TLVRestBlock: wire.TLVRestBlock{
  1953. TLVList: wire.TLVList{
  1954. wire.NewTLVBE(wire.LocateTLVTagsInfoSigData, info),
  1955. },
  1956. },
  1957. }
  1958. if err := s.LocateService.SetInfo(ctx, me, snac); err != nil {
  1959. return s.runtimeErr(ctx, fmt.Errorf("LocateService.SetInfo: %w", err))
  1960. }
  1961. return []string{}
  1962. }
  1963. // Signon handles the toc_signon, toc2_signon, and toc2_login TOC commands.
  1964. //
  1965. // From the TiK documentation (TOC1 toc_signon):
  1966. //
  1967. // The password needs to be roasted with the Roasting String if coming over a
  1968. // FLAP connection, CP connections don't use roasted passwords. The language
  1969. // specified will be used when generating web pages, such as the get info
  1970. // pages. Currently, the only supported language is "english". If the language
  1971. // sent isn't found, the default "english" language will be used. The version
  1972. // string will be used for the client identity, and must be less than 50
  1973. // characters.
  1974. //
  1975. // Passwords are roasted when sent to the host. This is done so they aren't
  1976. // sent in "clear text" over the wire, although they are still trivial to
  1977. // decode. Roasting is performed by first xoring each byte in the password
  1978. // with the equivalent modulo byte in the roasting string. The result is then
  1979. // converted to ascii hex, and prepended with "0x". So for example the
  1980. // password "password" roasts to "0x2408105c23001130".
  1981. //
  1982. // The Roasting String is Tic/Toc.
  1983. //
  1984. // toc2_signon uses the same parameters as toc_signon. toc2_login (TOC2) may
  1985. // include additional trailing parameters (e.g. agent string, code); see
  1986. // BlueTOC documentation.
  1987. //
  1988. // Command syntax: toc_signon <authorizer host> <authorizer port> <User Name> <Password> <language> <version>
  1989. // Command syntax: toc2_signon <authorizer host> <authorizer port> <User Name> <Password> <language> <version>
  1990. // Command syntax: toc2_login <authorizer host> <authorizer port> <User Name> <Password> <language> <version> [additional params...]
  1991. func (s OSCARProxy) Signon(ctx context.Context, args []byte, recalcWarning func(ctx context.Context, instance *state.SessionInstance) error, lowerWarnLevel func(ctx context.Context, instance *state.SessionInstance), chatRegistry *ChatRegistry) (*state.SessionInstance, []string) {
  1992. var cmd, userName, password string
  1993. if _, err := parseArgs(args, &cmd, nil, nil, &userName, &password); err != nil {
  1994. return nil, s.runtimeErr(ctx, fmt.Errorf("parseArgs: %w", err))
  1995. }
  1996. switch cmd {
  1997. case "toc_signon", "toc2_signon", "toc2_login":
  1998. // valid
  1999. default:
  2000. return nil, s.runtimeErr(ctx, errors.New("expected one of toc_signon, toc2_signon, toc2_login"))
  2001. }
  2002. if len(password) < 3 {
  2003. return nil, s.runtimeErr(ctx, fmt.Errorf("password too short for roasted hex (need 2-char prefix + hex)"))
  2004. }
  2005. passwordHash, err := hex.DecodeString(password[2:])
  2006. if err != nil {
  2007. return nil, s.runtimeErr(ctx, fmt.Errorf("hex.DecodeString: %w", err))
  2008. }
  2009. signonFrame := wire.FLAPSignonFrame{}
  2010. signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsScreenName, userName))
  2011. signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsRoastedTOCPassword, passwordHash))
  2012. if cmd == "toc2_login" || cmd == "toc2_signon" {
  2013. signonFrame.Append(wire.NewTLVBE(wire.LoginTLVTagsMultiConnFlags, wire.MultiConnFlagsRecentClient))
  2014. }
  2015. block, err := s.AuthService.FLAPLogin(ctx, signonFrame, "")
  2016. if err != nil {
  2017. return nil, s.runtimeErr(ctx, fmt.Errorf("AuthService.FLAPLogin: %w", err))
  2018. }
  2019. if block.HasTag(wire.LoginTLVTagsErrorSubcode) {
  2020. s.Logger.DebugContext(ctx, "login failed")
  2021. return nil, []string{"ERROR:" + wire.TOCErrorAuthIncorrectNickOrPassword}
  2022. }
  2023. authCookie, ok := block.Bytes(wire.OServiceTLVTagsLoginCookie)
  2024. if !ok {
  2025. return nil, s.runtimeErr(ctx, fmt.Errorf("unable to get session id from payload"))
  2026. }
  2027. // todo: naming for cookie: login cookie, server cookie, or auth cookie?
  2028. serverCookie, err := s.AuthService.CrackCookie(authCookie)
  2029. if err != nil {
  2030. return nil, s.runtimeErr(ctx, fmt.Errorf("AuthService.CrackCookie: %w", err))
  2031. }
  2032. fnCfg := func(sess *state.Session) {
  2033. sess.OnSessionClose(func() {
  2034. if !shuttingDown(ctx) {
  2035. if err := s.BuddyService.BroadcastBuddyDeparted(ctx, sess.IdentScreenName()); err != nil {
  2036. s.Logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
  2037. }
  2038. }
  2039. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
  2040. defer cancel()
  2041. // buddy list must be cleared before session is closed, otherwise
  2042. // there will be a race condition that could cause the buddy list
  2043. // be prematurely deleted.
  2044. if err := s.BuddyListRegistry.UnregisterBuddyList(ctx, sess.IdentScreenName()); err != nil {
  2045. s.Logger.ErrorContext(ctx, "error removing buddy list entry", "err", err.Error())
  2046. }
  2047. s.ChatSessionManager.RemoveUserFromAllChats(sess.IdentScreenName())
  2048. s.AuthService.Signout(ctx, sess)
  2049. })
  2050. }
  2051. instance, err := s.AuthService.RegisterBOSSession(ctx, serverCookie, fnCfg)
  2052. if err != nil {
  2053. return nil, s.runtimeErr(ctx, fmt.Errorf("AuthService.RegisterBOSSession: %w", err))
  2054. }
  2055. if err = instance.Session().RunOnce(func() error {
  2056. // make buddy list visible to other users
  2057. if err := s.BuddyListRegistry.RegisterBuddyList(ctx, instance.IdentScreenName()); err != nil {
  2058. return fmt.Errorf("unable to init buddy list: %w", err)
  2059. }
  2060. // restore warning level from last session
  2061. if err := recalcWarning(ctx, instance); err != nil {
  2062. return fmt.Errorf("failed to recalculate warning level: %w", err)
  2063. }
  2064. // periodically decay warning level
  2065. go lowerWarnLevel(ctx, instance)
  2066. return nil
  2067. }); err != nil {
  2068. return nil, s.runtimeErr(ctx, fmt.Errorf("Session.RunOnce: %w", err))
  2069. }
  2070. // Update user visibility when an instance closes, as the user's overall status may change.
  2071. // Example: With 1 away and 1 non-away instance, the user appears available. If the non-away
  2072. // instance closes, the user should appear away.
  2073. instance.OnClose(func() {
  2074. if shuttingDown(ctx) {
  2075. return
  2076. }
  2077. if instance.Session().Invisible() {
  2078. if err := s.BuddyService.BroadcastBuddyDeparted(ctx, instance.IdentScreenName()); err != nil {
  2079. s.Logger.ErrorContext(ctx, "error sending buddy departure notifications", "err", err.Error())
  2080. }
  2081. } else {
  2082. if err := s.BuddyService.BroadcastBuddyArrived(ctx, instance.IdentScreenName(), instance.Session().TLVUserInfo()); err != nil {
  2083. s.Logger.ErrorContext(ctx, "error sending buddy arrival notifications", "err", err.Error())
  2084. }
  2085. }
  2086. })
  2087. // set chat capability so that... tk
  2088. instance.SetCaps([][16]byte{wire.CapChat})
  2089. if cmd == "toc_signon" {
  2090. u, err := s.TOCConfigStore.User(ctx, instance.IdentScreenName())
  2091. if err != nil {
  2092. return nil, s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.User: %w", err))
  2093. }
  2094. if u == nil {
  2095. return nil, s.runtimeErr(ctx, fmt.Errorf("TOCConfigStore.User: user not found"))
  2096. }
  2097. return instance, []string{"SIGN_ON:TOC1.0", fmt.Sprintf("CONFIG:%s", u.TOCConfig), fmt.Sprintf("NICK:%s", instance.DisplayScreenName().String())}
  2098. }
  2099. supportsTOC2MsgEnc := cmd == "toc2_login"
  2100. instance.SetTOC2(supportsTOC2MsgEnc)
  2101. if err := s.FeedbagService.Use(ctx, instance); err != nil {
  2102. return instance, s.runtimeErr(ctx, fmt.Errorf("FeedbagService.Use: %w", err))
  2103. }
  2104. fb, err := s.FeedbagManager.Feedbag(ctx, instance.IdentScreenName())
  2105. if err != nil {
  2106. return instance, s.runtimeErr(ctx, fmt.Errorf("FeedbagManager.Feedbag: %w", err))
  2107. }
  2108. signon := []string{
  2109. "SIGN_ON:TOC2.0",
  2110. fmt.Sprintf("NICK:%s", instance.DisplayScreenName().String()),
  2111. }
  2112. cfg, err := buildToc2Config(fb)
  2113. if err != nil {
  2114. return instance, s.runtimeErr(ctx, fmt.Errorf("buildToc2Config: %w", err))
  2115. }
  2116. signon = append(signon, cfg...)
  2117. return instance, signon
  2118. }
  2119. func shuttingDown(ctx context.Context) bool {
  2120. select {
  2121. case <-ctx.Done():
  2122. // server is shutting down, don't send buddy notifications
  2123. return true
  2124. default:
  2125. }
  2126. return false
  2127. }
  2128. // buildToc2Config constructs configuration for CONFIG2
  2129. // - Lines are separated by LF; data between colons.
  2130. // - g: = group (before buddies in that group), b: = buddy (optionally :alias and :::::note),
  2131. // - d: = blocked, p: = private (permit), m: = privacy level (1-5), done: = end.
  2132. func buildToc2Config(fb []wire.FeedbagItem) ([]string, error) {
  2133. type buddy struct {
  2134. name string
  2135. alias string
  2136. note string
  2137. }
  2138. type group struct {
  2139. name string
  2140. buddies map[uint16]buddy
  2141. order []uint16
  2142. }
  2143. type feedbag struct {
  2144. order []uint16
  2145. groups map[uint16]group
  2146. }
  2147. buddylist := feedbag{
  2148. groups: make(map[uint16]group),
  2149. }
  2150. for _, item := range fb {
  2151. if item.ClassID == wire.FeedbagClassIdGroup {
  2152. // root group contains the order of all other groups
  2153. if item.GroupID == 0 {
  2154. val, hasVal := item.Uint16SliceBE(wire.FeedbagAttributesOrder)
  2155. if !hasVal {
  2156. return []string{}, fmt.Errorf("root group missing order attribute")
  2157. }
  2158. buddylist.order = val
  2159. continue
  2160. }
  2161. group := group{
  2162. name: item.Name,
  2163. buddies: make(map[uint16]buddy),
  2164. }
  2165. // order will be empty if the group is empty
  2166. val, _ := item.Uint16SliceBE(wire.FeedbagAttributesOrder)
  2167. group.order = val
  2168. buddylist.groups[item.GroupID] = group
  2169. }
  2170. }
  2171. var cfg []string
  2172. for _, item := range fb {
  2173. if item.ClassID == wire.FeedbagClassIdBuddy {
  2174. // Ensure group exists (handle orphaned buddies)
  2175. if _, exists := buddylist.groups[item.GroupID]; !exists {
  2176. buddylist.groups[item.GroupID] = group{
  2177. name: "", // Unknown group name
  2178. buddies: make(map[uint16]buddy),
  2179. order: []uint16{},
  2180. }
  2181. }
  2182. buddy := buddy{
  2183. name: item.Name,
  2184. }
  2185. if val, hasVal := item.String(wire.FeedbagAttributesNote); hasVal {
  2186. buddy.note = val
  2187. }
  2188. if val, hasVal := item.String(wire.FeedbagAttributesAlias); hasVal {
  2189. buddy.alias = val
  2190. }
  2191. buddylist.groups[item.GroupID].buddies[item.ItemID] = buddy
  2192. }
  2193. if item.ClassID == wire.FeedbagClassIDDeny {
  2194. cfg = append(cfg, "d:"+item.Name)
  2195. }
  2196. if item.ClassID == wire.FeedbagClassIDPermit {
  2197. cfg = append(cfg, "p:"+item.Name)
  2198. }
  2199. if item.ClassID == wire.FeedbagClassIdPdinfo {
  2200. val, hasVal := item.Uint8(wire.FeedbagAttributesPdMode)
  2201. if hasVal {
  2202. cfg = append(cfg, fmt.Sprintf("m:%d", val))
  2203. }
  2204. }
  2205. }
  2206. for _, gid := range buddylist.order {
  2207. group := buddylist.groups[gid]
  2208. cfg = append(cfg, "g:"+group.name)
  2209. for _, bid := range group.order {
  2210. buddy := group.buddies[bid]
  2211. tmpLine := "b:" + buddy.name
  2212. if buddy.alias != "" {
  2213. tmpLine += ":" + buddy.alias
  2214. }
  2215. if buddy.note != "" {
  2216. tmpLine += ":::::" + buddy.note
  2217. }
  2218. cfg = append(cfg, tmpLine)
  2219. }
  2220. }
  2221. cfg = append(cfg, "done:")
  2222. return []string{"CONFIG2:" + strings.Join(cfg, "\n") + "\n"}, nil
  2223. }
  2224. // newHTTPAuthToken creates a HMAC token for authenticating TOC HTTP requests
  2225. func (s OSCARProxy) newHTTPAuthToken(me state.IdentScreenName) (string, error) {
  2226. cookie, err := s.CookieBaker.Issue([]byte(me.String()))
  2227. if err != nil {
  2228. return "", err
  2229. }
  2230. // trim padding so that gaim doesn't choke on the long value
  2231. cookie = bytes.TrimRight(cookie, "\x00")
  2232. return hex.EncodeToString(cookie), nil
  2233. }
  2234. // parseArgs extracts arguments from a TOC command. Each positional argument is
  2235. // assigned to its corresponding args pointer. It returns the remaining
  2236. // arguments as varargs.
  2237. func parseArgs(payload []byte, args ...*string) (varArgs []string, err error) {
  2238. if len(payload) == 0 && len(args) == 0 {
  2239. return []string{}, nil
  2240. }
  2241. reader := csv.NewReader(bytes.NewReader(payload))
  2242. reader.Comma = ' '
  2243. reader.LazyQuotes = true
  2244. reader.TrimLeadingSpace = true
  2245. segs, err := reader.Read()
  2246. if err != nil {
  2247. return []string{}, fmt.Errorf("CSV reader error: %w", err)
  2248. }
  2249. if len(segs) < len(args) {
  2250. return []string{}, fmt.Errorf("command contains fewer arguments than expected")
  2251. }
  2252. // populate placeholder pointers with their corresponding values
  2253. for i, arg := range args {
  2254. if arg != nil {
  2255. *arg = strings.TrimSpace(segs[i])
  2256. }
  2257. }
  2258. // dump remaining arguments as varargs
  2259. return segs[len(args):], err
  2260. }
  2261. // runtimeErr is a convenience function that logs an error and returns a TOC
  2262. // internal server error.
  2263. func (s OSCARProxy) runtimeErr(ctx context.Context, err error) []string {
  2264. s.Logger.ErrorContext(ctx, "internal service error", "err", err.Error())
  2265. return []string{cmdInternalSvcErr}
  2266. }
  2267. func (s OSCARProxy) checkRateLimit(ctx context.Context, sender *state.SessionInstance, foodGroup uint16, subGroup uint16) ([]string, bool) {
  2268. rateClassID, ok := s.SNACRateLimits.RateClassLookup(foodGroup, subGroup)
  2269. if !ok {
  2270. s.Logger.ErrorContext(ctx, "rate limit not found, allowing request through")
  2271. return []string{}, false
  2272. }
  2273. if status := sender.Session().EvaluateRateLimit(time.Now(), rateClassID); status == wire.RateLimitStatusLimited {
  2274. s.Logger.DebugContext(ctx, "(toc) rate limit exceeded, dropping SNAC",
  2275. "foodgroup", wire.FoodGroupName(foodGroup),
  2276. "subgroup", wire.SubGroupName(foodGroup, subGroup))
  2277. return []string{rateLimitExceededErr}, true
  2278. }
  2279. return []string{}, false
  2280. }
  2281. // unescape removes escaping from the following TOC characters: \ { } ( ) [ ] $ "
  2282. func unescape(encoded string) string {
  2283. if !strings.ContainsRune(encoded, '\\') {
  2284. return encoded
  2285. }
  2286. var result strings.Builder
  2287. result.Grow(len(encoded))
  2288. escaped := false
  2289. for i := 0; i < len(encoded); i++ {
  2290. ch := encoded[i]
  2291. if escaped {
  2292. // append escaped character without the backslash
  2293. result.WriteByte(ch)
  2294. escaped = false
  2295. } else if ch == '\\' {
  2296. escaped = true
  2297. } else {
  2298. result.WriteByte(ch)
  2299. }
  2300. }
  2301. return result.String()
  2302. }
  2303. // parseTOC2Config parses the TOC2 config format string into a map of group names to buddy lists.
  2304. //
  2305. // Config format: {g:group<lf>b:buddy1<lf>b:buddy2<lf>}
  2306. // Where <lf> is a linefeed character (ASCII 10, \n).
  2307. //
  2308. // Extended format for buddies with alias/note:
  2309. // b:buddy:alias:::::note
  2310. //
  2311. // Returns a map where keys are group names and values are slices of buddy entries.
  2312. // Each buddy entry is a string that may contain alias/note information.
  2313. func parseTOC2Config(config string) (map[string][]string, error) {
  2314. // Remove surrounding braces
  2315. config = strings.TrimPrefix(config, "{")
  2316. config = strings.TrimSuffix(config, "}")
  2317. // Split by linefeed (ASCII 10)
  2318. lines := strings.Split(config, "\n")
  2319. result := make(map[string][]string)
  2320. var currentGroup string
  2321. for _, line := range lines {
  2322. line = strings.TrimSpace(line)
  2323. if line == "" {
  2324. continue
  2325. }
  2326. if strings.HasPrefix(line, "g:") {
  2327. // Group line: g:groupname
  2328. currentGroup = strings.TrimPrefix(line, "g:")
  2329. if currentGroup == "" {
  2330. return nil, fmt.Errorf("empty group name")
  2331. }
  2332. if result[currentGroup] == nil {
  2333. result[currentGroup] = []string{}
  2334. }
  2335. } else if strings.HasPrefix(line, "b:") {
  2336. // Buddy line: b:buddy or b:buddy:alias:::::note
  2337. if currentGroup == "" {
  2338. return nil, fmt.Errorf("buddy entry without group")
  2339. }
  2340. buddyEntry := strings.TrimPrefix(line, "b:")
  2341. if buddyEntry == "" {
  2342. return nil, fmt.Errorf("empty buddy name")
  2343. }
  2344. result[currentGroup] = append(result[currentGroup], buddyEntry)
  2345. }
  2346. }
  2347. return result, nil
  2348. }