irc.py 125 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117
  1. # -*- test-case-name: twisted.words.test.test_irc -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Internet Relay Chat protocol for client and server.
  6. Future Plans
  7. ============
  8. The way the IRCClient class works here encourages people to implement
  9. IRC clients by subclassing the ephemeral protocol class, and it tends
  10. to end up with way more state than it should for an object which will
  11. be destroyed as soon as the TCP transport drops. Someone oughta do
  12. something about that, ya know?
  13. The DCC support needs to have more hooks for the client for it to be
  14. able to ask the user things like "Do you want to accept this session?"
  15. and "Transfer #2 is 67% done." and otherwise manage the DCC sessions.
  16. Test coverage needs to be better.
  17. @var MAX_COMMAND_LENGTH: The maximum length of a command, as defined by RFC
  18. 2812 section 2.3.
  19. @var attributes: Singleton instance of L{_CharacterAttributes}, used for
  20. constructing formatted text information.
  21. @author: Kevin Turner
  22. @see: RFC 1459: Internet Relay Chat Protocol
  23. @see: RFC 2812: Internet Relay Chat: Client Protocol
  24. @see: U{The Client-To-Client-Protocol
  25. <http://www.irchelp.org/irchelp/rfc/ctcpspec.html>}
  26. """
  27. import errno
  28. import operator
  29. import os
  30. import random
  31. import re
  32. import shlex
  33. import socket
  34. import stat
  35. import string
  36. import struct
  37. import sys
  38. import textwrap
  39. import time
  40. import traceback
  41. from functools import reduce
  42. from os import path
  43. from typing import Optional
  44. from twisted.internet import protocol, reactor, task
  45. from twisted.persisted import styles
  46. from twisted.protocols import basic
  47. from twisted.python import _textattributes, log, reflect
  48. NUL = chr(0)
  49. CR = chr(0o15)
  50. NL = chr(0o12)
  51. LF = NL
  52. SPC = chr(0o40)
  53. # This includes the CRLF terminator characters.
  54. MAX_COMMAND_LENGTH = 512
  55. CHANNEL_PREFIXES = "&#!+"
  56. class IRCBadMessage(Exception):
  57. pass
  58. class IRCPasswordMismatch(Exception):
  59. pass
  60. class IRCBadModes(ValueError):
  61. """
  62. A malformed mode was encountered while attempting to parse a mode string.
  63. """
  64. def parsemsg(s):
  65. """
  66. Breaks a message from an IRC server into its prefix, command, and
  67. arguments.
  68. @param s: The message to break.
  69. @type s: L{bytes}
  70. @return: A tuple of (prefix, command, args).
  71. @rtype: L{tuple}
  72. """
  73. prefix = ""
  74. trailing = []
  75. if not s:
  76. raise IRCBadMessage("Empty line.")
  77. if s[0:1] == ":":
  78. prefix, s = s[1:].split(" ", 1)
  79. if s.find(" :") != -1:
  80. s, trailing = s.split(" :", 1)
  81. args = s.split()
  82. args.append(trailing)
  83. else:
  84. args = s.split()
  85. command = args.pop(0)
  86. return prefix, command, args
  87. def split(str, length=80):
  88. """
  89. Split a string into multiple lines.
  90. Whitespace near C{str[length]} will be preferred as a breaking point.
  91. C{"\\n"} will also be used as a breaking point.
  92. @param str: The string to split.
  93. @type str: C{str}
  94. @param length: The maximum length which will be allowed for any string in
  95. the result.
  96. @type length: C{int}
  97. @return: C{list} of C{str}
  98. """
  99. return [chunk for line in str.split("\n") for chunk in textwrap.wrap(line, length)]
  100. def _intOrDefault(value, default=None):
  101. """
  102. Convert a value to an integer if possible.
  103. @rtype: C{int} or type of L{default}
  104. @return: An integer when C{value} can be converted to an integer,
  105. otherwise return C{default}
  106. """
  107. if value:
  108. try:
  109. return int(value)
  110. except (TypeError, ValueError):
  111. pass
  112. return default
  113. class UnhandledCommand(RuntimeError):
  114. """
  115. A command dispatcher could not locate an appropriate command handler.
  116. """
  117. class _CommandDispatcherMixin:
  118. """
  119. Dispatch commands to handlers based on their name.
  120. Command handler names should be of the form C{prefix_commandName},
  121. where C{prefix} is the value specified by L{prefix}, and must
  122. accept the parameters as given to L{dispatch}.
  123. Attempting to mix this in more than once for a single class will cause
  124. strange behaviour, due to L{prefix} being overwritten.
  125. @type prefix: C{str}
  126. @ivar prefix: Command handler prefix, used to locate handler attributes
  127. """
  128. prefix: Optional[str] = None
  129. def dispatch(self, commandName, *args):
  130. """
  131. Perform actual command dispatch.
  132. """
  133. def _getMethodName(command):
  134. return f"{self.prefix}_{command}"
  135. def _getMethod(name):
  136. return getattr(self, _getMethodName(name), None)
  137. method = _getMethod(commandName)
  138. if method is not None:
  139. return method(*args)
  140. method = _getMethod("unknown")
  141. if method is None:
  142. raise UnhandledCommand(
  143. f"No handler for {_getMethodName(commandName)!r} could be found"
  144. )
  145. return method(commandName, *args)
  146. def parseModes(modes, params, paramModes=("", "")):
  147. """
  148. Parse an IRC mode string.
  149. The mode string is parsed into two lists of mode changes (added and
  150. removed), with each mode change represented as C{(mode, param)} where mode
  151. is the mode character, and param is the parameter passed for that mode, or
  152. L{None} if no parameter is required.
  153. @type modes: C{str}
  154. @param modes: Modes string to parse.
  155. @type params: C{list}
  156. @param params: Parameters specified along with L{modes}.
  157. @type paramModes: C{(str, str)}
  158. @param paramModes: A pair of strings (C{(add, remove)}) that indicate which modes take
  159. parameters when added or removed.
  160. @returns: Two lists of mode changes, one for modes added and the other for
  161. modes removed respectively, mode changes in each list are represented as
  162. C{(mode, param)}.
  163. """
  164. if len(modes) == 0:
  165. raise IRCBadModes("Empty mode string")
  166. if modes[0] not in "+-":
  167. raise IRCBadModes(f"Malformed modes string: {modes!r}")
  168. changes = ([], [])
  169. direction = None
  170. count = -1
  171. for ch in modes:
  172. if ch in "+-":
  173. if count == 0:
  174. raise IRCBadModes(f"Empty mode sequence: {modes!r}")
  175. direction = "+-".index(ch)
  176. count = 0
  177. else:
  178. param = None
  179. if ch in paramModes[direction]:
  180. try:
  181. param = params.pop(0)
  182. except IndexError:
  183. raise IRCBadModes(f"Not enough parameters: {ch!r}")
  184. changes[direction].append((ch, param))
  185. count += 1
  186. if len(params) > 0:
  187. raise IRCBadModes(f"Too many parameters: {modes!r} {params!r}")
  188. if count == 0:
  189. raise IRCBadModes(f"Empty mode sequence: {modes!r}")
  190. return changes
  191. class IRC(protocol.Protocol):
  192. """
  193. Internet Relay Chat server protocol.
  194. """
  195. buffer = ""
  196. hostname = None
  197. encoding: Optional[str] = None
  198. def connectionMade(self):
  199. self.channels = []
  200. if self.hostname is None:
  201. self.hostname = socket.getfqdn()
  202. def sendLine(self, line):
  203. line = line + CR + LF
  204. if isinstance(line, str):
  205. useEncoding = self.encoding if self.encoding else "utf-8"
  206. line = line.encode(useEncoding)
  207. self.transport.write(line)
  208. def sendMessage(self, command, *parameter_list, **prefix):
  209. """
  210. Send a line formatted as an IRC message.
  211. First argument is the command, all subsequent arguments are parameters
  212. to that command. If a prefix is desired, it may be specified with the
  213. keyword argument 'prefix'.
  214. The L{sendCommand} method is generally preferred over this one.
  215. Notably, this method does not support sending message tags, while the
  216. L{sendCommand} method does.
  217. """
  218. if not command:
  219. raise ValueError("IRC message requires a command.")
  220. if " " in command or command[0] == ":":
  221. # Not the ONLY way to screw up, but provides a little
  222. # sanity checking to catch likely dumb mistakes.
  223. raise ValueError(
  224. "Somebody screwed up, 'cuz this doesn't"
  225. " look like a command to me: %s" % command
  226. )
  227. line = " ".join([command] + list(parameter_list))
  228. if "prefix" in prefix:
  229. line = ":{} {}".format(prefix["prefix"], line)
  230. self.sendLine(line)
  231. if len(parameter_list) > 15:
  232. log.msg(
  233. "Message has %d parameters (RFC allows 15):\n%s"
  234. % (len(parameter_list), line)
  235. )
  236. def sendCommand(self, command, parameters, prefix=None, tags=None):
  237. """
  238. Send to the remote peer a line formatted as an IRC message.
  239. @param command: The command or numeric to send.
  240. @type command: L{unicode}
  241. @param parameters: The parameters to send with the command.
  242. @type parameters: A L{tuple} or L{list} of L{unicode} parameters
  243. @param prefix: The prefix to send with the command. If not
  244. given, no prefix is sent.
  245. @type prefix: L{unicode}
  246. @param tags: A dict of message tags. If not given, no message
  247. tags are sent. The dict key should be the name of the tag
  248. to send as a string; the value should be the unescaped value
  249. to send with the tag, or either None or "" if no value is to
  250. be sent with the tag.
  251. @type tags: L{dict} of tags (L{unicode}) => values (L{unicode})
  252. @see: U{https://ircv3.net/specs/core/message-tags-3.2.html}
  253. """
  254. if not command:
  255. raise ValueError("IRC message requires a command.")
  256. if " " in command or command[0] == ":":
  257. # Not the ONLY way to screw up, but provides a little
  258. # sanity checking to catch likely dumb mistakes.
  259. raise ValueError(f'Invalid command: "{command}"')
  260. if tags is None:
  261. tags = {}
  262. line = " ".join([command] + list(parameters))
  263. if prefix:
  264. line = f":{prefix} {line}"
  265. if tags:
  266. tagStr = self._stringTags(tags)
  267. line = f"@{tagStr} {line}"
  268. self.sendLine(line)
  269. if len(parameters) > 15:
  270. log.msg(
  271. "Message has %d parameters (RFC allows 15):\n%s"
  272. % (len(parameters), line)
  273. )
  274. def _stringTags(self, tags):
  275. """
  276. Converts a tag dictionary to a string.
  277. @param tags: The tag dict passed to sendMsg.
  278. @rtype: L{unicode}
  279. @return: IRCv3-format tag string
  280. """
  281. self._validateTags(tags)
  282. tagStrings = []
  283. for tag, value in tags.items():
  284. if value:
  285. tagStrings.append(f"{tag}={self._escapeTagValue(value)}")
  286. else:
  287. tagStrings.append(tag)
  288. return ";".join(tagStrings)
  289. def _validateTags(self, tags):
  290. """
  291. Checks the tag dict for errors and raises L{ValueError} if an
  292. error is found.
  293. @param tags: The tag dict passed to sendMsg.
  294. """
  295. for tag, value in tags.items():
  296. if not tag:
  297. raise ValueError("A tag name is required.")
  298. for char in tag:
  299. if not char.isalnum() and char not in ("-", "/", "."):
  300. raise ValueError("Tag contains invalid characters.")
  301. def _escapeTagValue(self, value):
  302. """
  303. Escape the given tag value according to U{escaping rules in IRCv3
  304. <https://ircv3.net/specs/core/message-tags-3.2.html>}.
  305. @param value: The string value to escape.
  306. @type value: L{str}
  307. @return: The escaped string for sending as a message value
  308. @rtype: L{str}
  309. """
  310. return (
  311. value.replace("\\", "\\\\")
  312. .replace(";", "\\:")
  313. .replace(" ", "\\s")
  314. .replace("\r", "\\r")
  315. .replace("\n", "\\n")
  316. )
  317. def dataReceived(self, data):
  318. """
  319. This hack is to support mIRC, which sends LF only, even though the RFC
  320. says CRLF. (Also, the flexibility of LineReceiver to turn "line mode"
  321. on and off was not required.)
  322. """
  323. if isinstance(data, bytes):
  324. data = data.decode("utf-8")
  325. lines = (self.buffer + data).split(LF)
  326. # Put the (possibly empty) element after the last LF back in the
  327. # buffer
  328. self.buffer = lines.pop()
  329. for line in lines:
  330. if len(line) <= 2:
  331. # This is a blank line, at best.
  332. continue
  333. if line[-1] == CR:
  334. line = line[:-1]
  335. prefix, command, params = parsemsg(line)
  336. # mIRC is a big pile of doo-doo
  337. command = command.upper()
  338. # DEBUG: log.msg( "%s %s %s" % (prefix, command, params))
  339. self.handleCommand(command, prefix, params)
  340. def handleCommand(self, command, prefix, params):
  341. """
  342. Determine the function to call for the given command and call it with
  343. the given arguments.
  344. @param command: The IRC command to determine the function for.
  345. @type command: L{bytes}
  346. @param prefix: The prefix of the IRC message (as returned by
  347. L{parsemsg}).
  348. @type prefix: L{bytes}
  349. @param params: A list of parameters to call the function with.
  350. @type params: L{list}
  351. """
  352. method = getattr(self, "irc_%s" % command, None)
  353. try:
  354. if method is not None:
  355. method(prefix, params)
  356. else:
  357. self.irc_unknown(prefix, command, params)
  358. except BaseException:
  359. log.deferr()
  360. def irc_unknown(self, prefix, command, params):
  361. """
  362. Called by L{handleCommand} on a command that doesn't have a defined
  363. handler. Subclasses should override this method.
  364. """
  365. raise NotImplementedError(command, prefix, params)
  366. # Helper methods
  367. def privmsg(self, sender, recip, message):
  368. """
  369. Send a message to a channel or user
  370. @type sender: C{str} or C{unicode}
  371. @param sender: Who is sending this message. Should be of the form
  372. username!ident@hostmask (unless you know better!).
  373. @type recip: C{str} or C{unicode}
  374. @param recip: The recipient of this message. If a channel, it must
  375. start with a channel prefix.
  376. @type message: C{str} or C{unicode}
  377. @param message: The message being sent.
  378. """
  379. self.sendCommand("PRIVMSG", (recip, f":{lowQuote(message)}"), sender)
  380. def notice(self, sender, recip, message):
  381. """
  382. Send a "notice" to a channel or user.
  383. Notices differ from privmsgs in that the RFC claims they are different.
  384. Robots are supposed to send notices and not respond to them. Clients
  385. typically display notices differently from privmsgs.
  386. @type sender: C{str} or C{unicode}
  387. @param sender: Who is sending this message. Should be of the form
  388. username!ident@hostmask (unless you know better!).
  389. @type recip: C{str} or C{unicode}
  390. @param recip: The recipient of this message. If a channel, it must
  391. start with a channel prefix.
  392. @type message: C{str} or C{unicode}
  393. @param message: The message being sent.
  394. """
  395. self.sendCommand("NOTICE", (recip, f":{message}"), sender)
  396. def action(self, sender, recip, message):
  397. """
  398. Send an action to a channel or user.
  399. @type sender: C{str} or C{unicode}
  400. @param sender: Who is sending this message. Should be of the form
  401. username!ident@hostmask (unless you know better!).
  402. @type recip: C{str} or C{unicode}
  403. @param recip: The recipient of this message. If a channel, it must
  404. start with a channel prefix.
  405. @type message: C{str} or C{unicode}
  406. @param message: The action being sent.
  407. """
  408. self.sendLine(f":{sender} ACTION {recip} :{message}")
  409. def topic(self, user, channel, topic, author=None):
  410. """
  411. Send the topic to a user.
  412. @type user: C{str} or C{unicode}
  413. @param user: The user receiving the topic. Only their nickname, not
  414. the full hostmask.
  415. @type channel: C{str} or C{unicode}
  416. @param channel: The channel for which this is the topic.
  417. @type topic: C{str} or C{unicode} or L{None}
  418. @param topic: The topic string, unquoted, or None if there is no topic.
  419. @type author: C{str} or C{unicode}
  420. @param author: If the topic is being changed, the full username and
  421. hostmask of the person changing it.
  422. """
  423. if author is None:
  424. if topic is None:
  425. self.sendLine(
  426. ":%s %s %s %s :%s"
  427. % (self.hostname, RPL_NOTOPIC, user, channel, "No topic is set.")
  428. )
  429. else:
  430. self.sendLine(
  431. ":%s %s %s %s :%s"
  432. % (self.hostname, RPL_TOPIC, user, channel, lowQuote(topic))
  433. )
  434. else:
  435. self.sendLine(f":{author} TOPIC {channel} :{lowQuote(topic)}")
  436. def topicAuthor(self, user, channel, author, date):
  437. """
  438. Send the author of and time at which a topic was set for the given
  439. channel.
  440. This sends a 333 reply message, which is not part of the IRC RFC.
  441. @type user: C{str} or C{unicode}
  442. @param user: The user receiving the topic. Only their nickname, not
  443. the full hostmask.
  444. @type channel: C{str} or C{unicode}
  445. @param channel: The channel for which this information is relevant.
  446. @type author: C{str} or C{unicode}
  447. @param author: The nickname (without hostmask) of the user who last set
  448. the topic.
  449. @type date: C{int}
  450. @param date: A POSIX timestamp (number of seconds since the epoch) at
  451. which the topic was last set.
  452. """
  453. self.sendLine(
  454. ":%s %d %s %s %s %d" % (self.hostname, 333, user, channel, author, date)
  455. )
  456. def names(self, user, channel, names):
  457. """
  458. Send the names of a channel's participants to a user.
  459. @type user: C{str} or C{unicode}
  460. @param user: The user receiving the name list. Only their nickname,
  461. not the full hostmask.
  462. @type channel: C{str} or C{unicode}
  463. @param channel: The channel for which this is the namelist.
  464. @type names: C{list} of C{str} or C{unicode}
  465. @param names: The names to send.
  466. """
  467. # XXX If unicode is given, these limits are not quite correct
  468. prefixLength = len(channel) + len(user) + 10
  469. namesLength = 512 - prefixLength
  470. L = []
  471. count = 0
  472. for n in names:
  473. if count + len(n) + 1 > namesLength:
  474. self.sendLine(
  475. ":%s %s %s = %s :%s"
  476. % (self.hostname, RPL_NAMREPLY, user, channel, " ".join(L))
  477. )
  478. L = [n]
  479. count = len(n)
  480. else:
  481. L.append(n)
  482. count += len(n) + 1
  483. if L:
  484. self.sendLine(
  485. ":%s %s %s = %s :%s"
  486. % (self.hostname, RPL_NAMREPLY, user, channel, " ".join(L))
  487. )
  488. self.sendLine(
  489. ":%s %s %s %s :End of /NAMES list"
  490. % (self.hostname, RPL_ENDOFNAMES, user, channel)
  491. )
  492. def who(self, user, channel, memberInfo):
  493. """
  494. Send a list of users participating in a channel.
  495. @type user: C{str} or C{unicode}
  496. @param user: The user receiving this member information. Only their
  497. nickname, not the full hostmask.
  498. @type channel: C{str} or C{unicode}
  499. @param channel: The channel for which this is the member information.
  500. @type memberInfo: C{list} of C{tuples}
  501. @param memberInfo: For each member of the given channel, a 7-tuple
  502. containing their username, their hostmask, the server to which they
  503. are connected, their nickname, the letter "H" or "G" (standing for
  504. "Here" or "Gone"), the hopcount from C{user} to this member, and
  505. this member's real name.
  506. """
  507. for info in memberInfo:
  508. (username, hostmask, server, nickname, flag, hops, realName) = info
  509. assert flag in ("H", "G")
  510. self.sendLine(
  511. ":%s %s %s %s %s %s %s %s %s :%d %s"
  512. % (
  513. self.hostname,
  514. RPL_WHOREPLY,
  515. user,
  516. channel,
  517. username,
  518. hostmask,
  519. server,
  520. nickname,
  521. flag,
  522. hops,
  523. realName,
  524. )
  525. )
  526. self.sendLine(
  527. ":%s %s %s %s :End of /WHO list."
  528. % (self.hostname, RPL_ENDOFWHO, user, channel)
  529. )
  530. def whois(
  531. self,
  532. user,
  533. nick,
  534. username,
  535. hostname,
  536. realName,
  537. server,
  538. serverInfo,
  539. oper,
  540. idle,
  541. signOn,
  542. channels,
  543. ):
  544. """
  545. Send information about the state of a particular user.
  546. @type user: C{str} or C{unicode}
  547. @param user: The user receiving this information. Only their nickname,
  548. not the full hostmask.
  549. @type nick: C{str} or C{unicode}
  550. @param nick: The nickname of the user this information describes.
  551. @type username: C{str} or C{unicode}
  552. @param username: The user's username (eg, ident response)
  553. @type hostname: C{str}
  554. @param hostname: The user's hostmask
  555. @type realName: C{str} or C{unicode}
  556. @param realName: The user's real name
  557. @type server: C{str} or C{unicode}
  558. @param server: The name of the server to which the user is connected
  559. @type serverInfo: C{str} or C{unicode}
  560. @param serverInfo: A descriptive string about that server
  561. @type oper: C{bool}
  562. @param oper: Indicates whether the user is an IRC operator
  563. @type idle: C{int}
  564. @param idle: The number of seconds since the user last sent a message
  565. @type signOn: C{int}
  566. @param signOn: A POSIX timestamp (number of seconds since the epoch)
  567. indicating the time the user signed on
  568. @type channels: C{list} of C{str} or C{unicode}
  569. @param channels: A list of the channels which the user is participating in
  570. """
  571. self.sendLine(
  572. ":%s %s %s %s %s %s * :%s"
  573. % (self.hostname, RPL_WHOISUSER, user, nick, username, hostname, realName)
  574. )
  575. self.sendLine(
  576. ":%s %s %s %s %s :%s"
  577. % (self.hostname, RPL_WHOISSERVER, user, nick, server, serverInfo)
  578. )
  579. if oper:
  580. self.sendLine(
  581. ":%s %s %s %s :is an IRC operator"
  582. % (self.hostname, RPL_WHOISOPERATOR, user, nick)
  583. )
  584. self.sendLine(
  585. ":%s %s %s %s %d %d :seconds idle, signon time"
  586. % (self.hostname, RPL_WHOISIDLE, user, nick, idle, signOn)
  587. )
  588. self.sendLine(
  589. ":%s %s %s %s :%s"
  590. % (self.hostname, RPL_WHOISCHANNELS, user, nick, " ".join(channels))
  591. )
  592. self.sendLine(
  593. ":%s %s %s %s :End of WHOIS list."
  594. % (self.hostname, RPL_ENDOFWHOIS, user, nick)
  595. )
  596. def join(self, who, where):
  597. """
  598. Send a join message.
  599. @type who: C{str} or C{unicode}
  600. @param who: The name of the user joining. Should be of the form
  601. username!ident@hostmask (unless you know better!).
  602. @type where: C{str} or C{unicode}
  603. @param where: The channel the user is joining.
  604. """
  605. self.sendLine(f":{who} JOIN {where}")
  606. def part(self, who, where, reason=None):
  607. """
  608. Send a part message.
  609. @type who: C{str} or C{unicode}
  610. @param who: The name of the user joining. Should be of the form
  611. username!ident@hostmask (unless you know better!).
  612. @type where: C{str} or C{unicode}
  613. @param where: The channel the user is joining.
  614. @type reason: C{str} or C{unicode}
  615. @param reason: A string describing the misery which caused this poor
  616. soul to depart.
  617. """
  618. if reason:
  619. self.sendLine(f":{who} PART {where} :{reason}")
  620. else:
  621. self.sendLine(f":{who} PART {where}")
  622. def channelMode(self, user, channel, mode, *args):
  623. """
  624. Send information about the mode of a channel.
  625. @type user: C{str} or C{unicode}
  626. @param user: The user receiving the name list. Only their nickname,
  627. not the full hostmask.
  628. @type channel: C{str} or C{unicode}
  629. @param channel: The channel for which this is the namelist.
  630. @type mode: C{str}
  631. @param mode: A string describing this channel's modes.
  632. @param args: Any additional arguments required by the modes.
  633. """
  634. self.sendLine(
  635. ":%s %s %s %s %s %s"
  636. % (self.hostname, RPL_CHANNELMODEIS, user, channel, mode, " ".join(args))
  637. )
  638. class ServerSupportedFeatures(_CommandDispatcherMixin):
  639. """
  640. Handle ISUPPORT messages.
  641. Feature names match those in the ISUPPORT RFC draft identically.
  642. Information regarding the specifics of ISUPPORT was gleaned from
  643. <http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt>.
  644. """
  645. prefix = "isupport"
  646. def __init__(self):
  647. self._features = {
  648. "CHANNELLEN": 200,
  649. "CHANTYPES": tuple("#&"),
  650. "MODES": 3,
  651. "NICKLEN": 9,
  652. "PREFIX": self._parsePrefixParam("(ovh)@+%"),
  653. # The ISUPPORT draft explicitly says that there is no default for
  654. # CHANMODES, but we're defaulting it here to handle the case where
  655. # the IRC server doesn't send us any ISUPPORT information, since
  656. # IRCClient.getChannelModeParams relies on this value.
  657. "CHANMODES": self._parseChanModesParam(["b", "", "lk", ""]),
  658. }
  659. @classmethod
  660. def _splitParamArgs(cls, params, valueProcessor=None):
  661. """
  662. Split ISUPPORT parameter arguments.
  663. Values can optionally be processed by C{valueProcessor}.
  664. For example::
  665. >>> ServerSupportedFeatures._splitParamArgs(['A:1', 'B:2'])
  666. (('A', '1'), ('B', '2'))
  667. @type params: C{iterable} of C{str}
  668. @type valueProcessor: C{callable} taking {str}
  669. @param valueProcessor: Callable to process argument values, or L{None}
  670. to perform no processing
  671. @rtype: C{list} of C{(str, object)}
  672. @return: Sequence of C{(name, processedValue)}
  673. """
  674. if valueProcessor is None:
  675. valueProcessor = lambda x: x
  676. def _parse():
  677. for param in params:
  678. if ":" not in param:
  679. param += ":"
  680. a, b = param.split(":", 1)
  681. yield a, valueProcessor(b)
  682. return list(_parse())
  683. @classmethod
  684. def _unescapeParamValue(cls, value):
  685. """
  686. Unescape an ISUPPORT parameter.
  687. The only form of supported escape is C{\\xHH}, where HH must be a valid
  688. 2-digit hexadecimal number.
  689. @rtype: C{str}
  690. """
  691. def _unescape():
  692. parts = value.split("\\x")
  693. # The first part can never be preceded by the escape.
  694. yield parts.pop(0)
  695. for s in parts:
  696. octet, rest = s[:2], s[2:]
  697. try:
  698. octet = int(octet, 16)
  699. except ValueError:
  700. raise ValueError(f"Invalid hex octet: {octet!r}")
  701. yield chr(octet) + rest
  702. if "\\x" not in value:
  703. return value
  704. return "".join(_unescape())
  705. @classmethod
  706. def _splitParam(cls, param):
  707. """
  708. Split an ISUPPORT parameter.
  709. @type param: C{str}
  710. @rtype: C{(str, list)}
  711. @return: C{(key, arguments)}
  712. """
  713. if "=" not in param:
  714. param += "="
  715. key, value = param.split("=", 1)
  716. return key, [cls._unescapeParamValue(v) for v in value.split(",")]
  717. @classmethod
  718. def _parsePrefixParam(cls, prefix):
  719. """
  720. Parse the ISUPPORT "PREFIX" parameter.
  721. The order in which the parameter arguments appear is significant, the
  722. earlier a mode appears the more privileges it gives.
  723. @rtype: C{dict} mapping C{str} to C{(str, int)}
  724. @return: A dictionary mapping a mode character to a two-tuple of
  725. C({symbol, priority)}, the lower a priority (the lowest being
  726. C{0}) the more privileges it gives
  727. """
  728. if not prefix:
  729. return None
  730. if prefix[0] != "(" and ")" not in prefix:
  731. raise ValueError("Malformed PREFIX parameter")
  732. modes, symbols = prefix.split(")", 1)
  733. symbols = zip(symbols, range(len(symbols)))
  734. modes = modes[1:]
  735. return dict(zip(modes, symbols))
  736. @classmethod
  737. def _parseChanModesParam(self, params):
  738. """
  739. Parse the ISUPPORT "CHANMODES" parameter.
  740. See L{isupport_CHANMODES} for a detailed explanation of this parameter.
  741. """
  742. names = ("addressModes", "param", "setParam", "noParam")
  743. if len(params) > len(names):
  744. raise ValueError(
  745. "Expecting a maximum of %d channel mode parameters, got %d"
  746. % (len(names), len(params))
  747. )
  748. items = map(lambda key, value: (key, value or ""), names, params)
  749. return dict(items)
  750. def getFeature(self, feature, default=None):
  751. """
  752. Get a server supported feature's value.
  753. A feature with the value L{None} is equivalent to the feature being
  754. unsupported.
  755. @type feature: C{str}
  756. @param feature: Feature name
  757. @type default: C{object}
  758. @param default: The value to default to, assuming that C{feature}
  759. is not supported
  760. @return: Feature value
  761. """
  762. return self._features.get(feature, default)
  763. def hasFeature(self, feature):
  764. """
  765. Determine whether a feature is supported or not.
  766. @rtype: C{bool}
  767. """
  768. return self.getFeature(feature) is not None
  769. def parse(self, params):
  770. """
  771. Parse ISUPPORT parameters.
  772. If an unknown parameter is encountered, it is simply added to the
  773. dictionary, keyed by its name, as a tuple of the parameters provided.
  774. @type params: C{iterable} of C{str}
  775. @param params: Iterable of ISUPPORT parameters to parse
  776. """
  777. for param in params:
  778. key, value = self._splitParam(param)
  779. if key.startswith("-"):
  780. self._features.pop(key[1:], None)
  781. else:
  782. self._features[key] = self.dispatch(key, value)
  783. def isupport_unknown(self, command, params):
  784. """
  785. Unknown ISUPPORT parameter.
  786. """
  787. return tuple(params)
  788. def isupport_CHANLIMIT(self, params):
  789. """
  790. The maximum number of each channel type a user may join.
  791. """
  792. return self._splitParamArgs(params, _intOrDefault)
  793. def isupport_CHANMODES(self, params):
  794. """
  795. Available channel modes.
  796. There are 4 categories of channel mode::
  797. addressModes - Modes that add or remove an address to or from a
  798. list, these modes always take a parameter.
  799. param - Modes that change a setting on a channel, these modes
  800. always take a parameter.
  801. setParam - Modes that change a setting on a channel, these modes
  802. only take a parameter when being set.
  803. noParam - Modes that change a setting on a channel, these modes
  804. never take a parameter.
  805. """
  806. try:
  807. return self._parseChanModesParam(params)
  808. except ValueError:
  809. return self.getFeature("CHANMODES")
  810. def isupport_CHANNELLEN(self, params):
  811. """
  812. Maximum length of a channel name a client may create.
  813. """
  814. return _intOrDefault(params[0], self.getFeature("CHANNELLEN"))
  815. def isupport_CHANTYPES(self, params):
  816. """
  817. Valid channel prefixes.
  818. """
  819. return tuple(params[0])
  820. def isupport_EXCEPTS(self, params):
  821. """
  822. Mode character for "ban exceptions".
  823. The presence of this parameter indicates that the server supports
  824. this functionality.
  825. """
  826. return params[0] or "e"
  827. def isupport_IDCHAN(self, params):
  828. """
  829. Safe channel identifiers.
  830. The presence of this parameter indicates that the server supports
  831. this functionality.
  832. """
  833. return self._splitParamArgs(params)
  834. def isupport_INVEX(self, params):
  835. """
  836. Mode character for "invite exceptions".
  837. The presence of this parameter indicates that the server supports
  838. this functionality.
  839. """
  840. return params[0] or "I"
  841. def isupport_KICKLEN(self, params):
  842. """
  843. Maximum length of a kick message a client may provide.
  844. """
  845. return _intOrDefault(params[0])
  846. def isupport_MAXLIST(self, params):
  847. """
  848. Maximum number of "list modes" a client may set on a channel at once.
  849. List modes are identified by the "addressModes" key in CHANMODES.
  850. """
  851. return self._splitParamArgs(params, _intOrDefault)
  852. def isupport_MODES(self, params):
  853. """
  854. Maximum number of modes accepting parameters that may be sent, by a
  855. client, in a single MODE command.
  856. """
  857. return _intOrDefault(params[0])
  858. def isupport_NETWORK(self, params):
  859. """
  860. IRC network name.
  861. """
  862. return params[0]
  863. def isupport_NICKLEN(self, params):
  864. """
  865. Maximum length of a nickname the client may use.
  866. """
  867. return _intOrDefault(params[0], self.getFeature("NICKLEN"))
  868. def isupport_PREFIX(self, params):
  869. """
  870. Mapping of channel modes that clients may have to status flags.
  871. """
  872. try:
  873. return self._parsePrefixParam(params[0])
  874. except ValueError:
  875. return self.getFeature("PREFIX")
  876. def isupport_SAFELIST(self, params):
  877. """
  878. Flag indicating that a client may request a LIST without being
  879. disconnected due to the large amount of data generated.
  880. """
  881. return True
  882. def isupport_STATUSMSG(self, params):
  883. """
  884. The server supports sending messages to only to clients on a channel
  885. with a specific status.
  886. """
  887. return params[0]
  888. def isupport_TARGMAX(self, params):
  889. """
  890. Maximum number of targets allowable for commands that accept multiple
  891. targets.
  892. """
  893. return dict(self._splitParamArgs(params, _intOrDefault))
  894. def isupport_TOPICLEN(self, params):
  895. """
  896. Maximum length of a topic that may be set.
  897. """
  898. return _intOrDefault(params[0])
  899. class IRCClient(basic.LineReceiver):
  900. """
  901. Internet Relay Chat client protocol, with sprinkles.
  902. In addition to providing an interface for an IRC client protocol,
  903. this class also contains reasonable implementations of many common
  904. CTCP methods.
  905. TODO
  906. ====
  907. - Limit the length of messages sent (because the IRC server probably
  908. does).
  909. - Add flood protection/rate limiting for my CTCP replies.
  910. - NickServ cooperation. (a mix-in?)
  911. @ivar nickname: Nickname the client will use.
  912. @ivar password: Password used to log on to the server. May be L{None}.
  913. @ivar realname: Supplied to the server during login as the "Real name"
  914. or "ircname". May be L{None}.
  915. @ivar username: Supplied to the server during login as the "User name".
  916. May be L{None}
  917. @ivar userinfo: Sent in reply to a C{USERINFO} CTCP query. If L{None}, no
  918. USERINFO reply will be sent.
  919. "This is used to transmit a string which is settable by
  920. the user (and never should be set by the client)."
  921. @ivar fingerReply: Sent in reply to a C{FINGER} CTCP query. If L{None}, no
  922. FINGER reply will be sent.
  923. @type fingerReply: Callable or String
  924. @ivar versionName: CTCP VERSION reply, client name. If L{None}, no VERSION
  925. reply will be sent.
  926. @type versionName: C{str}, or None.
  927. @ivar versionNum: CTCP VERSION reply, client version.
  928. @type versionNum: C{str}, or None.
  929. @ivar versionEnv: CTCP VERSION reply, environment the client is running in.
  930. @type versionEnv: C{str}, or None.
  931. @ivar sourceURL: CTCP SOURCE reply, a URL where the source code of this
  932. client may be found. If L{None}, no SOURCE reply will be sent.
  933. @ivar lineRate: Minimum delay between lines sent to the server. If
  934. L{None}, no delay will be imposed.
  935. @type lineRate: Number of Seconds.
  936. @ivar motd: Either L{None} or, between receipt of I{RPL_MOTDSTART} and
  937. I{RPL_ENDOFMOTD}, a L{list} of L{str}, each of which is the content
  938. of an I{RPL_MOTD} message.
  939. @ivar erroneousNickFallback: Default nickname assigned when an unregistered
  940. client triggers an C{ERR_ERRONEUSNICKNAME} while trying to register
  941. with an illegal nickname.
  942. @type erroneousNickFallback: C{str}
  943. @ivar _registered: Whether or not the user is registered. It becomes True
  944. once a welcome has been received from the server.
  945. @type _registered: C{bool}
  946. @ivar _attemptedNick: The nickname that will try to get registered. It may
  947. change if it is illegal or already taken. L{nickname} becomes the
  948. L{_attemptedNick} that is successfully registered.
  949. @type _attemptedNick: C{str}
  950. @type supported: L{ServerSupportedFeatures}
  951. @ivar supported: Available ISUPPORT features on the server
  952. @type hostname: C{str}
  953. @ivar hostname: Host name of the IRC server the client is connected to.
  954. Initially the host name is L{None} and later is set to the host name
  955. from which the I{RPL_WELCOME} message is received.
  956. @type _heartbeat: L{task.LoopingCall}
  957. @ivar _heartbeat: Looping call to perform the keepalive by calling
  958. L{IRCClient._sendHeartbeat} every L{heartbeatInterval} seconds, or
  959. L{None} if there is no heartbeat.
  960. @type heartbeatInterval: C{float}
  961. @ivar heartbeatInterval: Interval, in seconds, to send I{PING} messages to
  962. the server as a form of keepalive, defaults to 120 seconds. Use L{None}
  963. to disable the heartbeat.
  964. """
  965. hostname = None
  966. motd = None
  967. nickname = "irc"
  968. password = None
  969. realname = None
  970. username = None
  971. ### Responses to various CTCP queries.
  972. userinfo = None
  973. # fingerReply is a callable returning a string, or a str()able object.
  974. fingerReply = None
  975. versionName = None
  976. versionNum = None
  977. versionEnv = None
  978. sourceURL = "http://twistedmatrix.com/downloads/"
  979. dcc_destdir = "."
  980. dcc_sessions = None
  981. # If this is false, no attempt will be made to identify
  982. # ourself to the server.
  983. performLogin = 1
  984. lineRate = None
  985. _queue = None
  986. _queueEmptying = None
  987. delimiter = b"\n" # b'\r\n' will also work (see dataReceived)
  988. __pychecker__ = "unusednames=params,prefix,channel"
  989. _registered = False
  990. _attemptedNick = ""
  991. erroneousNickFallback = "defaultnick"
  992. _heartbeat = None
  993. heartbeatInterval = 120
  994. def _reallySendLine(self, line):
  995. quoteLine = lowQuote(line)
  996. if isinstance(quoteLine, str):
  997. quoteLine = quoteLine.encode("utf-8")
  998. quoteLine += b"\r"
  999. return basic.LineReceiver.sendLine(self, quoteLine)
  1000. def sendLine(self, line):
  1001. if self.lineRate is None:
  1002. self._reallySendLine(line)
  1003. else:
  1004. self._queue.append(line)
  1005. if not self._queueEmptying:
  1006. self._sendLine()
  1007. def _sendLine(self):
  1008. if self._queue:
  1009. self._reallySendLine(self._queue.pop(0))
  1010. self._queueEmptying = reactor.callLater(self.lineRate, self._sendLine)
  1011. else:
  1012. self._queueEmptying = None
  1013. def connectionLost(self, reason):
  1014. basic.LineReceiver.connectionLost(self, reason)
  1015. self.stopHeartbeat()
  1016. def _createHeartbeat(self):
  1017. """
  1018. Create the heartbeat L{LoopingCall}.
  1019. """
  1020. return task.LoopingCall(self._sendHeartbeat)
  1021. def _sendHeartbeat(self):
  1022. """
  1023. Send a I{PING} message to the IRC server as a form of keepalive.
  1024. """
  1025. self.sendLine("PING " + self.hostname)
  1026. def stopHeartbeat(self):
  1027. """
  1028. Stop sending I{PING} messages to keep the connection to the server
  1029. alive.
  1030. @since: 11.1
  1031. """
  1032. if self._heartbeat is not None:
  1033. self._heartbeat.stop()
  1034. self._heartbeat = None
  1035. def startHeartbeat(self):
  1036. """
  1037. Start sending I{PING} messages every L{IRCClient.heartbeatInterval}
  1038. seconds to keep the connection to the server alive during periods of no
  1039. activity.
  1040. @since: 11.1
  1041. """
  1042. self.stopHeartbeat()
  1043. if self.heartbeatInterval is None:
  1044. return
  1045. self._heartbeat = self._createHeartbeat()
  1046. self._heartbeat.start(self.heartbeatInterval, now=False)
  1047. ### Interface level client->user output methods
  1048. ###
  1049. ### You'll want to override these.
  1050. ### Methods relating to the server itself
  1051. def created(self, when):
  1052. """
  1053. Called with creation date information about the server, usually at logon.
  1054. @type when: C{str}
  1055. @param when: A string describing when the server was created, probably.
  1056. """
  1057. def yourHost(self, info):
  1058. """
  1059. Called with daemon information about the server, usually at logon.
  1060. @type info: C{str}
  1061. @param info: A string describing what software the server is running, probably.
  1062. """
  1063. def myInfo(self, servername, version, umodes, cmodes):
  1064. """
  1065. Called with information about the server, usually at logon.
  1066. @type servername: C{str}
  1067. @param servername: The hostname of this server.
  1068. @type version: C{str}
  1069. @param version: A description of what software this server runs.
  1070. @type umodes: C{str}
  1071. @param umodes: All the available user modes.
  1072. @type cmodes: C{str}
  1073. @param cmodes: All the available channel modes.
  1074. """
  1075. def luserClient(self, info):
  1076. """
  1077. Called with information about the number of connections, usually at logon.
  1078. @type info: C{str}
  1079. @param info: A description of the number of clients and servers
  1080. connected to the network, probably.
  1081. """
  1082. def bounce(self, info):
  1083. """
  1084. Called with information about where the client should reconnect.
  1085. @type info: C{str}
  1086. @param info: A plaintext description of the address that should be
  1087. connected to.
  1088. """
  1089. def isupport(self, options):
  1090. """
  1091. Called with various information about what the server supports.
  1092. @type options: C{list} of C{str}
  1093. @param options: Descriptions of features or limits of the server, possibly
  1094. in the form "NAME=VALUE".
  1095. """
  1096. def luserChannels(self, channels):
  1097. """
  1098. Called with the number of channels existent on the server.
  1099. @type channels: C{int}
  1100. """
  1101. def luserOp(self, ops):
  1102. """
  1103. Called with the number of ops logged on to the server.
  1104. @type ops: C{int}
  1105. """
  1106. def luserMe(self, info):
  1107. """
  1108. Called with information about the server connected to.
  1109. @type info: C{str}
  1110. @param info: A plaintext string describing the number of users and servers
  1111. connected to this server.
  1112. """
  1113. ### Methods involving me directly
  1114. def privmsg(self, user, channel, message):
  1115. """
  1116. Called when I have a message from a user to me or a channel.
  1117. """
  1118. pass
  1119. def joined(self, channel):
  1120. """
  1121. Called when I finish joining a channel.
  1122. channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
  1123. intact.
  1124. """
  1125. def left(self, channel):
  1126. """
  1127. Called when I have left a channel.
  1128. channel has the starting character (C{'#'}, C{'&'}, C{'!'}, or C{'+'})
  1129. intact.
  1130. """
  1131. def noticed(self, user, channel, message):
  1132. """
  1133. Called when I have a notice from a user to me or a channel.
  1134. If the client makes any automated replies, it must not do so in
  1135. response to a NOTICE message, per the RFC::
  1136. The difference between NOTICE and PRIVMSG is that
  1137. automatic replies MUST NEVER be sent in response to a
  1138. NOTICE message. [...] The object of this rule is to avoid
  1139. loops between clients automatically sending something in
  1140. response to something it received.
  1141. """
  1142. def modeChanged(self, user, channel, set, modes, args):
  1143. """
  1144. Called when users or channel's modes are changed.
  1145. @type user: C{str}
  1146. @param user: The user and hostmask which instigated this change.
  1147. @type channel: C{str}
  1148. @param channel: The channel where the modes are changed. If args is
  1149. empty the channel for which the modes are changing. If the changes are
  1150. at server level it could be equal to C{user}.
  1151. @type set: C{bool} or C{int}
  1152. @param set: True if the mode(s) is being added, False if it is being
  1153. removed. If some modes are added and others removed at the same time
  1154. this function will be called twice, the first time with all the added
  1155. modes, the second with the removed ones. (To change this behaviour
  1156. override the irc_MODE method)
  1157. @type modes: C{str}
  1158. @param modes: The mode or modes which are being changed.
  1159. @type args: C{tuple}
  1160. @param args: Any additional information required for the mode
  1161. change.
  1162. """
  1163. def pong(self, user, secs):
  1164. """
  1165. Called with the results of a CTCP PING query.
  1166. """
  1167. pass
  1168. def signedOn(self):
  1169. """
  1170. Called after successfully signing on to the server.
  1171. """
  1172. pass
  1173. def kickedFrom(self, channel, kicker, message):
  1174. """
  1175. Called when I am kicked from a channel.
  1176. """
  1177. pass
  1178. def nickChanged(self, nick):
  1179. """
  1180. Called when my nick has been changed.
  1181. """
  1182. self.nickname = nick
  1183. ### Things I observe other people doing in a channel.
  1184. def userJoined(self, user, channel):
  1185. """
  1186. Called when I see another user joining a channel.
  1187. """
  1188. pass
  1189. def userLeft(self, user, channel):
  1190. """
  1191. Called when I see another user leaving a channel.
  1192. """
  1193. pass
  1194. def userQuit(self, user, quitMessage):
  1195. """
  1196. Called when I see another user disconnect from the network.
  1197. """
  1198. pass
  1199. def userKicked(self, kickee, channel, kicker, message):
  1200. """
  1201. Called when I observe someone else being kicked from a channel.
  1202. """
  1203. pass
  1204. def action(self, user, channel, data):
  1205. """
  1206. Called when I see a user perform an ACTION on a channel.
  1207. """
  1208. pass
  1209. def topicUpdated(self, user, channel, newTopic):
  1210. """
  1211. In channel, user changed the topic to newTopic.
  1212. Also called when first joining a channel.
  1213. """
  1214. pass
  1215. def userRenamed(self, oldname, newname):
  1216. """
  1217. A user changed their name from oldname to newname.
  1218. """
  1219. pass
  1220. ### Information from the server.
  1221. def receivedMOTD(self, motd):
  1222. """
  1223. I received a message-of-the-day banner from the server.
  1224. motd is a list of strings, where each string was sent as a separate
  1225. message from the server. To display, you might want to use::
  1226. '\\n'.join(motd)
  1227. to get a nicely formatted string.
  1228. """
  1229. pass
  1230. ### user input commands, client->server
  1231. ### Your client will want to invoke these.
  1232. def join(self, channel, key=None):
  1233. """
  1234. Join a channel.
  1235. @type channel: C{str}
  1236. @param channel: The name of the channel to join. If it has no prefix,
  1237. C{'#'} will be prepended to it.
  1238. @type key: C{str}
  1239. @param key: If specified, the key used to join the channel.
  1240. """
  1241. if channel[0] not in CHANNEL_PREFIXES:
  1242. channel = "#" + channel
  1243. if key:
  1244. self.sendLine(f"JOIN {channel} {key}")
  1245. else:
  1246. self.sendLine(f"JOIN {channel}")
  1247. def leave(self, channel, reason=None):
  1248. """
  1249. Leave a channel.
  1250. @type channel: C{str}
  1251. @param channel: The name of the channel to leave. If it has no prefix,
  1252. C{'#'} will be prepended to it.
  1253. @type reason: C{str}
  1254. @param reason: If given, the reason for leaving.
  1255. """
  1256. if channel[0] not in CHANNEL_PREFIXES:
  1257. channel = "#" + channel
  1258. if reason:
  1259. self.sendLine(f"PART {channel} :{reason}")
  1260. else:
  1261. self.sendLine(f"PART {channel}")
  1262. def kick(self, channel, user, reason=None):
  1263. """
  1264. Attempt to kick a user from a channel.
  1265. @type channel: C{str}
  1266. @param channel: The name of the channel to kick the user from. If it has
  1267. no prefix, C{'#'} will be prepended to it.
  1268. @type user: C{str}
  1269. @param user: The nick of the user to kick.
  1270. @type reason: C{str}
  1271. @param reason: If given, the reason for kicking the user.
  1272. """
  1273. if channel[0] not in CHANNEL_PREFIXES:
  1274. channel = "#" + channel
  1275. if reason:
  1276. self.sendLine(f"KICK {channel} {user} :{reason}")
  1277. else:
  1278. self.sendLine(f"KICK {channel} {user}")
  1279. part = leave
  1280. def invite(self, user, channel):
  1281. """
  1282. Attempt to invite user to channel
  1283. @type user: C{str}
  1284. @param user: The user to invite
  1285. @type channel: C{str}
  1286. @param channel: The channel to invite the user too
  1287. @since: 11.0
  1288. """
  1289. if channel[0] not in CHANNEL_PREFIXES:
  1290. channel = "#" + channel
  1291. self.sendLine(f"INVITE {user} {channel}")
  1292. def topic(self, channel, topic=None):
  1293. """
  1294. Attempt to set the topic of the given channel, or ask what it is.
  1295. If topic is None, then I sent a topic query instead of trying to set the
  1296. topic. The server should respond with a TOPIC message containing the
  1297. current topic of the given channel.
  1298. @type channel: C{str}
  1299. @param channel: The name of the channel to change the topic on. If it
  1300. has no prefix, C{'#'} will be prepended to it.
  1301. @type topic: C{str}
  1302. @param topic: If specified, what to set the topic to.
  1303. """
  1304. # << TOPIC #xtestx :fff
  1305. if channel[0] not in CHANNEL_PREFIXES:
  1306. channel = "#" + channel
  1307. if topic != None:
  1308. self.sendLine(f"TOPIC {channel} :{topic}")
  1309. else:
  1310. self.sendLine(f"TOPIC {channel}")
  1311. def mode(self, chan, set, modes, limit=None, user=None, mask=None):
  1312. """
  1313. Change the modes on a user or channel.
  1314. The C{limit}, C{user}, and C{mask} parameters are mutually exclusive.
  1315. @type chan: C{str}
  1316. @param chan: The name of the channel to operate on.
  1317. @type set: C{bool}
  1318. @param set: True to give the user or channel permissions and False to
  1319. remove them.
  1320. @type modes: C{str}
  1321. @param modes: The mode flags to set on the user or channel.
  1322. @type limit: C{int}
  1323. @param limit: In conjunction with the C{'l'} mode flag, limits the
  1324. number of users on the channel.
  1325. @type user: C{str}
  1326. @param user: The user to change the mode on.
  1327. @type mask: C{str}
  1328. @param mask: In conjunction with the C{'b'} mode flag, sets a mask of
  1329. users to be banned from the channel.
  1330. """
  1331. if set:
  1332. line = f"MODE {chan} +{modes}"
  1333. else:
  1334. line = f"MODE {chan} -{modes}"
  1335. if limit is not None:
  1336. line = "%s %d" % (line, limit)
  1337. elif user is not None:
  1338. line = f"{line} {user}"
  1339. elif mask is not None:
  1340. line = f"{line} {mask}"
  1341. self.sendLine(line)
  1342. def say(self, channel, message, length=None):
  1343. """
  1344. Send a message to a channel
  1345. @type channel: C{str}
  1346. @param channel: The channel to say the message on. If it has no prefix,
  1347. C{'#'} will be prepended to it.
  1348. @type message: C{str}
  1349. @param message: The message to say.
  1350. @type length: C{int}
  1351. @param length: The maximum number of octets to send at a time. This has
  1352. the effect of turning a single call to C{msg()} into multiple
  1353. commands to the server. This is useful when long messages may be
  1354. sent that would otherwise cause the server to kick us off or
  1355. silently truncate the text we are sending. If None is passed, the
  1356. entire message is always send in one command.
  1357. """
  1358. if channel[0] not in CHANNEL_PREFIXES:
  1359. channel = "#" + channel
  1360. self.msg(channel, message, length)
  1361. def _safeMaximumLineLength(self, command):
  1362. """
  1363. Estimate a safe maximum line length for the given command.
  1364. This is done by assuming the maximum values for nickname length,
  1365. realname and hostname combined with the command that needs to be sent
  1366. and some guessing. A theoretical maximum value is used because it is
  1367. possible that our nickname, username or hostname changes (on the server
  1368. side) while the length is still being calculated.
  1369. """
  1370. # :nickname!realname@hostname COMMAND ...
  1371. theoretical = ":{}!{}@{} {}".format(
  1372. "a" * self.supported.getFeature("NICKLEN"),
  1373. # This value is based on observation.
  1374. "b" * 10,
  1375. # See <http://tools.ietf.org/html/rfc2812#section-2.3.1>.
  1376. "c" * 63,
  1377. command,
  1378. )
  1379. # Fingers crossed.
  1380. fudge = 10
  1381. return MAX_COMMAND_LENGTH - len(theoretical) - fudge
  1382. def _sendMessage(self, msgType, user, message, length=None):
  1383. """
  1384. Send a message or notice to a user or channel.
  1385. The message will be split into multiple commands to the server if:
  1386. - The message contains any newline characters
  1387. - Any span between newline characters is longer than the given
  1388. line-length.
  1389. @param msgType: Whether a PRIVMSG or NOTICE should be sent.
  1390. @type msgType: C{str}
  1391. @param user: Username or channel name to which to direct the
  1392. message.
  1393. @type user: C{str}
  1394. @param message: Text to send.
  1395. @type message: C{str}
  1396. @param length: Maximum number of octets to send in a single
  1397. command, including the IRC protocol framing. If L{None} is given
  1398. then L{IRCClient._safeMaximumLineLength} is used to determine a
  1399. value.
  1400. @type length: C{int}
  1401. """
  1402. fmt = f"{msgType} {user} :"
  1403. if length is None:
  1404. length = self._safeMaximumLineLength(fmt)
  1405. # Account for the line terminator.
  1406. minimumLength = len(fmt) + 2
  1407. if length <= minimumLength:
  1408. raise ValueError(
  1409. "Maximum length must exceed %d for message "
  1410. "to %s" % (minimumLength, user)
  1411. )
  1412. for line in split(message, length - minimumLength):
  1413. self.sendLine(fmt + line)
  1414. def msg(self, user, message, length=None):
  1415. """
  1416. Send a message to a user or channel.
  1417. The message will be split into multiple commands to the server if:
  1418. - The message contains any newline characters
  1419. - Any span between newline characters is longer than the given
  1420. line-length.
  1421. @param user: Username or channel name to which to direct the
  1422. message.
  1423. @type user: C{str}
  1424. @param message: Text to send.
  1425. @type message: C{str}
  1426. @param length: Maximum number of octets to send in a single
  1427. command, including the IRC protocol framing. If L{None} is given
  1428. then L{IRCClient._safeMaximumLineLength} is used to determine a
  1429. value.
  1430. @type length: C{int}
  1431. """
  1432. self._sendMessage("PRIVMSG", user, message, length)
  1433. def notice(self, user, message, length=None):
  1434. """
  1435. Send a notice to a user.
  1436. Notices are like normal message, but should never get automated
  1437. replies.
  1438. @type user: C{str}
  1439. @param user: The user to send a notice to.
  1440. @type message: C{str}
  1441. @param message: The contents of the notice to send.
  1442. @param length: Maximum number of octets to send in a single
  1443. command, including the IRC protocol framing. If L{None} is given
  1444. then L{IRCClient._safeMaximumLineLength} is used to determine a
  1445. value.
  1446. @type length: C{int}
  1447. """
  1448. self._sendMessage("NOTICE", user, message, length)
  1449. def away(self, message=""):
  1450. """
  1451. Mark this client as away.
  1452. @type message: C{str}
  1453. @param message: If specified, the away message.
  1454. """
  1455. self.sendLine("AWAY :%s" % message)
  1456. def back(self):
  1457. """
  1458. Clear the away status.
  1459. """
  1460. # An empty away marks us as back
  1461. self.away()
  1462. def whois(self, nickname, server=None):
  1463. """
  1464. Retrieve user information about the given nickname.
  1465. @type nickname: C{str}
  1466. @param nickname: The nickname about which to retrieve information.
  1467. @since: 8.2
  1468. """
  1469. if server is None:
  1470. self.sendLine("WHOIS " + nickname)
  1471. else:
  1472. self.sendLine(f"WHOIS {server} {nickname}")
  1473. def register(self, nickname, hostname="foo", servername="bar"):
  1474. """
  1475. Login to the server.
  1476. @type nickname: C{str}
  1477. @param nickname: The nickname to register.
  1478. @type hostname: C{str}
  1479. @param hostname: If specified, the hostname to logon as.
  1480. @type servername: C{str}
  1481. @param servername: If specified, the servername to logon as.
  1482. """
  1483. if self.password is not None:
  1484. self.sendLine("PASS %s" % self.password)
  1485. self.setNick(nickname)
  1486. if self.username is None:
  1487. self.username = nickname
  1488. self.sendLine(
  1489. "USER {} {} {} :{}".format(
  1490. self.username, hostname, servername, self.realname
  1491. )
  1492. )
  1493. def setNick(self, nickname):
  1494. """
  1495. Set this client's nickname.
  1496. @type nickname: C{str}
  1497. @param nickname: The nickname to change to.
  1498. """
  1499. self._attemptedNick = nickname
  1500. self.sendLine("NICK %s" % nickname)
  1501. def quit(self, message=""):
  1502. """
  1503. Disconnect from the server
  1504. @type message: C{str}
  1505. @param message: If specified, the message to give when quitting the
  1506. server.
  1507. """
  1508. self.sendLine("QUIT :%s" % message)
  1509. ### user input commands, client->client
  1510. def describe(self, channel, action):
  1511. """
  1512. Strike a pose.
  1513. @type channel: C{str}
  1514. @param channel: The name of the channel to have an action on. If it
  1515. has no prefix, it is sent to the user of that name.
  1516. @type action: C{str}
  1517. @param action: The action to preform.
  1518. @since: 9.0
  1519. """
  1520. self.ctcpMakeQuery(channel, [("ACTION", action)])
  1521. _pings = None
  1522. _MAX_PINGRING = 12
  1523. def ping(self, user, text=None):
  1524. """
  1525. Measure round-trip delay to another IRC client.
  1526. """
  1527. if self._pings is None:
  1528. self._pings = {}
  1529. if text is None:
  1530. chars = string.ascii_letters + string.digits + string.punctuation
  1531. key = "".join([random.choice(chars) for i in range(12)])
  1532. else:
  1533. key = str(text)
  1534. self._pings[(user, key)] = time.time()
  1535. self.ctcpMakeQuery(user, [("PING", key)])
  1536. if len(self._pings) > self._MAX_PINGRING:
  1537. # Remove some of the oldest entries.
  1538. byValue = [(v, k) for (k, v) in self._pings.items()]
  1539. byValue.sort()
  1540. excess = len(self._pings) - self._MAX_PINGRING
  1541. for i in range(excess):
  1542. del self._pings[byValue[i][1]]
  1543. def dccSend(self, user, file):
  1544. """
  1545. This is supposed to send a user a file directly. This generally
  1546. doesn't work on any client, and this method is included only for
  1547. backwards compatibility and completeness.
  1548. @param user: C{str} representing the user
  1549. @param file: an open file (unknown, since this is not implemented)
  1550. """
  1551. raise NotImplementedError(
  1552. "XXX!!! Help! I need to bind a socket, have it listen, and tell me its address. "
  1553. "(and stop accepting once we've made a single connection.)"
  1554. )
  1555. def dccResume(self, user, fileName, port, resumePos):
  1556. """
  1557. Send a DCC RESUME request to another user.
  1558. """
  1559. self.ctcpMakeQuery(user, [("DCC", ["RESUME", fileName, port, resumePos])])
  1560. def dccAcceptResume(self, user, fileName, port, resumePos):
  1561. """
  1562. Send a DCC ACCEPT response to clients who have requested a resume.
  1563. """
  1564. self.ctcpMakeQuery(user, [("DCC", ["ACCEPT", fileName, port, resumePos])])
  1565. ### server->client messages
  1566. ### You might want to fiddle with these,
  1567. ### but it is safe to leave them alone.
  1568. def irc_ERR_NICKNAMEINUSE(self, prefix, params):
  1569. """
  1570. Called when we try to register or change to a nickname that is already
  1571. taken.
  1572. """
  1573. self._attemptedNick = self.alterCollidedNick(self._attemptedNick)
  1574. self.setNick(self._attemptedNick)
  1575. def alterCollidedNick(self, nickname):
  1576. """
  1577. Generate an altered version of a nickname that caused a collision in an
  1578. effort to create an unused related name for subsequent registration.
  1579. @param nickname: The nickname a user is attempting to register.
  1580. @type nickname: C{str}
  1581. @returns: A string that is in some way different from the nickname.
  1582. @rtype: C{str}
  1583. """
  1584. return nickname + "_"
  1585. def irc_ERR_ERRONEUSNICKNAME(self, prefix, params):
  1586. """
  1587. Called when we try to register or change to an illegal nickname.
  1588. The server should send this reply when the nickname contains any
  1589. disallowed characters. The bot will stall, waiting for RPL_WELCOME, if
  1590. we don't handle this during sign-on.
  1591. @note: The method uses the spelling I{erroneus}, as it appears in
  1592. the RFC, section 6.1.
  1593. """
  1594. if not self._registered:
  1595. self.setNick(self.erroneousNickFallback)
  1596. def irc_ERR_PASSWDMISMATCH(self, prefix, params):
  1597. """
  1598. Called when the login was incorrect.
  1599. """
  1600. raise IRCPasswordMismatch("Password Incorrect.")
  1601. def irc_RPL_WELCOME(self, prefix, params):
  1602. """
  1603. Called when we have received the welcome from the server.
  1604. """
  1605. self.hostname = prefix
  1606. self._registered = True
  1607. self.nickname = self._attemptedNick
  1608. self.signedOn()
  1609. self.startHeartbeat()
  1610. def irc_JOIN(self, prefix, params):
  1611. """
  1612. Called when a user joins a channel.
  1613. """
  1614. nick = prefix.split("!")[0]
  1615. channel = params[-1]
  1616. if nick == self.nickname:
  1617. self.joined(channel)
  1618. else:
  1619. self.userJoined(nick, channel)
  1620. def irc_PART(self, prefix, params):
  1621. """
  1622. Called when a user leaves a channel.
  1623. """
  1624. nick = prefix.split("!")[0]
  1625. channel = params[0]
  1626. if nick == self.nickname:
  1627. self.left(channel)
  1628. else:
  1629. self.userLeft(nick, channel)
  1630. def irc_QUIT(self, prefix, params):
  1631. """
  1632. Called when a user has quit.
  1633. """
  1634. nick = prefix.split("!")[0]
  1635. self.userQuit(nick, params[0])
  1636. def irc_MODE(self, user, params):
  1637. """
  1638. Parse a server mode change message.
  1639. """
  1640. channel, modes, args = params[0], params[1], params[2:]
  1641. if modes[0] not in "-+":
  1642. modes = "+" + modes
  1643. if channel == self.nickname:
  1644. # This is a mode change to our individual user, not a channel mode
  1645. # that involves us.
  1646. paramModes = self.getUserModeParams()
  1647. else:
  1648. paramModes = self.getChannelModeParams()
  1649. try:
  1650. added, removed = parseModes(modes, args, paramModes)
  1651. except IRCBadModes:
  1652. log.err(
  1653. None,
  1654. "An error occurred while parsing the following "
  1655. "MODE message: MODE %s" % (" ".join(params),),
  1656. )
  1657. else:
  1658. if added:
  1659. modes, params = zip(*added)
  1660. self.modeChanged(user, channel, True, "".join(modes), params)
  1661. if removed:
  1662. modes, params = zip(*removed)
  1663. self.modeChanged(user, channel, False, "".join(modes), params)
  1664. def irc_PING(self, prefix, params):
  1665. """
  1666. Called when some has pinged us.
  1667. """
  1668. self.sendLine("PONG %s" % params[-1])
  1669. def irc_PRIVMSG(self, prefix, params):
  1670. """
  1671. Called when we get a message.
  1672. """
  1673. user = prefix
  1674. channel = params[0]
  1675. message = params[-1]
  1676. if not message:
  1677. # Don't raise an exception if we get blank message.
  1678. return
  1679. if message[0] == X_DELIM:
  1680. m = ctcpExtract(message)
  1681. if m["extended"]:
  1682. self.ctcpQuery(user, channel, m["extended"])
  1683. if not m["normal"]:
  1684. return
  1685. message = " ".join(m["normal"])
  1686. self.privmsg(user, channel, message)
  1687. def irc_NOTICE(self, prefix, params):
  1688. """
  1689. Called when a user gets a notice.
  1690. """
  1691. user = prefix
  1692. channel = params[0]
  1693. message = params[-1]
  1694. if message[0] == X_DELIM:
  1695. m = ctcpExtract(message)
  1696. if m["extended"]:
  1697. self.ctcpReply(user, channel, m["extended"])
  1698. if not m["normal"]:
  1699. return
  1700. message = " ".join(m["normal"])
  1701. self.noticed(user, channel, message)
  1702. def irc_NICK(self, prefix, params):
  1703. """
  1704. Called when a user changes their nickname.
  1705. """
  1706. nick = prefix.split("!", 1)[0]
  1707. if nick == self.nickname:
  1708. self.nickChanged(params[0])
  1709. else:
  1710. self.userRenamed(nick, params[0])
  1711. def irc_KICK(self, prefix, params):
  1712. """
  1713. Called when a user is kicked from a channel.
  1714. """
  1715. kicker = prefix.split("!")[0]
  1716. channel = params[0]
  1717. kicked = params[1]
  1718. message = params[-1]
  1719. if kicked.lower() == self.nickname.lower():
  1720. # Yikes!
  1721. self.kickedFrom(channel, kicker, message)
  1722. else:
  1723. self.userKicked(kicked, channel, kicker, message)
  1724. def irc_TOPIC(self, prefix, params):
  1725. """
  1726. Someone in the channel set the topic.
  1727. """
  1728. user = prefix.split("!")[0]
  1729. channel = params[0]
  1730. newtopic = params[1]
  1731. self.topicUpdated(user, channel, newtopic)
  1732. def irc_RPL_TOPIC(self, prefix, params):
  1733. """
  1734. Called when the topic for a channel is initially reported or when it
  1735. subsequently changes.
  1736. """
  1737. user = prefix.split("!")[0]
  1738. channel = params[1]
  1739. newtopic = params[2]
  1740. self.topicUpdated(user, channel, newtopic)
  1741. def irc_RPL_NOTOPIC(self, prefix, params):
  1742. user = prefix.split("!")[0]
  1743. channel = params[1]
  1744. newtopic = ""
  1745. self.topicUpdated(user, channel, newtopic)
  1746. def irc_RPL_MOTDSTART(self, prefix, params):
  1747. if params[-1].startswith("- "):
  1748. params[-1] = params[-1][2:]
  1749. self.motd = [params[-1]]
  1750. def irc_RPL_MOTD(self, prefix, params):
  1751. if params[-1].startswith("- "):
  1752. params[-1] = params[-1][2:]
  1753. if self.motd is None:
  1754. self.motd = []
  1755. self.motd.append(params[-1])
  1756. def irc_RPL_ENDOFMOTD(self, prefix, params):
  1757. """
  1758. I{RPL_ENDOFMOTD} indicates the end of the message of the day
  1759. messages. Deliver the accumulated lines to C{receivedMOTD}.
  1760. """
  1761. motd = self.motd
  1762. self.motd = None
  1763. self.receivedMOTD(motd)
  1764. def irc_RPL_CREATED(self, prefix, params):
  1765. self.created(params[1])
  1766. def irc_RPL_YOURHOST(self, prefix, params):
  1767. self.yourHost(params[1])
  1768. def irc_RPL_MYINFO(self, prefix, params):
  1769. info = params[1].split(None, 3)
  1770. while len(info) < 4:
  1771. info.append(None)
  1772. self.myInfo(*info)
  1773. def irc_RPL_BOUNCE(self, prefix, params):
  1774. self.bounce(params[1])
  1775. def irc_RPL_ISUPPORT(self, prefix, params):
  1776. args = params[1:-1]
  1777. # Several ISUPPORT messages, in no particular order, may be sent
  1778. # to the client at any given point in time (usually only on connect,
  1779. # though.) For this reason, ServerSupportedFeatures.parse is intended
  1780. # to mutate the supported feature list.
  1781. self.supported.parse(args)
  1782. self.isupport(args)
  1783. def irc_RPL_LUSERCLIENT(self, prefix, params):
  1784. self.luserClient(params[1])
  1785. def irc_RPL_LUSEROP(self, prefix, params):
  1786. try:
  1787. self.luserOp(int(params[1]))
  1788. except ValueError:
  1789. pass
  1790. def irc_RPL_LUSERCHANNELS(self, prefix, params):
  1791. try:
  1792. self.luserChannels(int(params[1]))
  1793. except ValueError:
  1794. pass
  1795. def irc_RPL_LUSERME(self, prefix, params):
  1796. self.luserMe(params[1])
  1797. def irc_unknown(self, prefix, command, params):
  1798. pass
  1799. ### Receiving a CTCP query from another party
  1800. ### It is safe to leave these alone.
  1801. def ctcpQuery(self, user, channel, messages):
  1802. """
  1803. Dispatch method for any CTCP queries received.
  1804. Duplicated CTCP queries are ignored and no dispatch is
  1805. made. Unrecognized CTCP queries invoke L{IRCClient.ctcpUnknownQuery}.
  1806. """
  1807. seen = set()
  1808. for tag, data in messages:
  1809. method = getattr(self, "ctcpQuery_%s" % tag, None)
  1810. if tag not in seen:
  1811. if method is not None:
  1812. method(user, channel, data)
  1813. else:
  1814. self.ctcpUnknownQuery(user, channel, tag, data)
  1815. seen.add(tag)
  1816. def ctcpUnknownQuery(self, user, channel, tag, data):
  1817. """
  1818. Fallback handler for unrecognized CTCP queries.
  1819. No CTCP I{ERRMSG} reply is made to remove a potential denial of service
  1820. avenue.
  1821. """
  1822. log.msg(f"Unknown CTCP query from {user!r}: {tag!r} {data!r}")
  1823. def ctcpQuery_ACTION(self, user, channel, data):
  1824. self.action(user, channel, data)
  1825. def ctcpQuery_PING(self, user, channel, data):
  1826. nick = user.split("!")[0]
  1827. self.ctcpMakeReply(nick, [("PING", data)])
  1828. def ctcpQuery_FINGER(self, user, channel, data):
  1829. if data is not None:
  1830. self.quirkyMessage(f"Why did {user} send '{data}' with a FINGER query?")
  1831. if not self.fingerReply:
  1832. return
  1833. if callable(self.fingerReply):
  1834. reply = self.fingerReply()
  1835. else:
  1836. reply = str(self.fingerReply)
  1837. nick = user.split("!")[0]
  1838. self.ctcpMakeReply(nick, [("FINGER", reply)])
  1839. def ctcpQuery_VERSION(self, user, channel, data):
  1840. if data is not None:
  1841. self.quirkyMessage(f"Why did {user} send '{data}' with a VERSION query?")
  1842. if self.versionName:
  1843. nick = user.split("!")[0]
  1844. self.ctcpMakeReply(
  1845. nick,
  1846. [
  1847. (
  1848. "VERSION",
  1849. "%s:%s:%s"
  1850. % (
  1851. self.versionName,
  1852. self.versionNum or "",
  1853. self.versionEnv or "",
  1854. ),
  1855. )
  1856. ],
  1857. )
  1858. def ctcpQuery_SOURCE(self, user, channel, data):
  1859. if data is not None:
  1860. self.quirkyMessage(f"Why did {user} send '{data}' with a SOURCE query?")
  1861. if self.sourceURL:
  1862. nick = user.split("!")[0]
  1863. # The CTCP document (Zeuge, Rollo, Mesander 1994) says that SOURCE
  1864. # replies should be responded to with the location of an anonymous
  1865. # FTP server in host:directory:file format. I'm taking the liberty
  1866. # of bringing it into the 21st century by sending a URL instead.
  1867. self.ctcpMakeReply(nick, [("SOURCE", self.sourceURL), ("SOURCE", None)])
  1868. def ctcpQuery_USERINFO(self, user, channel, data):
  1869. if data is not None:
  1870. self.quirkyMessage(f"Why did {user} send '{data}' with a USERINFO query?")
  1871. if self.userinfo:
  1872. nick = user.split("!")[0]
  1873. self.ctcpMakeReply(nick, [("USERINFO", self.userinfo)])
  1874. def ctcpQuery_CLIENTINFO(self, user, channel, data):
  1875. """
  1876. A master index of what CTCP tags this client knows.
  1877. If no arguments are provided, respond with a list of known tags, sorted
  1878. in alphabetical order.
  1879. If an argument is provided, provide human-readable help on
  1880. the usage of that tag.
  1881. """
  1882. nick = user.split("!")[0]
  1883. if not data:
  1884. # XXX: prefixedMethodNames gets methods from my *class*,
  1885. # but it's entirely possible that this *instance* has more
  1886. # methods.
  1887. names = sorted(reflect.prefixedMethodNames(self.__class__, "ctcpQuery_"))
  1888. self.ctcpMakeReply(nick, [("CLIENTINFO", " ".join(names))])
  1889. else:
  1890. args = data.split()
  1891. method = getattr(self, f"ctcpQuery_{args[0]}", None)
  1892. if not method:
  1893. self.ctcpMakeReply(
  1894. nick,
  1895. [
  1896. (
  1897. "ERRMSG",
  1898. "CLIENTINFO %s :" "Unknown query '%s'" % (data, args[0]),
  1899. )
  1900. ],
  1901. )
  1902. return
  1903. doc = getattr(method, "__doc__", "")
  1904. self.ctcpMakeReply(nick, [("CLIENTINFO", doc)])
  1905. def ctcpQuery_ERRMSG(self, user, channel, data):
  1906. # Yeah, this seems strange, but that's what the spec says to do
  1907. # when faced with an ERRMSG query (not a reply).
  1908. nick = user.split("!")[0]
  1909. self.ctcpMakeReply(nick, [("ERRMSG", "%s :No error has occurred." % data)])
  1910. def ctcpQuery_TIME(self, user, channel, data):
  1911. if data is not None:
  1912. self.quirkyMessage(f"Why did {user} send '{data}' with a TIME query?")
  1913. nick = user.split("!")[0]
  1914. self.ctcpMakeReply(
  1915. nick, [("TIME", ":%s" % time.asctime(time.localtime(time.time())))]
  1916. )
  1917. def ctcpQuery_DCC(self, user, channel, data):
  1918. """
  1919. Initiate a Direct Client Connection
  1920. @param user: The hostmask of the user/client.
  1921. @type user: L{bytes}
  1922. @param channel: The name of the IRC channel.
  1923. @type channel: L{bytes}
  1924. @param data: The DCC request message.
  1925. @type data: L{bytes}
  1926. """
  1927. if not data:
  1928. return
  1929. dcctype = data.split(None, 1)[0].upper()
  1930. handler = getattr(self, "dcc_" + dcctype, None)
  1931. if handler:
  1932. if self.dcc_sessions is None:
  1933. self.dcc_sessions = []
  1934. data = data[len(dcctype) + 1 :]
  1935. handler(user, channel, data)
  1936. else:
  1937. nick = user.split("!")[0]
  1938. self.ctcpMakeReply(
  1939. nick,
  1940. [("ERRMSG", f"DCC {data} :Unknown DCC type '{dcctype}'")],
  1941. )
  1942. self.quirkyMessage(f"{user} offered unknown DCC type {dcctype}")
  1943. def dcc_SEND(self, user, channel, data):
  1944. # Use shlex.split for those who send files with spaces in the names.
  1945. data = shlex.split(data)
  1946. if len(data) < 3:
  1947. raise IRCBadMessage(f"malformed DCC SEND request: {data!r}")
  1948. (filename, address, port) = data[:3]
  1949. address = dccParseAddress(address)
  1950. try:
  1951. port = int(port)
  1952. except ValueError:
  1953. raise IRCBadMessage(f"Indecipherable port {port!r}")
  1954. size = -1
  1955. if len(data) >= 4:
  1956. try:
  1957. size = int(data[3])
  1958. except ValueError:
  1959. pass
  1960. # XXX Should we bother passing this data?
  1961. self.dccDoSend(user, address, port, filename, size, data)
  1962. def dcc_ACCEPT(self, user, channel, data):
  1963. data = shlex.split(data)
  1964. if len(data) < 3:
  1965. raise IRCBadMessage(f"malformed DCC SEND ACCEPT request: {data!r}")
  1966. (filename, port, resumePos) = data[:3]
  1967. try:
  1968. port = int(port)
  1969. resumePos = int(resumePos)
  1970. except ValueError:
  1971. return
  1972. self.dccDoAcceptResume(user, filename, port, resumePos)
  1973. def dcc_RESUME(self, user, channel, data):
  1974. data = shlex.split(data)
  1975. if len(data) < 3:
  1976. raise IRCBadMessage(f"malformed DCC SEND RESUME request: {data!r}")
  1977. (filename, port, resumePos) = data[:3]
  1978. try:
  1979. port = int(port)
  1980. resumePos = int(resumePos)
  1981. except ValueError:
  1982. return
  1983. self.dccDoResume(user, filename, port, resumePos)
  1984. def dcc_CHAT(self, user, channel, data):
  1985. data = shlex.split(data)
  1986. if len(data) < 3:
  1987. raise IRCBadMessage(f"malformed DCC CHAT request: {data!r}")
  1988. (filename, address, port) = data[:3]
  1989. address = dccParseAddress(address)
  1990. try:
  1991. port = int(port)
  1992. except ValueError:
  1993. raise IRCBadMessage(f"Indecipherable port {port!r}")
  1994. self.dccDoChat(user, channel, address, port, data)
  1995. ### The dccDo methods are the slightly higher-level siblings of
  1996. ### common dcc_ methods; the arguments have been parsed for them.
  1997. def dccDoSend(self, user, address, port, fileName, size, data):
  1998. """
  1999. Called when I receive a DCC SEND offer from a client.
  2000. By default, I do nothing here.
  2001. @param user: The hostmask of the requesting user.
  2002. @type user: L{bytes}
  2003. @param address: The IP address of the requesting user.
  2004. @type address: L{bytes}
  2005. @param port: An integer representing the port of the requesting user.
  2006. @type port: L{int}
  2007. @param fileName: The name of the file to be transferred.
  2008. @type fileName: L{bytes}
  2009. @param size: The size of the file to be transferred, which may be C{-1}
  2010. if the size of the file was not specified in the DCC SEND request.
  2011. @type size: L{int}
  2012. @param data: A 3-list of [fileName, address, port].
  2013. @type data: L{list}
  2014. """
  2015. def dccDoResume(self, user, file, port, resumePos):
  2016. """
  2017. Called when a client is trying to resume an offered file via DCC send.
  2018. It should be either replied to with a DCC ACCEPT or ignored (default).
  2019. @param user: The hostmask of the user who wants to resume the transfer
  2020. of a file previously offered via DCC send.
  2021. @type user: L{bytes}
  2022. @param file: The name of the file to resume the transfer of.
  2023. @type file: L{bytes}
  2024. @param port: An integer representing the port of the requesting user.
  2025. @type port: L{int}
  2026. @param resumePos: The position in the file from where the transfer
  2027. should resume.
  2028. @type resumePos: L{int}
  2029. """
  2030. pass
  2031. def dccDoAcceptResume(self, user, file, port, resumePos):
  2032. """
  2033. Called when a client has verified and accepted a DCC resume request
  2034. made by us. By default it will do nothing.
  2035. @param user: The hostmask of the user who has accepted the DCC resume
  2036. request.
  2037. @type user: L{bytes}
  2038. @param file: The name of the file to resume the transfer of.
  2039. @type file: L{bytes}
  2040. @param port: An integer representing the port of the accepting user.
  2041. @type port: L{int}
  2042. @param resumePos: The position in the file from where the transfer
  2043. should resume.
  2044. @type resumePos: L{int}
  2045. """
  2046. pass
  2047. def dccDoChat(self, user, channel, address, port, data):
  2048. pass
  2049. # factory = DccChatFactory(self, queryData=(user, channel, data))
  2050. # reactor.connectTCP(address, port, factory)
  2051. # self.dcc_sessions.append(factory)
  2052. # def ctcpQuery_SED(self, user, data):
  2053. # """Simple Encryption Doodoo
  2054. #
  2055. # Feel free to implement this, but no specification is available.
  2056. # """
  2057. # raise NotImplementedError
  2058. def ctcpMakeReply(self, user, messages):
  2059. """
  2060. Send one or more C{extended messages} as a CTCP reply.
  2061. @type messages: a list of extended messages. An extended
  2062. message is a (tag, data) tuple, where 'data' may be L{None}.
  2063. """
  2064. self.notice(user, ctcpStringify(messages))
  2065. ### client CTCP query commands
  2066. def ctcpMakeQuery(self, user, messages):
  2067. """
  2068. Send one or more C{extended messages} as a CTCP query.
  2069. @type messages: a list of extended messages. An extended
  2070. message is a (tag, data) tuple, where 'data' may be L{None}.
  2071. """
  2072. self.msg(user, ctcpStringify(messages))
  2073. ### Receiving a response to a CTCP query (presumably to one we made)
  2074. ### You may want to add methods here, or override UnknownReply.
  2075. def ctcpReply(self, user, channel, messages):
  2076. """
  2077. Dispatch method for any CTCP replies received.
  2078. """
  2079. for m in messages:
  2080. method = getattr(self, "ctcpReply_%s" % m[0], None)
  2081. if method:
  2082. method(user, channel, m[1])
  2083. else:
  2084. self.ctcpUnknownReply(user, channel, m[0], m[1])
  2085. def ctcpReply_PING(self, user, channel, data):
  2086. nick = user.split("!", 1)[0]
  2087. if (not self._pings) or ((nick, data) not in self._pings):
  2088. raise IRCBadMessage(f"Bogus PING response from {user}: {data}")
  2089. t0 = self._pings[(nick, data)]
  2090. self.pong(user, time.time() - t0)
  2091. def ctcpUnknownReply(self, user, channel, tag, data):
  2092. """
  2093. Called when a fitting ctcpReply_ method is not found.
  2094. @param user: The hostmask of the user.
  2095. @type user: L{bytes}
  2096. @param channel: The name of the IRC channel.
  2097. @type channel: L{bytes}
  2098. @param tag: The CTCP request tag for which no fitting method is found.
  2099. @type tag: L{bytes}
  2100. @param data: The CTCP message.
  2101. @type data: L{bytes}
  2102. """
  2103. # FIXME:7560:
  2104. # Add code for handling arbitrary queries and not treat them as
  2105. # anomalies.
  2106. log.msg(f"Unknown CTCP reply from {user}: {tag} {data}\n")
  2107. ### Error handlers
  2108. ### You may override these with something more appropriate to your UI.
  2109. def badMessage(self, line, excType, excValue, tb):
  2110. """
  2111. When I get a message that's so broken I can't use it.
  2112. @param line: The indecipherable message.
  2113. @type line: L{bytes}
  2114. @param excType: The exception type of the exception raised by the
  2115. message.
  2116. @type excType: L{type}
  2117. @param excValue: The exception parameter of excType or its associated
  2118. value(the second argument to C{raise}).
  2119. @type excValue: L{BaseException}
  2120. @param tb: The Traceback as a traceback object.
  2121. @type tb: L{traceback}
  2122. """
  2123. log.msg(line)
  2124. log.msg("".join(traceback.format_exception(excType, excValue, tb)))
  2125. def quirkyMessage(self, s):
  2126. """
  2127. This is called when I receive a message which is peculiar, but not
  2128. wholly indecipherable.
  2129. @param s: The peculiar message.
  2130. @type s: L{bytes}
  2131. """
  2132. log.msg(s + "\n")
  2133. ### Protocol methods
  2134. def connectionMade(self):
  2135. self.supported = ServerSupportedFeatures()
  2136. self._queue = []
  2137. if self.performLogin:
  2138. self.register(self.nickname)
  2139. def dataReceived(self, data):
  2140. if isinstance(data, str):
  2141. data = data.encode("utf-8")
  2142. data = data.replace(b"\r", b"")
  2143. basic.LineReceiver.dataReceived(self, data)
  2144. def lineReceived(self, line):
  2145. if isinstance(line, bytes):
  2146. line = line.decode("utf-8")
  2147. line = lowDequote(line)
  2148. try:
  2149. prefix, command, params = parsemsg(line)
  2150. if command in numeric_to_symbolic:
  2151. command = numeric_to_symbolic[command]
  2152. self.handleCommand(command, prefix, params)
  2153. except IRCBadMessage:
  2154. self.badMessage(line, *sys.exc_info())
  2155. def getUserModeParams(self):
  2156. """
  2157. Get user modes that require parameters for correct parsing.
  2158. @rtype: C{[str, str]}
  2159. @return: C{[add, remove]}
  2160. """
  2161. return ["", ""]
  2162. def getChannelModeParams(self):
  2163. """
  2164. Get channel modes that require parameters for correct parsing.
  2165. @rtype: C{[str, str]}
  2166. @return: C{[add, remove]}
  2167. """
  2168. # PREFIX modes are treated as "type B" CHANMODES, they always take
  2169. # parameter.
  2170. params = ["", ""]
  2171. prefixes = self.supported.getFeature("PREFIX", {})
  2172. params[0] = params[1] = "".join(prefixes.keys())
  2173. chanmodes = self.supported.getFeature("CHANMODES")
  2174. if chanmodes is not None:
  2175. params[0] += chanmodes.get("addressModes", "")
  2176. params[0] += chanmodes.get("param", "")
  2177. params[1] = params[0]
  2178. params[0] += chanmodes.get("setParam", "")
  2179. return params
  2180. def handleCommand(self, command, prefix, params):
  2181. """
  2182. Determine the function to call for the given command and call it with
  2183. the given arguments.
  2184. @param command: The IRC command to determine the function for.
  2185. @type command: L{bytes}
  2186. @param prefix: The prefix of the IRC message (as returned by
  2187. L{parsemsg}).
  2188. @type prefix: L{bytes}
  2189. @param params: A list of parameters to call the function with.
  2190. @type params: L{list}
  2191. """
  2192. method = getattr(self, "irc_%s" % command, None)
  2193. try:
  2194. if method is not None:
  2195. method(prefix, params)
  2196. else:
  2197. self.irc_unknown(prefix, command, params)
  2198. except BaseException:
  2199. log.deferr()
  2200. def __getstate__(self):
  2201. dct = self.__dict__.copy()
  2202. dct["dcc_sessions"] = None
  2203. dct["_pings"] = None
  2204. return dct
  2205. def dccParseAddress(address):
  2206. if "." in address:
  2207. pass
  2208. else:
  2209. try:
  2210. address = int(address)
  2211. except ValueError:
  2212. raise IRCBadMessage(f"Indecipherable address {address!r}")
  2213. else:
  2214. address = (
  2215. (address >> 24) & 0xFF,
  2216. (address >> 16) & 0xFF,
  2217. (address >> 8) & 0xFF,
  2218. address & 0xFF,
  2219. )
  2220. address = ".".join(map(str, address))
  2221. return address
  2222. class DccFileReceiveBasic(protocol.Protocol, styles.Ephemeral):
  2223. """
  2224. Bare protocol to receive a Direct Client Connection SEND stream.
  2225. This does enough to keep the other guy talking, but you'll want to extend
  2226. my dataReceived method to *do* something with the data I get.
  2227. @ivar bytesReceived: An integer representing the number of bytes of data
  2228. received.
  2229. @type bytesReceived: L{int}
  2230. """
  2231. bytesReceived = 0
  2232. def __init__(self, resumeOffset=0):
  2233. """
  2234. @param resumeOffset: An integer representing the amount of bytes from
  2235. where the transfer of data should be resumed.
  2236. @type resumeOffset: L{int}
  2237. """
  2238. self.bytesReceived = resumeOffset
  2239. self.resume = resumeOffset != 0
  2240. def dataReceived(self, data):
  2241. """
  2242. See: L{protocol.Protocol.dataReceived}
  2243. Warning: This just acknowledges to the remote host that the data has
  2244. been received; it doesn't I{do} anything with the data, so you'll want
  2245. to override this.
  2246. """
  2247. self.bytesReceived = self.bytesReceived + len(data)
  2248. self.transport.write(struct.pack("!i", self.bytesReceived))
  2249. class DccSendProtocol(protocol.Protocol, styles.Ephemeral):
  2250. """
  2251. Protocol for an outgoing Direct Client Connection SEND.
  2252. @ivar blocksize: An integer representing the size of an individual block of
  2253. data.
  2254. @type blocksize: L{int}
  2255. @ivar file: The file to be sent. This can be either a file object or
  2256. simply the name of the file.
  2257. @type file: L{file} or L{bytes}
  2258. @ivar bytesSent: An integer representing the number of bytes sent.
  2259. @type bytesSent: L{int}
  2260. @ivar completed: An integer representing whether the transfer has been
  2261. completed or not.
  2262. @type completed: L{int}
  2263. @ivar connected: An integer representing whether the connection has been
  2264. established or not.
  2265. @type connected: L{int}
  2266. """
  2267. blocksize = 1024
  2268. file = None
  2269. bytesSent = 0
  2270. completed = 0
  2271. connected = 0
  2272. def __init__(self, file):
  2273. if type(file) is str:
  2274. self.file = open(file)
  2275. def connectionMade(self):
  2276. self.connected = 1
  2277. self.sendBlock()
  2278. def dataReceived(self, data):
  2279. # XXX: Do we need to check to see if len(data) != fmtsize?
  2280. bytesShesGot = struct.unpack("!I", data)
  2281. if bytesShesGot < self.bytesSent:
  2282. # Wait for her.
  2283. # XXX? Add some checks to see if we've stalled out?
  2284. return
  2285. elif bytesShesGot > self.bytesSent:
  2286. # self.transport.log("DCC SEND %s: She says she has %d bytes "
  2287. # "but I've only sent %d. I'm stopping "
  2288. # "this screwy transfer."
  2289. # % (self.file,
  2290. # bytesShesGot, self.bytesSent))
  2291. self.transport.loseConnection()
  2292. return
  2293. self.sendBlock()
  2294. def sendBlock(self):
  2295. block = self.file.read(self.blocksize)
  2296. if block:
  2297. self.transport.write(block)
  2298. self.bytesSent = self.bytesSent + len(block)
  2299. else:
  2300. # Nothing more to send, transfer complete.
  2301. self.transport.loseConnection()
  2302. self.completed = 1
  2303. def connectionLost(self, reason):
  2304. self.connected = 0
  2305. if hasattr(self.file, "close"):
  2306. self.file.close()
  2307. class DccSendFactory(protocol.Factory):
  2308. protocol = DccSendProtocol # type: ignore[assignment]
  2309. def __init__(self, file):
  2310. self.file = file
  2311. def buildProtocol(self, connection):
  2312. p = self.protocol(self.file)
  2313. p.factory = self
  2314. return p
  2315. def fileSize(file):
  2316. """
  2317. I'll try my damndest to determine the size of this file object.
  2318. @param file: The file object to determine the size of.
  2319. @type file: L{io.IOBase}
  2320. @rtype: L{int} or L{None}
  2321. @return: The size of the file object as an integer if it can be determined,
  2322. otherwise return L{None}.
  2323. """
  2324. size = None
  2325. if hasattr(file, "fileno"):
  2326. fileno = file.fileno()
  2327. try:
  2328. stat_ = os.fstat(fileno)
  2329. size = stat_[stat.ST_SIZE]
  2330. except BaseException:
  2331. pass
  2332. else:
  2333. return size
  2334. if hasattr(file, "name") and path.exists(file.name):
  2335. try:
  2336. size = path.getsize(file.name)
  2337. except BaseException:
  2338. pass
  2339. else:
  2340. return size
  2341. if hasattr(file, "seek") and hasattr(file, "tell"):
  2342. try:
  2343. try:
  2344. file.seek(0, 2)
  2345. size = file.tell()
  2346. finally:
  2347. file.seek(0, 0)
  2348. except BaseException:
  2349. pass
  2350. else:
  2351. return size
  2352. return size
  2353. class DccChat(basic.LineReceiver, styles.Ephemeral):
  2354. """
  2355. Direct Client Connection protocol type CHAT.
  2356. DCC CHAT is really just your run o' the mill basic.LineReceiver
  2357. protocol. This class only varies from that slightly, accepting
  2358. either LF or CR LF for a line delimeter for incoming messages
  2359. while always using CR LF for outgoing.
  2360. The lineReceived method implemented here uses the DCC connection's
  2361. 'client' attribute (provided upon construction) to deliver incoming
  2362. lines from the DCC chat via IRCClient's normal privmsg interface.
  2363. That's something of a spoof, which you may well want to override.
  2364. """
  2365. queryData = None
  2366. delimiter = CR.encode("ascii") + NL.encode("ascii")
  2367. client = None
  2368. remoteParty = None
  2369. buffer = b""
  2370. def __init__(self, client, queryData=None):
  2371. """
  2372. Initialize a new DCC CHAT session.
  2373. queryData is a 3-tuple of
  2374. (fromUser, targetUserOrChannel, data)
  2375. as received by the CTCP query.
  2376. (To be honest, fromUser is the only thing that's currently
  2377. used here. targetUserOrChannel is potentially useful, while
  2378. the 'data' argument is solely for informational purposes.)
  2379. """
  2380. self.client = client
  2381. if queryData:
  2382. self.queryData = queryData
  2383. self.remoteParty = self.queryData[0]
  2384. def dataReceived(self, data):
  2385. self.buffer = self.buffer + data
  2386. lines = self.buffer.split(LF)
  2387. # Put the (possibly empty) element after the last LF back in the
  2388. # buffer
  2389. self.buffer = lines.pop()
  2390. for line in lines:
  2391. if line[-1] == CR:
  2392. line = line[:-1]
  2393. self.lineReceived(line)
  2394. def lineReceived(self, line):
  2395. log.msg(f"DCC CHAT<{self.remoteParty}> {line}")
  2396. self.client.privmsg(self.remoteParty, self.client.nickname, line)
  2397. class DccChatFactory(protocol.ClientFactory):
  2398. protocol = DccChat # type: ignore[assignment]
  2399. noisy = False
  2400. def __init__(self, client, queryData):
  2401. self.client = client
  2402. self.queryData = queryData
  2403. def buildProtocol(self, addr):
  2404. p = self.protocol(client=self.client, queryData=self.queryData)
  2405. p.factory = self
  2406. return p
  2407. def clientConnectionFailed(self, unused_connector, unused_reason):
  2408. self.client.dcc_sessions.remove(self)
  2409. def clientConnectionLost(self, unused_connector, unused_reason):
  2410. self.client.dcc_sessions.remove(self)
  2411. def dccDescribe(data):
  2412. """
  2413. Given the data chunk from a DCC query, return a descriptive string.
  2414. @param data: The data from a DCC query.
  2415. @type data: L{bytes}
  2416. @rtype: L{bytes}
  2417. @return: A descriptive string.
  2418. """
  2419. orig_data = data
  2420. data = data.split()
  2421. if len(data) < 4:
  2422. return orig_data
  2423. (dcctype, arg, address, port) = data[:4]
  2424. if "." in address:
  2425. pass
  2426. else:
  2427. try:
  2428. address = int(address)
  2429. except ValueError:
  2430. pass
  2431. else:
  2432. address = (
  2433. (address >> 24) & 0xFF,
  2434. (address >> 16) & 0xFF,
  2435. (address >> 8) & 0xFF,
  2436. address & 0xFF,
  2437. )
  2438. address = ".".join(map(str, address))
  2439. if dcctype == "SEND":
  2440. filename = arg
  2441. size_txt = ""
  2442. if len(data) >= 5:
  2443. try:
  2444. size = int(data[4])
  2445. size_txt = " of size %d bytes" % (size,)
  2446. except ValueError:
  2447. pass
  2448. dcc_text = "SEND for file '{}'{} at host {}, port {}".format(
  2449. filename,
  2450. size_txt,
  2451. address,
  2452. port,
  2453. )
  2454. elif dcctype == "CHAT":
  2455. dcc_text = f"CHAT for host {address}, port {port}"
  2456. else:
  2457. dcc_text = orig_data
  2458. return dcc_text
  2459. class DccFileReceive(DccFileReceiveBasic):
  2460. """
  2461. Higher-level coverage for getting a file from DCC SEND.
  2462. I allow you to change the file's name and destination directory. I won't
  2463. overwrite an existing file unless I've been told it's okay to do so. If
  2464. passed the resumeOffset keyword argument I will attempt to resume the file
  2465. from that amount of bytes.
  2466. XXX: I need to let the client know when I am finished.
  2467. XXX: I need to decide how to keep a progress indicator updated.
  2468. XXX: Client needs a way to tell me "Do not finish until I say so."
  2469. XXX: I need to make sure the client understands if the file cannot be written.
  2470. @ivar filename: The name of the file to get.
  2471. @type filename: L{bytes}
  2472. @ivar fileSize: The size of the file to get, which has a default value of
  2473. C{-1} if the size of the file was not specified in the DCC SEND
  2474. request.
  2475. @type fileSize: L{int}
  2476. @ivar destDir: The destination directory for the file to be received.
  2477. @type destDir: L{bytes}
  2478. @ivar overwrite: An integer representing whether an existing file should be
  2479. overwritten or not. This initially is an L{int} but can be modified to
  2480. be a L{bool} using the L{set_overwrite} method.
  2481. @type overwrite: L{int} or L{bool}
  2482. @ivar queryData: queryData is a 3-tuple of (user, channel, data).
  2483. @type queryData: L{tuple}
  2484. @ivar fromUser: This is the hostmask of the requesting user and is found at
  2485. index 0 of L{queryData}.
  2486. @type fromUser: L{bytes}
  2487. """
  2488. filename = "dcc"
  2489. fileSize = -1
  2490. destDir = "."
  2491. overwrite = 0
  2492. fromUser: Optional[bytes] = None
  2493. queryData = None
  2494. def __init__(
  2495. self, filename, fileSize=-1, queryData=None, destDir=".", resumeOffset=0
  2496. ):
  2497. DccFileReceiveBasic.__init__(self, resumeOffset=resumeOffset)
  2498. self.filename = filename
  2499. self.destDir = destDir
  2500. self.fileSize = fileSize
  2501. self._resumeOffset = resumeOffset
  2502. if queryData:
  2503. self.queryData = queryData
  2504. self.fromUser = self.queryData[0]
  2505. def set_directory(self, directory):
  2506. """
  2507. Set the directory where the downloaded file will be placed.
  2508. May raise OSError if the supplied directory path is not suitable.
  2509. @param directory: The directory where the file to be received will be
  2510. placed.
  2511. @type directory: L{bytes}
  2512. """
  2513. if not path.exists(directory):
  2514. raise OSError(errno.ENOENT, "You see no directory there.", directory)
  2515. if not path.isdir(directory):
  2516. raise OSError(
  2517. errno.ENOTDIR,
  2518. "You cannot put a file into " "something which is not a directory.",
  2519. directory,
  2520. )
  2521. if not os.access(directory, os.X_OK | os.W_OK):
  2522. raise OSError(
  2523. errno.EACCES, "This directory is too hard to write in to.", directory
  2524. )
  2525. self.destDir = directory
  2526. def set_filename(self, filename):
  2527. """
  2528. Change the name of the file being transferred.
  2529. This replaces the file name provided by the sender.
  2530. @param filename: The new name for the file.
  2531. @type filename: L{bytes}
  2532. """
  2533. self.filename = filename
  2534. def set_overwrite(self, boolean):
  2535. """
  2536. May I overwrite existing files?
  2537. @param boolean: A boolean value representing whether existing files
  2538. should be overwritten or not.
  2539. @type boolean: L{bool}
  2540. """
  2541. self.overwrite = boolean
  2542. # Protocol-level methods.
  2543. def connectionMade(self):
  2544. dst = path.abspath(path.join(self.destDir, self.filename))
  2545. exists = path.exists(dst)
  2546. if self.resume and exists:
  2547. # I have been told I want to resume, and a file already
  2548. # exists - Here we go
  2549. self.file = open(dst, "rb+")
  2550. self.file.seek(self._resumeOffset)
  2551. self.file.truncate()
  2552. log.msg(
  2553. "Attempting to resume %s - starting from %d bytes"
  2554. % (self.file, self.file.tell())
  2555. )
  2556. elif self.resume and not exists:
  2557. raise OSError(
  2558. errno.ENOENT,
  2559. "You cannot resume writing to a file " "that does not exist!",
  2560. dst,
  2561. )
  2562. elif self.overwrite or not exists:
  2563. self.file = open(dst, "wb")
  2564. else:
  2565. raise OSError(
  2566. errno.EEXIST,
  2567. "There's a file in the way. " "Perhaps that's why you cannot open it.",
  2568. dst,
  2569. )
  2570. def dataReceived(self, data):
  2571. self.file.write(data)
  2572. DccFileReceiveBasic.dataReceived(self, data)
  2573. # XXX: update a progress indicator here?
  2574. def connectionLost(self, reason):
  2575. """
  2576. When the connection is lost, I close the file.
  2577. @param reason: The reason why the connection was lost.
  2578. @type reason: L{Failure}
  2579. """
  2580. self.connected = 0
  2581. logmsg = f"{self} closed."
  2582. if self.fileSize > 0:
  2583. logmsg = "%s %d/%d bytes received" % (
  2584. logmsg,
  2585. self.bytesReceived,
  2586. self.fileSize,
  2587. )
  2588. if self.bytesReceived == self.fileSize:
  2589. pass # Hooray!
  2590. elif self.bytesReceived < self.fileSize:
  2591. logmsg = "%s (Warning: %d bytes short)" % (
  2592. logmsg,
  2593. self.fileSize - self.bytesReceived,
  2594. )
  2595. else:
  2596. logmsg = f"{logmsg} (file larger than expected)"
  2597. else:
  2598. logmsg = "%s %d bytes received" % (logmsg, self.bytesReceived)
  2599. if hasattr(self, "file"):
  2600. logmsg = f"{logmsg} and written to {self.file.name}.\n"
  2601. if hasattr(self.file, "close"):
  2602. self.file.close()
  2603. # self.transport.log(logmsg)
  2604. def __str__(self) -> str:
  2605. if not self.connected:
  2606. return f"<Unconnected DccFileReceive object at {id(self):x}>"
  2607. transport = self.transport
  2608. assert transport is not None
  2609. from_ = str(transport.getPeer())
  2610. if self.fromUser is not None:
  2611. from_ = f"{self.fromUser!r} ({from_})"
  2612. s = f"DCC transfer of '{self.filename}' from {from_}"
  2613. return s
  2614. def __repr__(self) -> str:
  2615. s = f"<{self.__class__} at {id(self):x}: GET {self.filename}>"
  2616. return s
  2617. _OFF = "\x0f"
  2618. _BOLD = "\x02"
  2619. _COLOR = "\x03"
  2620. _REVERSE_VIDEO = "\x16"
  2621. _UNDERLINE = "\x1f"
  2622. # Mapping of IRC color names to their color values.
  2623. _IRC_COLORS = dict(
  2624. zip(
  2625. [
  2626. "white",
  2627. "black",
  2628. "blue",
  2629. "green",
  2630. "lightRed",
  2631. "red",
  2632. "magenta",
  2633. "orange",
  2634. "yellow",
  2635. "lightGreen",
  2636. "cyan",
  2637. "lightCyan",
  2638. "lightBlue",
  2639. "lightMagenta",
  2640. "gray",
  2641. "lightGray",
  2642. ],
  2643. range(16),
  2644. )
  2645. )
  2646. # Mapping of IRC color values to their color names.
  2647. _IRC_COLOR_NAMES = {code: name for name, code in _IRC_COLORS.items()}
  2648. class _CharacterAttributes(_textattributes.CharacterAttributesMixin):
  2649. """
  2650. Factory for character attributes, including foreground and background color
  2651. and non-color attributes such as bold, reverse video and underline.
  2652. Character attributes are applied to actual text by using object
  2653. indexing-syntax (C{obj['abc']}) after accessing a factory attribute, for
  2654. example::
  2655. attributes.bold['Some text']
  2656. These can be nested to mix attributes::
  2657. attributes.bold[attributes.underline['Some text']]
  2658. And multiple values can be passed::
  2659. attributes.normal[attributes.bold['Some'], ' text']
  2660. Non-color attributes can be accessed by attribute name, available
  2661. attributes are:
  2662. - bold
  2663. - reverseVideo
  2664. - underline
  2665. Available colors are:
  2666. 0. white
  2667. 1. black
  2668. 2. blue
  2669. 3. green
  2670. 4. light red
  2671. 5. red
  2672. 6. magenta
  2673. 7. orange
  2674. 8. yellow
  2675. 9. light green
  2676. 10. cyan
  2677. 11. light cyan
  2678. 12. light blue
  2679. 13. light magenta
  2680. 14. gray
  2681. 15. light gray
  2682. @ivar fg: Foreground colors accessed by attribute name, see above
  2683. for possible names.
  2684. @ivar bg: Background colors accessed by attribute name, see above
  2685. for possible names.
  2686. @since: 13.1
  2687. """
  2688. fg = _textattributes._ColorAttribute(
  2689. _textattributes._ForegroundColorAttr, _IRC_COLORS
  2690. )
  2691. bg = _textattributes._ColorAttribute(
  2692. _textattributes._BackgroundColorAttr, _IRC_COLORS
  2693. )
  2694. attrs = {"bold": _BOLD, "reverseVideo": _REVERSE_VIDEO, "underline": _UNDERLINE}
  2695. attributes = _CharacterAttributes()
  2696. class _FormattingState(_textattributes._FormattingStateMixin):
  2697. """
  2698. Formatting state/attributes of a single character.
  2699. Attributes include:
  2700. - Formatting nullifier
  2701. - Bold
  2702. - Underline
  2703. - Reverse video
  2704. - Foreground color
  2705. - Background color
  2706. @since: 13.1
  2707. """
  2708. compareAttributes = (
  2709. "off",
  2710. "bold",
  2711. "underline",
  2712. "reverseVideo",
  2713. "foreground",
  2714. "background",
  2715. )
  2716. def __init__(
  2717. self,
  2718. off=False,
  2719. bold=False,
  2720. underline=False,
  2721. reverseVideo=False,
  2722. foreground=None,
  2723. background=None,
  2724. ):
  2725. self.off = off
  2726. self.bold = bold
  2727. self.underline = underline
  2728. self.reverseVideo = reverseVideo
  2729. self.foreground = foreground
  2730. self.background = background
  2731. def toMIRCControlCodes(self):
  2732. """
  2733. Emit a mIRC control sequence that will set up all the attributes this
  2734. formatting state has set.
  2735. @return: A string containing mIRC control sequences that mimic this
  2736. formatting state.
  2737. """
  2738. attrs = []
  2739. if self.bold:
  2740. attrs.append(_BOLD)
  2741. if self.underline:
  2742. attrs.append(_UNDERLINE)
  2743. if self.reverseVideo:
  2744. attrs.append(_REVERSE_VIDEO)
  2745. if self.foreground is not None or self.background is not None:
  2746. c = ""
  2747. if self.foreground is not None:
  2748. c += "%02d" % (self.foreground,)
  2749. if self.background is not None:
  2750. c += ",%02d" % (self.background,)
  2751. attrs.append(_COLOR + c)
  2752. return _OFF + "".join(map(str, attrs))
  2753. def _foldr(f, z, xs):
  2754. """
  2755. Apply a function of two arguments cumulatively to the items of
  2756. a sequence, from right to left, so as to reduce the sequence to
  2757. a single value.
  2758. @type f: C{callable} taking 2 arguments
  2759. @param z: Initial value.
  2760. @param xs: Sequence to reduce.
  2761. @return: Single value resulting from reducing C{xs}.
  2762. """
  2763. return reduce(lambda x, y: f(y, x), reversed(xs), z)
  2764. class _FormattingParser(_CommandDispatcherMixin):
  2765. """
  2766. A finite-state machine that parses formatted IRC text.
  2767. Currently handled formatting includes: bold, reverse, underline,
  2768. mIRC color codes and the ability to remove all current formatting.
  2769. @see: U{http://www.mirc.co.uk/help/color.txt}
  2770. @type _formatCodes: C{dict} mapping C{str} to C{str}
  2771. @cvar _formatCodes: Mapping of format code values to names.
  2772. @type state: C{str}
  2773. @ivar state: Current state of the finite-state machine.
  2774. @type _buffer: C{str}
  2775. @ivar _buffer: Buffer, containing the text content, of the formatting
  2776. sequence currently being parsed, the buffer is used as the content for
  2777. L{_attrs} before being added to L{_result} and emptied upon calling
  2778. L{emit}.
  2779. @type _attrs: C{set}
  2780. @ivar _attrs: Set of the applicable formatting states (bold, underline,
  2781. etc.) for the current L{_buffer}, these are applied to L{_buffer} when
  2782. calling L{emit}.
  2783. @type foreground: L{_ForegroundColorAttr}
  2784. @ivar foreground: Current foreground color attribute, or L{None}.
  2785. @type background: L{_BackgroundColorAttr}
  2786. @ivar background: Current background color attribute, or L{None}.
  2787. @ivar _result: Current parse result.
  2788. """
  2789. prefix = "state"
  2790. _formatCodes = {
  2791. _OFF: "off",
  2792. _BOLD: "bold",
  2793. _COLOR: "color",
  2794. _REVERSE_VIDEO: "reverseVideo",
  2795. _UNDERLINE: "underline",
  2796. }
  2797. def __init__(self):
  2798. self.state = "TEXT"
  2799. self._buffer = ""
  2800. self._attrs = set()
  2801. self._result = None
  2802. self.foreground = None
  2803. self.background = None
  2804. def process(self, ch):
  2805. """
  2806. Handle input.
  2807. @type ch: C{str}
  2808. @param ch: A single character of input to process
  2809. """
  2810. self.dispatch(self.state, ch)
  2811. def complete(self):
  2812. """
  2813. Flush the current buffer and return the final parsed result.
  2814. @return: Structured text and attributes.
  2815. """
  2816. self.emit()
  2817. if self._result is None:
  2818. self._result = attributes.normal
  2819. return self._result
  2820. def emit(self):
  2821. """
  2822. Add the currently parsed input to the result.
  2823. """
  2824. if self._buffer:
  2825. attrs = [getattr(attributes, name) for name in self._attrs]
  2826. attrs.extend(filter(None, [self.foreground, self.background]))
  2827. if not attrs:
  2828. attrs.append(attributes.normal)
  2829. attrs.append(self._buffer)
  2830. attr = _foldr(operator.getitem, attrs.pop(), attrs)
  2831. if self._result is None:
  2832. self._result = attr
  2833. else:
  2834. self._result[attr]
  2835. self._buffer = ""
  2836. def state_TEXT(self, ch):
  2837. """
  2838. Handle the "text" state.
  2839. Along with regular text, single token formatting codes are handled
  2840. in this state too.
  2841. @param ch: The character being processed.
  2842. """
  2843. formatName = self._formatCodes.get(ch)
  2844. if formatName == "color":
  2845. self.emit()
  2846. self.state = "COLOR_FOREGROUND"
  2847. else:
  2848. if formatName is None:
  2849. self._buffer += ch
  2850. else:
  2851. self.emit()
  2852. if formatName == "off":
  2853. self._attrs = set()
  2854. self.foreground = self.background = None
  2855. else:
  2856. self._attrs.symmetric_difference_update([formatName])
  2857. def state_COLOR_FOREGROUND(self, ch):
  2858. """
  2859. Handle the foreground color state.
  2860. Foreground colors can consist of up to two digits and may optionally
  2861. end in a I{,}. Any non-digit or non-comma characters are treated as
  2862. invalid input and result in the state being reset to "text".
  2863. @param ch: The character being processed.
  2864. """
  2865. # Color codes may only be a maximum of two characters.
  2866. if ch.isdigit() and len(self._buffer) < 2:
  2867. self._buffer += ch
  2868. else:
  2869. if self._buffer:
  2870. # Wrap around for color numbers higher than we support, like
  2871. # most other IRC clients.
  2872. col = int(self._buffer) % len(_IRC_COLORS)
  2873. self.foreground = getattr(attributes.fg, _IRC_COLOR_NAMES[col])
  2874. else:
  2875. # If there were no digits, then this has been an empty color
  2876. # code and we can reset the color state.
  2877. self.foreground = self.background = None
  2878. if ch == "," and self._buffer:
  2879. # If there's a comma and it's not the first thing, move on to
  2880. # the background state.
  2881. self._buffer = ""
  2882. self.state = "COLOR_BACKGROUND"
  2883. else:
  2884. # Otherwise, this is a bogus color code, fall back to text.
  2885. self._buffer = ""
  2886. self.state = "TEXT"
  2887. self.emit()
  2888. self.process(ch)
  2889. def state_COLOR_BACKGROUND(self, ch):
  2890. """
  2891. Handle the background color state.
  2892. Background colors can consist of up to two digits and must occur after
  2893. a foreground color and must be preceded by a I{,}. Any non-digit
  2894. character is treated as invalid input and results in the state being
  2895. set to "text".
  2896. @param ch: The character being processed.
  2897. """
  2898. # Color codes may only be a maximum of two characters.
  2899. if ch.isdigit() and len(self._buffer) < 2:
  2900. self._buffer += ch
  2901. else:
  2902. if self._buffer:
  2903. # Wrap around for color numbers higher than we support, like
  2904. # most other IRC clients.
  2905. col = int(self._buffer) % len(_IRC_COLORS)
  2906. self.background = getattr(attributes.bg, _IRC_COLOR_NAMES[col])
  2907. self._buffer = ""
  2908. self.emit()
  2909. self.state = "TEXT"
  2910. self.process(ch)
  2911. def parseFormattedText(text):
  2912. """
  2913. Parse text containing IRC formatting codes into structured information.
  2914. Color codes are mapped from 0 to 15 and wrap around if greater than 15.
  2915. @type text: C{str}
  2916. @param text: Formatted text to parse.
  2917. @return: Structured text and attributes.
  2918. @since: 13.1
  2919. """
  2920. state = _FormattingParser()
  2921. for ch in text:
  2922. state.process(ch)
  2923. return state.complete()
  2924. def assembleFormattedText(formatted):
  2925. """
  2926. Assemble formatted text from structured information.
  2927. Currently handled formatting includes: bold, reverse, underline,
  2928. mIRC color codes and the ability to remove all current formatting.
  2929. It is worth noting that assembled text will always begin with the control
  2930. code to disable other attributes for the sake of correctness.
  2931. For example::
  2932. from twisted.words.protocols.irc import attributes as A
  2933. assembleFormattedText(
  2934. A.normal[A.bold['Time: '], A.fg.lightRed['Now!']])
  2935. Would produce "Time: " in bold formatting, followed by "Now!" with a
  2936. foreground color of light red and without any additional formatting.
  2937. Available attributes are:
  2938. - bold
  2939. - reverseVideo
  2940. - underline
  2941. Available colors are:
  2942. 0. white
  2943. 1. black
  2944. 2. blue
  2945. 3. green
  2946. 4. light red
  2947. 5. red
  2948. 6. magenta
  2949. 7. orange
  2950. 8. yellow
  2951. 9. light green
  2952. 10. cyan
  2953. 11. light cyan
  2954. 12. light blue
  2955. 13. light magenta
  2956. 14. gray
  2957. 15. light gray
  2958. @see: U{http://www.mirc.co.uk/help/color.txt}
  2959. @param formatted: Structured text and attributes.
  2960. @rtype: C{str}
  2961. @return: String containing mIRC control sequences that mimic those
  2962. specified by I{formatted}.
  2963. @since: 13.1
  2964. """
  2965. return _textattributes.flatten(formatted, _FormattingState(), "toMIRCControlCodes")
  2966. def stripFormatting(text):
  2967. """
  2968. Remove all formatting codes from C{text}, leaving only the text.
  2969. @type text: C{str}
  2970. @param text: Formatted text to parse.
  2971. @rtype: C{str}
  2972. @return: Plain text without any control sequences.
  2973. @since: 13.1
  2974. """
  2975. formatted = parseFormattedText(text)
  2976. return _textattributes.flatten(formatted, _textattributes.DefaultFormattingState())
  2977. # CTCP constants and helper functions
  2978. X_DELIM = chr(0o01)
  2979. def ctcpExtract(message):
  2980. """
  2981. Extract CTCP data from a string.
  2982. @return: A C{dict} containing two keys:
  2983. - C{'extended'}: A list of CTCP (tag, data) tuples.
  2984. - C{'normal'}: A list of strings which were not inside a CTCP delimiter.
  2985. """
  2986. extended_messages = []
  2987. normal_messages = []
  2988. retval = {"extended": extended_messages, "normal": normal_messages}
  2989. messages = message.split(X_DELIM)
  2990. odd = 0
  2991. # X1 extended data X2 nomal data X3 extended data X4 normal...
  2992. while messages:
  2993. if odd:
  2994. extended_messages.append(messages.pop(0))
  2995. else:
  2996. normal_messages.append(messages.pop(0))
  2997. odd = not odd
  2998. extended_messages[:] = list(filter(None, extended_messages))
  2999. normal_messages[:] = list(filter(None, normal_messages))
  3000. extended_messages[:] = list(map(ctcpDequote, extended_messages))
  3001. for i in range(len(extended_messages)):
  3002. m = extended_messages[i].split(SPC, 1)
  3003. tag = m[0]
  3004. if len(m) > 1:
  3005. data = m[1]
  3006. else:
  3007. data = None
  3008. extended_messages[i] = (tag, data)
  3009. return retval
  3010. # CTCP escaping
  3011. M_QUOTE = chr(0o20)
  3012. mQuoteTable = {
  3013. NUL: M_QUOTE + "0",
  3014. NL: M_QUOTE + "n",
  3015. CR: M_QUOTE + "r",
  3016. M_QUOTE: M_QUOTE + M_QUOTE,
  3017. }
  3018. mDequoteTable = {}
  3019. for k, v in mQuoteTable.items():
  3020. mDequoteTable[v[-1]] = k
  3021. del k, v
  3022. mEscape_re = re.compile(f"{re.escape(M_QUOTE)}.", re.DOTALL)
  3023. def lowQuote(s):
  3024. for c in (M_QUOTE, NUL, NL, CR):
  3025. s = s.replace(c, mQuoteTable[c])
  3026. return s
  3027. def lowDequote(s):
  3028. def sub(matchobj, mDequoteTable=mDequoteTable):
  3029. s = matchobj.group()[1]
  3030. try:
  3031. s = mDequoteTable[s]
  3032. except KeyError:
  3033. s = s
  3034. return s
  3035. return mEscape_re.sub(sub, s)
  3036. X_QUOTE = "\\"
  3037. xQuoteTable = {X_DELIM: X_QUOTE + "a", X_QUOTE: X_QUOTE + X_QUOTE}
  3038. xDequoteTable = {}
  3039. for k, v in xQuoteTable.items():
  3040. xDequoteTable[v[-1]] = k
  3041. xEscape_re = re.compile(f"{re.escape(X_QUOTE)}.", re.DOTALL)
  3042. def ctcpQuote(s):
  3043. for c in (X_QUOTE, X_DELIM):
  3044. s = s.replace(c, xQuoteTable[c])
  3045. return s
  3046. def ctcpDequote(s):
  3047. def sub(matchobj, xDequoteTable=xDequoteTable):
  3048. s = matchobj.group()[1]
  3049. try:
  3050. s = xDequoteTable[s]
  3051. except KeyError:
  3052. s = s
  3053. return s
  3054. return xEscape_re.sub(sub, s)
  3055. def ctcpStringify(messages):
  3056. """
  3057. @type messages: a list of extended messages. An extended
  3058. message is a (tag, data) tuple, where 'data' may be L{None}, a
  3059. string, or a list of strings to be joined with whitespace.
  3060. @returns: String
  3061. """
  3062. coded_messages = []
  3063. for tag, data in messages:
  3064. if data:
  3065. if not isinstance(data, str):
  3066. try:
  3067. # data as list-of-strings
  3068. data = " ".join(map(str, data))
  3069. except TypeError:
  3070. # No? Then use it's %s representation.
  3071. pass
  3072. m = f"{tag} {data}"
  3073. else:
  3074. m = str(tag)
  3075. m = ctcpQuote(m)
  3076. m = f"{X_DELIM}{m}{X_DELIM}"
  3077. coded_messages.append(m)
  3078. line = "".join(coded_messages)
  3079. return line
  3080. # Constants (from RFC 2812)
  3081. RPL_WELCOME = "001"
  3082. RPL_YOURHOST = "002"
  3083. RPL_CREATED = "003"
  3084. RPL_MYINFO = "004"
  3085. RPL_ISUPPORT = "005"
  3086. RPL_BOUNCE = "010"
  3087. RPL_USERHOST = "302"
  3088. RPL_ISON = "303"
  3089. RPL_AWAY = "301"
  3090. RPL_UNAWAY = "305"
  3091. RPL_NOWAWAY = "306"
  3092. RPL_WHOISUSER = "311"
  3093. RPL_WHOISSERVER = "312"
  3094. RPL_WHOISOPERATOR = "313"
  3095. RPL_WHOISIDLE = "317"
  3096. RPL_ENDOFWHOIS = "318"
  3097. RPL_WHOISCHANNELS = "319"
  3098. RPL_WHOWASUSER = "314"
  3099. RPL_ENDOFWHOWAS = "369"
  3100. RPL_LISTSTART = "321"
  3101. RPL_LIST = "322"
  3102. RPL_LISTEND = "323"
  3103. RPL_UNIQOPIS = "325"
  3104. RPL_CHANNELMODEIS = "324"
  3105. RPL_NOTOPIC = "331"
  3106. RPL_TOPIC = "332"
  3107. RPL_INVITING = "341"
  3108. RPL_SUMMONING = "342"
  3109. RPL_INVITELIST = "346"
  3110. RPL_ENDOFINVITELIST = "347"
  3111. RPL_EXCEPTLIST = "348"
  3112. RPL_ENDOFEXCEPTLIST = "349"
  3113. RPL_VERSION = "351"
  3114. RPL_WHOREPLY = "352"
  3115. RPL_ENDOFWHO = "315"
  3116. RPL_NAMREPLY = "353"
  3117. RPL_ENDOFNAMES = "366"
  3118. RPL_LINKS = "364"
  3119. RPL_ENDOFLINKS = "365"
  3120. RPL_BANLIST = "367"
  3121. RPL_ENDOFBANLIST = "368"
  3122. RPL_INFO = "371"
  3123. RPL_ENDOFINFO = "374"
  3124. RPL_MOTDSTART = "375"
  3125. RPL_MOTD = "372"
  3126. RPL_ENDOFMOTD = "376"
  3127. RPL_YOUREOPER = "381"
  3128. RPL_REHASHING = "382"
  3129. RPL_YOURESERVICE = "383"
  3130. RPL_TIME = "391"
  3131. RPL_USERSSTART = "392"
  3132. RPL_USERS = "393"
  3133. RPL_ENDOFUSERS = "394"
  3134. RPL_NOUSERS = "395"
  3135. RPL_TRACELINK = "200"
  3136. RPL_TRACECONNECTING = "201"
  3137. RPL_TRACEHANDSHAKE = "202"
  3138. RPL_TRACEUNKNOWN = "203"
  3139. RPL_TRACEOPERATOR = "204"
  3140. RPL_TRACEUSER = "205"
  3141. RPL_TRACESERVER = "206"
  3142. RPL_TRACESERVICE = "207"
  3143. RPL_TRACENEWTYPE = "208"
  3144. RPL_TRACECLASS = "209"
  3145. RPL_TRACERECONNECT = "210"
  3146. RPL_TRACELOG = "261"
  3147. RPL_TRACEEND = "262"
  3148. RPL_STATSLINKINFO = "211"
  3149. RPL_STATSCOMMANDS = "212"
  3150. RPL_ENDOFSTATS = "219"
  3151. RPL_STATSUPTIME = "242"
  3152. RPL_STATSOLINE = "243"
  3153. RPL_UMODEIS = "221"
  3154. RPL_SERVLIST = "234"
  3155. RPL_SERVLISTEND = "235"
  3156. RPL_LUSERCLIENT = "251"
  3157. RPL_LUSEROP = "252"
  3158. RPL_LUSERUNKNOWN = "253"
  3159. RPL_LUSERCHANNELS = "254"
  3160. RPL_LUSERME = "255"
  3161. RPL_ADMINME = "256"
  3162. RPL_ADMINLOC1 = "257"
  3163. RPL_ADMINLOC2 = "258"
  3164. RPL_ADMINEMAIL = "259"
  3165. RPL_TRYAGAIN = "263"
  3166. ERR_NOSUCHNICK = "401"
  3167. ERR_NOSUCHSERVER = "402"
  3168. ERR_NOSUCHCHANNEL = "403"
  3169. ERR_CANNOTSENDTOCHAN = "404"
  3170. ERR_TOOMANYCHANNELS = "405"
  3171. ERR_WASNOSUCHNICK = "406"
  3172. ERR_TOOMANYTARGETS = "407"
  3173. ERR_NOSUCHSERVICE = "408"
  3174. ERR_NOORIGIN = "409"
  3175. ERR_NORECIPIENT = "411"
  3176. ERR_NOTEXTTOSEND = "412"
  3177. ERR_NOTOPLEVEL = "413"
  3178. ERR_WILDTOPLEVEL = "414"
  3179. ERR_BADMASK = "415"
  3180. # Defined in errata.
  3181. # https://www.rfc-editor.org/errata_search.php?rfc=2812&eid=2822
  3182. ERR_TOOMANYMATCHES = "416"
  3183. ERR_UNKNOWNCOMMAND = "421"
  3184. ERR_NOMOTD = "422"
  3185. ERR_NOADMININFO = "423"
  3186. ERR_FILEERROR = "424"
  3187. ERR_NONICKNAMEGIVEN = "431"
  3188. ERR_ERRONEUSNICKNAME = "432"
  3189. ERR_NICKNAMEINUSE = "433"
  3190. ERR_NICKCOLLISION = "436"
  3191. ERR_UNAVAILRESOURCE = "437"
  3192. ERR_USERNOTINCHANNEL = "441"
  3193. ERR_NOTONCHANNEL = "442"
  3194. ERR_USERONCHANNEL = "443"
  3195. ERR_NOLOGIN = "444"
  3196. ERR_SUMMONDISABLED = "445"
  3197. ERR_USERSDISABLED = "446"
  3198. ERR_NOTREGISTERED = "451"
  3199. ERR_NEEDMOREPARAMS = "461"
  3200. ERR_ALREADYREGISTRED = "462"
  3201. ERR_NOPERMFORHOST = "463"
  3202. ERR_PASSWDMISMATCH = "464"
  3203. ERR_YOUREBANNEDCREEP = "465"
  3204. ERR_YOUWILLBEBANNED = "466"
  3205. ERR_KEYSET = "467"
  3206. ERR_CHANNELISFULL = "471"
  3207. ERR_UNKNOWNMODE = "472"
  3208. ERR_INVITEONLYCHAN = "473"
  3209. ERR_BANNEDFROMCHAN = "474"
  3210. ERR_BADCHANNELKEY = "475"
  3211. ERR_BADCHANMASK = "476"
  3212. ERR_NOCHANMODES = "477"
  3213. ERR_BANLISTFULL = "478"
  3214. ERR_NOPRIVILEGES = "481"
  3215. ERR_CHANOPRIVSNEEDED = "482"
  3216. ERR_CANTKILLSERVER = "483"
  3217. ERR_RESTRICTED = "484"
  3218. ERR_UNIQOPPRIVSNEEDED = "485"
  3219. ERR_NOOPERHOST = "491"
  3220. ERR_NOSERVICEHOST = "492"
  3221. ERR_UMODEUNKNOWNFLAG = "501"
  3222. ERR_USERSDONTMATCH = "502"
  3223. # And hey, as long as the strings are already intern'd...
  3224. symbolic_to_numeric = {
  3225. "RPL_WELCOME": "001",
  3226. "RPL_YOURHOST": "002",
  3227. "RPL_CREATED": "003",
  3228. "RPL_MYINFO": "004",
  3229. "RPL_ISUPPORT": "005",
  3230. "RPL_BOUNCE": "010",
  3231. "RPL_USERHOST": "302",
  3232. "RPL_ISON": "303",
  3233. "RPL_AWAY": "301",
  3234. "RPL_UNAWAY": "305",
  3235. "RPL_NOWAWAY": "306",
  3236. "RPL_WHOISUSER": "311",
  3237. "RPL_WHOISSERVER": "312",
  3238. "RPL_WHOISOPERATOR": "313",
  3239. "RPL_WHOISIDLE": "317",
  3240. "RPL_ENDOFWHOIS": "318",
  3241. "RPL_WHOISCHANNELS": "319",
  3242. "RPL_WHOWASUSER": "314",
  3243. "RPL_ENDOFWHOWAS": "369",
  3244. "RPL_LISTSTART": "321",
  3245. "RPL_LIST": "322",
  3246. "RPL_LISTEND": "323",
  3247. "RPL_UNIQOPIS": "325",
  3248. "RPL_CHANNELMODEIS": "324",
  3249. "RPL_NOTOPIC": "331",
  3250. "RPL_TOPIC": "332",
  3251. "RPL_INVITING": "341",
  3252. "RPL_SUMMONING": "342",
  3253. "RPL_INVITELIST": "346",
  3254. "RPL_ENDOFINVITELIST": "347",
  3255. "RPL_EXCEPTLIST": "348",
  3256. "RPL_ENDOFEXCEPTLIST": "349",
  3257. "RPL_VERSION": "351",
  3258. "RPL_WHOREPLY": "352",
  3259. "RPL_ENDOFWHO": "315",
  3260. "RPL_NAMREPLY": "353",
  3261. "RPL_ENDOFNAMES": "366",
  3262. "RPL_LINKS": "364",
  3263. "RPL_ENDOFLINKS": "365",
  3264. "RPL_BANLIST": "367",
  3265. "RPL_ENDOFBANLIST": "368",
  3266. "RPL_INFO": "371",
  3267. "RPL_ENDOFINFO": "374",
  3268. "RPL_MOTDSTART": "375",
  3269. "RPL_MOTD": "372",
  3270. "RPL_ENDOFMOTD": "376",
  3271. "RPL_YOUREOPER": "381",
  3272. "RPL_REHASHING": "382",
  3273. "RPL_YOURESERVICE": "383",
  3274. "RPL_TIME": "391",
  3275. "RPL_USERSSTART": "392",
  3276. "RPL_USERS": "393",
  3277. "RPL_ENDOFUSERS": "394",
  3278. "RPL_NOUSERS": "395",
  3279. "RPL_TRACELINK": "200",
  3280. "RPL_TRACECONNECTING": "201",
  3281. "RPL_TRACEHANDSHAKE": "202",
  3282. "RPL_TRACEUNKNOWN": "203",
  3283. "RPL_TRACEOPERATOR": "204",
  3284. "RPL_TRACEUSER": "205",
  3285. "RPL_TRACESERVER": "206",
  3286. "RPL_TRACESERVICE": "207",
  3287. "RPL_TRACENEWTYPE": "208",
  3288. "RPL_TRACECLASS": "209",
  3289. "RPL_TRACERECONNECT": "210",
  3290. "RPL_TRACELOG": "261",
  3291. "RPL_TRACEEND": "262",
  3292. "RPL_STATSLINKINFO": "211",
  3293. "RPL_STATSCOMMANDS": "212",
  3294. "RPL_ENDOFSTATS": "219",
  3295. "RPL_STATSUPTIME": "242",
  3296. "RPL_STATSOLINE": "243",
  3297. "RPL_UMODEIS": "221",
  3298. "RPL_SERVLIST": "234",
  3299. "RPL_SERVLISTEND": "235",
  3300. "RPL_LUSERCLIENT": "251",
  3301. "RPL_LUSEROP": "252",
  3302. "RPL_LUSERUNKNOWN": "253",
  3303. "RPL_LUSERCHANNELS": "254",
  3304. "RPL_LUSERME": "255",
  3305. "RPL_ADMINME": "256",
  3306. "RPL_ADMINLOC1": "257",
  3307. "RPL_ADMINLOC2": "258",
  3308. "RPL_ADMINEMAIL": "259",
  3309. "RPL_TRYAGAIN": "263",
  3310. "ERR_NOSUCHNICK": "401",
  3311. "ERR_NOSUCHSERVER": "402",
  3312. "ERR_NOSUCHCHANNEL": "403",
  3313. "ERR_CANNOTSENDTOCHAN": "404",
  3314. "ERR_TOOMANYCHANNELS": "405",
  3315. "ERR_WASNOSUCHNICK": "406",
  3316. "ERR_TOOMANYTARGETS": "407",
  3317. "ERR_NOSUCHSERVICE": "408",
  3318. "ERR_NOORIGIN": "409",
  3319. "ERR_NORECIPIENT": "411",
  3320. "ERR_NOTEXTTOSEND": "412",
  3321. "ERR_NOTOPLEVEL": "413",
  3322. "ERR_WILDTOPLEVEL": "414",
  3323. "ERR_BADMASK": "415",
  3324. "ERR_TOOMANYMATCHES": "416",
  3325. "ERR_UNKNOWNCOMMAND": "421",
  3326. "ERR_NOMOTD": "422",
  3327. "ERR_NOADMININFO": "423",
  3328. "ERR_FILEERROR": "424",
  3329. "ERR_NONICKNAMEGIVEN": "431",
  3330. "ERR_ERRONEUSNICKNAME": "432",
  3331. "ERR_NICKNAMEINUSE": "433",
  3332. "ERR_NICKCOLLISION": "436",
  3333. "ERR_UNAVAILRESOURCE": "437",
  3334. "ERR_USERNOTINCHANNEL": "441",
  3335. "ERR_NOTONCHANNEL": "442",
  3336. "ERR_USERONCHANNEL": "443",
  3337. "ERR_NOLOGIN": "444",
  3338. "ERR_SUMMONDISABLED": "445",
  3339. "ERR_USERSDISABLED": "446",
  3340. "ERR_NOTREGISTERED": "451",
  3341. "ERR_NEEDMOREPARAMS": "461",
  3342. "ERR_ALREADYREGISTRED": "462",
  3343. "ERR_NOPERMFORHOST": "463",
  3344. "ERR_PASSWDMISMATCH": "464",
  3345. "ERR_YOUREBANNEDCREEP": "465",
  3346. "ERR_YOUWILLBEBANNED": "466",
  3347. "ERR_KEYSET": "467",
  3348. "ERR_CHANNELISFULL": "471",
  3349. "ERR_UNKNOWNMODE": "472",
  3350. "ERR_INVITEONLYCHAN": "473",
  3351. "ERR_BANNEDFROMCHAN": "474",
  3352. "ERR_BADCHANNELKEY": "475",
  3353. "ERR_BADCHANMASK": "476",
  3354. "ERR_NOCHANMODES": "477",
  3355. "ERR_BANLISTFULL": "478",
  3356. "ERR_NOPRIVILEGES": "481",
  3357. "ERR_CHANOPRIVSNEEDED": "482",
  3358. "ERR_CANTKILLSERVER": "483",
  3359. "ERR_RESTRICTED": "484",
  3360. "ERR_UNIQOPPRIVSNEEDED": "485",
  3361. "ERR_NOOPERHOST": "491",
  3362. "ERR_NOSERVICEHOST": "492",
  3363. "ERR_UMODEUNKNOWNFLAG": "501",
  3364. "ERR_USERSDONTMATCH": "502",
  3365. }
  3366. numeric_to_symbolic = {}
  3367. for k, v in symbolic_to_numeric.items():
  3368. numeric_to_symbolic[v] = k