]>
code.delx.au - pymsnt/blob - src/misciq.py
1 # Copyright 2004-2005 James Bunton <james@delx.cjb.net>
2 # Licensed for distribution under the GPL version 2, check COPYING for details
5 from twisted
.internet
import reactor
, task
, protocol
, error
6 from tlib
.xmlw
import Element
, jid
7 from debug
import LogEvent
, INFO
, WARN
, ERROR
20 def __init__(self
, pytrans
):
21 self
.pytrans
= pytrans
22 self
.pytrans
.adHocCommands
.addCommand("connectusers", self
.incomingIq
, "command_ConnectUsers")
25 for jid
in self
.pytrans
.xdb
.files():
26 jabw
.sendPresence(self
.pytrans
, jid
, config
.jid
, ptype
="probe")
28 def incomingIq(self
, el
):
29 to
= el
.getAttribute("from")
30 ID
= el
.getAttribute("id")
31 ulang
= utils
.getLang(el
)
33 if config
.admins
.count(jid
.intern(to
).userhost()) == 0:
34 self
.pytrans
.discovery
.sendIqError(to
=to
, fro
=config
.jid
, ID
=ID
, xmlns
=disco
.COMMANDS
, etype
="cancel", condition
="not-authorized")
40 iq
= Element((None, "iq"))
41 iq
.attributes
["to"] = to
42 iq
.attributes
["from"] = config
.jid
44 iq
.attributes
["id"] = ID
45 iq
.attributes
["type"] = "result"
47 command
= iq
.addElement("command")
48 command
.attributes
["sessionid"] = self
.pytrans
.makeMessageID()
49 command
.attributes
["xmlns"] = disco
.COMMANDS
50 command
.attributes
["status"] = "completed"
52 x
= command
.addElement("x")
53 x
.attributes
["xmlns"] = disco
.XDATA
54 x
.attributes
["type"] = "result"
56 title
= x
.addElement("title")
57 title
.addContent(lang
.get(ulang
).command_ConnectUsers
)
59 field
= x
.addElement("field")
60 field
.attributes
["type"] = "fixed"
61 field
.addElement("value").addContent(lang
.get(ulang
).command_Done
)
67 def __init__(self
, pytrans
):
68 self
.pytrans
= pytrans
69 self
.pytrans
.adHocCommands
.addCommand("stats", self
.incomingIq
, "command_Statistics")
71 # self.stats is indexed by a unique ID, with value being the value for that statistic
73 self
.stats
["Uptime"] = 0
74 self
.stats
["OnlineUsers"] = 0
75 self
.stats
["TotalUsers"] = 0
77 legacy
.startStats(self
)
79 def incomingIq(self
, el
):
80 to
= el
.getAttribute("from")
81 ID
= el
.getAttribute("id")
82 ulang
= utils
.getLang(el
)
84 iq
= Element((None, "iq"))
85 iq
.attributes
["to"] = to
86 iq
.attributes
["from"] = config
.jid
88 iq
.attributes
["id"] = ID
89 iq
.attributes
["type"] = "result"
91 command
= iq
.addElement("command")
92 command
.attributes
["sessionid"] = self
.pytrans
.makeMessageID()
93 command
.attributes
["xmlns"] = disco
.COMMANDS
94 command
.attributes
["status"] = "completed"
96 x
= command
.addElement("x")
97 x
.attributes
["xmlns"] = disco
.XDATA
98 x
.attributes
["type"] = "result"
100 title
= x
.addElement("title")
101 title
.addContent(lang
.get(ulang
).command_Statistics
)
103 for key
in self
.stats
:
104 label
= getattr(lang
.get(ulang
), "command_%s" % key
)
105 description
= getattr(lang
.get(ulang
), "command_%s_Desc" % key
)
106 field
= x
.addElement("field")
107 field
.attributes
["var"] = key
108 field
.attributes
["label"] = label
109 field
.attributes
["type"] = "text-single"
110 field
.addElement("value").addContent(str(self
.stats
[key
]))
111 field
.addElement("desc").addContent(description
)
113 self
.pytrans
.send(iq
)
118 def __init__(self
, pytrans
):
119 self
.pytrans
= pytrans
120 self
.pytrans
.discovery
.addFeature(disco
.COMMANDS
, self
.incomingIq
, config
.jid
)
121 self
.pytrans
.discovery
.addNode(disco
.COMMANDS
, self
.sendCommandList
, "command_CommandList", config
.jid
, True)
123 self
.commands
= {} # Dict of handlers indexed by node
124 self
.commandNames
= {} # Dict of names indexed by node
126 def addCommand(self
, command
, handler
, name
):
127 self
.commands
[command
] = handler
128 self
.commandNames
[command
] = name
129 self
.pytrans
.discovery
.addNode(command
, self
.incomingIq
, name
, config
.jid
, False)
131 def incomingIq(self
, el
):
132 itype
= el
.getAttribute("type")
133 fro
= el
.getAttribute("from")
134 froj
= jid
.intern(fro
)
135 to
= el
.getAttribute("to")
136 ID
= el
.getAttribute("id")
138 LogEvent(INFO
, "", "Looking for handler")
141 for child
in el
.elements():
143 node
= child
.getAttribute("node")
146 if(child
.name
== "query" and xmlns
== disco
.DISCO_INFO
):
147 if(node
and self
.commands
.has_key(node
) and (itype
== "get")):
148 self
.sendCommandInfoResponse(to
=fro
, ID
=ID
)
150 elif(child
.name
== "query" and xmlns
== disco
.DISCO_ITEMS
):
151 if(node
and self
.commands
.has_key(node
) and (itype
== "get")):
152 self
.sendCommandItemsResponse(to
=fro
, ID
=ID
)
154 elif(child
.name
== "command" and xmlns
== disco
.COMMANDS
):
155 if((node
and self
.commands
.has_key(node
)) and (itype
== "set" or itype
== "error")):
156 self
.commands
[node
](el
)
159 LogEvent(WARN
, "", "Unknown Ad-Hoc command received.")
160 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
=xmlns
, etype
="cancel", condition
="feature-not-implemented")
163 def sendCommandList(self
, el
):
164 to
= el
.getAttribute("from")
165 ID
= el
.getAttribute("id")
166 ulang
= utils
.getLang(el
)
168 iq
= Element((None, "iq"))
169 iq
.attributes
["to"] = to
170 iq
.attributes
["from"] = config
.jid
172 iq
.attributes
["id"] = ID
173 iq
.attributes
["type"] = "result"
175 query
= iq
.addElement("query")
176 query
.attributes
["xmlns"] = disco
.DISCO_ITEMS
177 query
.attributes
["node"] = disco
.COMMANDS
179 for command
in self
.commands
:
180 item
= query
.addElement("item")
181 item
.attributes
["jid"] = config
.jid
182 item
.attributes
["node"] = command
183 item
.attributes
["name"] = getattr(lang
.get(ulang
), self
.commandNames
[command
])
185 self
.pytrans
.send(iq
)
187 def sendCommandInfoResponse(self
, to
, ID
):
188 LogEvent(INFO
, "", "Replying to disco#info")
189 iq
= Element((None, "iq"))
190 iq
.attributes
["type"] = "result"
191 iq
.attributes
["from"] = config
.jid
192 iq
.attributes
["to"] = to
193 if(ID
): iq
.attributes
["id"] = ID
194 query
= iq
.addElement("query")
195 query
.attributes
["xmlns"] = disco
.DISCO_INFO
197 feature
= query
.addElement("feature")
198 feature
.attributes
["var"] = disco
.COMMANDS
199 self
.pytrans
.send(iq
)
201 def sendCommandItemsResponse(self
, to
, ID
):
202 LogEvent(INFO
, "", "Replying to disco#items")
203 iq
= Element((None, "iq"))
204 iq
.attributes
["type"] = "result"
205 iq
.attributes
["from"] = config
.jid
206 iq
.attributes
["to"] = to
207 if(ID
): iq
.attributes
["id"] = ID
208 query
= iq
.addElement("query")
209 query
.attributes
["xmlns"] = disco
.DISCO_ITEMS
210 self
.pytrans
.send(iq
)
214 def __init__(self
, pytrans
):
215 self
.pytrans
= pytrans
216 self
.pytrans
.discovery
.addFeature("vcard-temp", self
.incomingIq
, "USER")
217 self
.pytrans
.discovery
.addFeature("vcard-temp", self
.incomingIq
, config
.jid
)
219 def incomingIq(self
, el
):
220 itype
= el
.getAttribute("type")
221 fro
= el
.getAttribute("from")
222 froj
= jid
.intern(fro
)
223 to
= el
.getAttribute("to")
225 ID
= el
.getAttribute("id")
226 if itype
!= "get" and itype
!= "error":
227 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
="vcard-temp", etype
="cancel", condition
="feature-not-implemented")
230 LogEvent(INFO
, "", "Sending vCard")
232 toGateway
= not (to
.find('@') > 0)
235 if not self
.pytrans
.sessions
.has_key(froj
.userhost()):
236 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
="vcard-temp", etype
="auth", condition
="not-authorized")
238 s
= self
.pytrans
.sessions
[froj
.userhost()]
240 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
="vcard-temp", etype
="auth", condition
="not-authorized")
243 c
= s
.contactList
.findContact(toj
.userhost())
245 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
="vcard-temp", etype
="cancel", condition
="recipient-unavailable")
249 iq
= Element((None, "iq"))
250 iq
.attributes
["to"] = fro
251 iq
.attributes
["from"] = to
253 iq
.attributes
["id"] = ID
254 iq
.attributes
["type"] = "result"
255 vCard
= iq
.addElement("vCard")
256 vCard
.attributes
["xmlns"] = "vcard-temp"
258 FN
= vCard
.addElement("FN")
259 FN
.addContent(config
.discoName
)
260 DESC
= vCard
.addElement("DESC")
261 DESC
.addContent(config
.discoName
)
262 URL
= vCard
.addElement("URL")
263 URL
.addContent(legacy
.url
)
266 NICKNAME
= vCard
.addElement("NICKNAME")
267 NICKNAME
.addContent(c
.nickname
)
269 PHOTO
= c
.avatar
.makePhotoElement()
270 vCard
.addChild(PHOTO
)
272 self
.pytrans
.send(iq
)
274 class IqAvatarFactory
:
275 def __init__(self
, pytrans
):
276 self
.pytrans
= pytrans
277 self
.pytrans
.discovery
.addFeature(disco
.IQAVATAR
, self
.incomingIq
, "USER")
278 self
.pytrans
.discovery
.addFeature(disco
.STORAGEAVATAR
, self
.incomingIq
, "USER")
280 def incomingIq(self
, el
):
281 itype
= el
.getAttribute("type")
282 fro
= el
.getAttribute("from")
283 froj
= jid
.intern(fro
)
284 to
= el
.getAttribute("to")
286 ID
= el
.getAttribute("id")
288 if(itype
!= "get" and itype
!= "error"):
289 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
=disco
.IQAVATAR
, etype
="cancel", condition
="feature-not-implemented")
292 LogEvent(INFO
, "", "Retrieving avatar")
294 if(not self
.pytrans
.sessions
.has_key(froj
.userhost())):
295 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
=disco
.IQAVATAR
, etype
="auth", condition
="not-authorized")
297 s
= self
.pytrans
.sessions
[froj
.userhost()]
299 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
=disco
.IQAVATAR
, etype
="auth", condition
="not-authorized")
302 c
= s
.contactList
.findContact(toj
.userhost())
304 self
.pytrans
.discovery
.sendIqError(to
=fro
, fro
=config
.jid
, ID
=ID
, xmlns
=disco
.IQAVATAR
, etype
="cancel", condition
="recipient-unavailable")
307 iq
= Element((None, "iq"))
308 iq
.attributes
["to"] = fro
309 iq
.attributes
["from"] = to
311 iq
.attributes
["id"] = ID
312 iq
.attributes
["type"] = "result"
313 query
= iq
.addElement("query")
314 query
.attributes
["xmlns"] = disco
.IQAVATAR
316 DATA
= c
.avatar
.makeDataElement()
319 self
.pytrans
.send(iq
)
324 def __init__(self
, pytrans
):
325 self
.pytrans
= pytrans
326 # self.pingCounter = 0
327 # self.pingTask = task.LoopingCall(self.pingCheck)
328 self
.pingTask
= task
.LoopingCall(self
.whitespace
)
329 # reactor.callLater(10.0, self.start)
332 # self.pingTask.start(120.0)
334 def whitespace(self
):
335 self
.pytrans
.send(" ")
337 # def pingCheck(self):
338 # if(self.pingCounter >= 2 and self.pytrans.xmlstream): # Two minutes of no response from the main server
339 # LogEvent(WARN, "", "Disconnecting because the main server has ignored our pings for too long.")
340 # self.pytrans.xmlstream.transport.loseConnection()
341 # elif(config.mainServerJID):
342 # d = self.pytrans.discovery.sendIq(self.makePingPacket())
343 # d.addCallback(self.pongReceived)
344 # d.addErrback(self.pongFailed)
345 # self.pingCounter += 1
347 # def pongReceived(self, el):
348 # self.pingCounter = 0
350 # def pongFailed(self, el):
353 # def makePingPacket(self):
354 # iq = Element((None, "iq"))
355 # iq.attributes["from"] = config.jid
356 # iq.attributes["to"] = config.mainServerJID
357 # iq.attributes["type"] = "get"
358 # query = iq.addElement("query")
359 # query.attributes["xmlns"] = disco.IQVERSION
362 class GatewayTranslator
:
363 def __init__(self
, pytrans
):
364 self
.pytrans
= pytrans
365 self
.pytrans
.discovery
.addFeature(disco
.IQGATEWAY
, self
.incomingIq
, config
.jid
)
367 def incomingIq(self
, el
):
368 fro
= el
.getAttribute("from")
369 ID
= el
.getAttribute("id")
370 itype
= el
.getAttribute("type")
372 self
.sendPrompt(fro
, ID
, utils
.getLang(el
))
373 elif(itype
== "set"):
374 self
.sendTranslation(fro
, ID
, el
)
377 def sendPrompt(self
, to
, ID
, ulang
):
380 iq
= Element((None, "iq"))
382 iq
.attributes
["type"] = "result"
383 iq
.attributes
["from"] = config
.jid
384 iq
.attributes
["to"] = to
386 iq
.attributes
["id"] = ID
387 query
= iq
.addElement("query")
388 query
.attributes
["xmlns"] = disco
.IQGATEWAY
389 desc
= query
.addElement("desc")
390 desc
.addContent(lang
.get(ulang
).gatewayTranslator
)
391 prompt
= query
.addElement("prompt")
393 self
.pytrans
.send(iq
)
395 def sendTranslation(self
, to
, ID
, el
):
398 # Find the user's legacy account
400 for query
in el
.elements():
401 if(query
.name
== "query"):
402 for child
in query
.elements():
403 if(child
.name
== "prompt"):
404 legacyaccount
= str(child
)
409 if(legacyaccount
and len(legacyaccount
) > 0):
410 LogEvent(INFO
, "", "Sending translated account.")
411 iq
= Element((None, "iq"))
412 iq
.attributes
["type"] = "result"
413 iq
.attributes
["from"] = config
.jid
414 iq
.attributes
["to"] = to
416 iq
.attributes
["id"] = ID
417 query
= iq
.addElement("query")
418 query
.attributes
["xmlns"] = disco
.IQGATEWAY
419 prompt
= query
.addElement("prompt")
420 prompt
.addContent(legacy
.translateAccount(legacyaccount
))
422 self
.pytrans
.send(iq
)
425 self
.pytrans
.discovery
.sendIqError(to
, ID
, disco
.IQGATEWAY
)
426 self
.pytrans
.discovery
.sendIqError(to
=to
, fro
=config
.jid
, ID
=ID
, xmlns
=disco
.IQGATEWAY
, etype
="retry", condition
="bad-request")
431 def __init__(self
, pytrans
):
432 self
.pytrans
= pytrans
433 self
.pytrans
.discovery
.addFeature(disco
.IQVERSION
, self
.incomingIq
, config
.jid
)
434 self
.pytrans
.discovery
.addFeature(disco
.IQVERSION
, self
.incomingIq
, "USER")
436 self
.version
= "%s - SVN r%s" % (legacy
.version
, svninfo
.getSVNVersion())
438 self
.version
= legacy
.version
439 self
.os
= "Python" + ".".join([str(x
) for x
in sys
.version_info
[0:3]]) + " - " + sys
.platform
441 def incomingIq(self
, el
):
442 eltype
= el
.getAttribute("type")
443 if(eltype
!= "get"): return # Only answer "get" stanzas
447 def sendVersion(self
, el
):
449 iq
= Element((None, "iq"))
450 iq
.attributes
["type"] = "result"
451 iq
.attributes
["from"] = el
.getAttribute("to")
452 iq
.attributes
["to"] = el
.getAttribute("from")
453 if(el
.getAttribute("id")):
454 iq
.attributes
["id"] = el
.getAttribute("id")
455 query
= iq
.addElement("query")
456 query
.attributes
["xmlns"] = disco
.IQVERSION
457 name
= query
.addElement("name")
458 name
.addContent(config
.discoName
)
459 version
= query
.addElement("version")
460 version
.addContent(self
.version
)
461 os
= query
.addElement("os")
462 os
.addContent(self
.os
)
464 self
.pytrans
.send(iq
)
467 class FileTransferOOBSend
:
468 def __init__(self
, pytrans
):
469 self
.pytrans
= pytrans
470 self
.pytrans
.discovery
.addFeature(disco
.IQOOB
, self
.incomingOOB
, "USER")
472 def incomingOOB(self
, el
):
473 ID
= el
.getAttribute("id")
475 self
.pytrans
.discovery
.sendIqError(to
=el
.getAttribute("from"), fro
=el
.getAttribute("to"), ID
=ID
, xmlns
=disco
.IQOOB
, etype
="cancel", condition
="feature-not-implemented")
477 if el
.attributes
["type"] != "set":
479 for child
in el
.elements():
480 if child
.name
== "query":
485 for child
in query
.elements():
486 if child
.name
== "url":
487 url
= child
.__str
__()
492 froj
= jid
.intern(el
.getAttribute("from"))
493 toj
= jid
.intern(el
.getAttribute("to"))
494 session
= self
.pytrans
.sessions
.get(froj
.userhost(), None)
498 res
= utils
.getURLBits(url
, "http")
501 host
, port
, path
, filename
= res
505 iq
= Element((None, "iq"))
506 iq
.attributes
["to"] = froj
.full()
507 iq
.attributes
["from"] = toj
.full()
508 iq
.attributes
["type"] = "result"
510 iq
.attributes
["id"] = ID
511 iq
.addElement("query").attributes
["xmlns"] = "jabber:iq:oob"
512 self
.pytrans
.send(iq
)
514 def startTransfer(consumer
):
515 factory
= protocol
.ClientFactory()
516 factory
.protocol
= ft
.OOBSendConnector
520 factory
.consumer
= consumer
521 factory
.finished
= sendResult
522 reactor
.connectTCP(host
, port
, factory
)
524 def doSendFile(length
):
525 ft
.FTSend(session
, toj
.userhost(), startTransfer
, errOut
, filename
, length
)
527 # Make a HEAD request to grab the length of data first
528 factory
= protocol
.ClientFactory()
529 factory
.protocol
= ft
.OOBHeaderHelper
533 factory
.gotLength
= doSendFile
534 reactor
.connectTCP(host
, port
, factory
)
538 class Socks5FileTransfer
:
539 def __init__(self
, pytrans
):
540 self
.pytrans
= pytrans
541 self
.pytrans
.discovery
.addFeature(disco
.SI
, self
.incomingSI
, "USER")
542 self
.pytrans
.discovery
.addFeature(disco
.FT
, lambda: None, "USER")
543 self
.pytrans
.discovery
.addFeature(disco
.S5B
, self
.incomingS5B
, "USER")
546 def incomingSI(self
, el
):
547 ID
= el
.getAttribute("id")
549 self
.pytrans
.discovery
.sendIqError(to
=el
.getAttribute("from"), fro
=el
.getAttribute("to"), ID
=ID
, xmlns
=disco
.SI
, etype
="cancel", condition
="bad-request")
551 toj
= jid
.intern(el
.getAttribute("to"))
552 froj
= jid
.intern(el
.getAttribute("from"))
553 session
= self
.pytrans
.sessions
.get(froj
.userhost(), None)
558 if not (si
and si
.getAttribute("profile") == disco
.FT
):
561 if not (file and file.uri
== disco
.FT
):
565 filename
= file["name"]
566 filesize
= int(file["size"])
572 # Check that we can use socks5 bytestreams
574 if not (feature
and feature
.uri
== disco
.FEATURE_NEG
):
577 if not (x
and x
.uri
== disco
.XDATA
):
580 if not (field
and field
.getAttribute("var") == "stream-method"):
582 for option
in field
.elements():
586 value
= value
.__str
__()
587 if value
== disco
.S5B
:
590 return errOut() # Socks5 bytestreams not supported :(
593 def startTransfer(consumer
):
594 iq
= Element((None, "iq"))
595 iq
["type"] = "result"
596 iq
["to"] = froj
.full()
597 iq
["from"] = toj
.full()
599 si
= iq
.addElement("si")
600 si
["xmlns"] = disco
.SI
601 feature
= si
.addElement("feature")
602 feature
["xmlns"] = disco
.FEATURE_NEG
603 x
= feature
.addElement("x")
604 x
["xmlns"] = disco
.XDATA
606 field
= x
.addElement("field")
607 field
["var"] = "stream-method"
608 value
= field
.addElement("value")
609 value
.addContent(disco
.S5B
)
610 self
.pytrans
.send(iq
)
611 self
.sessions
[(froj
.full(), sid
)] = consumer
613 ft
.FTSend(session
, toj
.userhost(), startTransfer
, errOut
, filename
, filesize
)
615 def incomingS5B(self
, el
):
616 ID
= el
.getAttribute("id")
618 self
.pytrans
.discovery
.sendIqError(to
=el
.getAttribute("from"), fro
=el
.getAttribute("to"), ID
=ID
, xmlns
=disco
.S5B
, etype
="cancel", condition
="item-not-found")
620 if el
.getAttribute("type") != "set":
623 toj
= jid
.intern(el
.getAttribute("to"))
624 froj
= jid
.intern(el
.getAttribute("from"))
627 if not (query
and query
.getAttribute("mode", "tcp") == "tcp"):
629 sid
= query
.getAttribute("sid")
630 consumer
= self
.sessions
.pop((froj
.full(), sid
), None)
634 for streamhost
in query
.elements():
635 if streamhost
.name
== "streamhost":
637 JID
= streamhost
["jid"]
638 host
= streamhost
["host"]
639 port
= int(streamhost
["port"])
644 streamhosts
.append((JID
, host
, port
))
647 def gotStreamhost(host
):
648 for streamhost
in streamhosts
:
649 if streamhost
[1] == host
:
656 for connector
in factory
.connectors
:
657 # Stop any other connections
659 connector
.stopConnecting()
660 except error
.NotConnectingError
:
663 if factory
.streamHostTimeout
:
664 factory
.streamHostTimeout
.cancel()
665 factory
.streamHostTimeout
= None
667 iq
= Element((None, "iq"))
668 iq
["type"] = "result"
669 iq
["from"] = toj
.full()
670 iq
["to"] = froj
.full()
672 query
= iq
.addElement("query")
673 query
["xmlns"] = disco
.S5B
674 streamhost
= query
.addElement("streamhost-used")
675 streamhost
["jid"] = jid
676 self
.pytrans
.send(iq
)
679 # Try the streamhosts
681 factory
= protocol
.ClientFactory()
682 factory
.protocol
= ft
.JEP65ConnectionSend
683 factory
.consumer
= consumer
684 factory
.hash = utils
.socks5Hash(sid
, froj
.full(), toj
.full())
685 factory
.madeConnection
= gotStreamhost
686 factory
.connectors
= []
687 factory
.streamHostTimeout
= reactor
.callLater(120, consumer
.error
)
689 for streamhost
in streamhosts
:
690 factory
.connectors
.append(reactor
.connectTCP(streamhost
[1], streamhost
[2], factory
))