Advertisement
r1cky1

Bot Commands

Apr 8th, 2017
108
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 50.79 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. import logging
  3. import threading
  4. import time
  5. import traceback
  6. from colorama import init, Fore, Style
  7. import config
  8. import user
  9. import apis.tinychat
  10. from rtmplib import rtmp
  11. from page import acc, params
  12. from util import string_util, file_handler
  13.  
  14. __version__ = '7.0.0'
  15.  
  16. # Console colors.
  17. COLOR = {
  18. 'white': Fore.WHITE,
  19. 'green': Fore.GREEN,
  20. 'bright_green': Style.BRIGHT + Fore.GREEN,
  21. 'yellow': Fore.YELLOW,
  22. 'bright_yellow': Style.BRIGHT + Fore.YELLOW,
  23. 'cyan': Fore.CYAN,
  24. 'bright_cyan': Style.BRIGHT + Fore.CYAN,
  25. 'red': Fore.RED,
  26. 'bright_red': Style.BRIGHT + Fore.RED,
  27. 'magenta': Fore.MAGENTA,
  28. 'bright_magenta': Style.BRIGHT + Fore.MAGENTA
  29. }
  30.  
  31. CONFIG = config
  32. init(autoreset=True)
  33. log = logging.getLogger(__name__)
  34.  
  35.  
  36. def write_to_log(msg, room_name):
  37. """ Writes chat events to log.
  38.  
  39. :param msg: the message to write to the log.
  40. :type msg: str
  41. :param room_name: the room name.
  42. :type room_name: str
  43. """
  44. d = time.strftime('%Y-%m-%d')
  45. file_name = d + '.log'
  46. path = config.CONFIG_PATH + room_name + '/logs/'
  47. file_handler.file_writer(path, file_name, msg.encode(encoding='UTF-8', errors='ignore'))
  48.  
  49.  
  50. class TinychatRTMPClient(object):
  51. """
  52. Tinychat client responsible for managing the connection and the different
  53. events that may occur in the chat room.
  54. """
  55. def __init__(self, roomname, nick='', account='', password='', room_pass=None, proxy=None):
  56. """ Create a instance of the TinychatRTMPClient class.
  57.  
  58. :param roomname: The room name to connect to.(required)
  59. :type roomname: str
  60. :param nick: The nick name to use in the chat room.(optional)
  61. :type nick: str
  62. :param account: Tinychat account name.(optional)
  63. :type account: str
  64. :param password: The password for the tinychat account.(optional)
  65. :type password: str
  66. :param room_pass: The password for the room.(optional)
  67. :type room_pass: str | None
  68. :param proxy: A proxy in the format IP:PORT for the connection to the remote server,
  69. but also for doing the various web requests related to establishing a connection.(optional)
  70. :type proxy: str | None
  71. """
  72. self.roomname = roomname
  73. self.nickname = nick
  74. self.account = account
  75. self.password = password
  76. self.room_pass = room_pass
  77. self.is_client_mod = False
  78. self.is_client_owner = False
  79. self.connection = None
  80. self.green_connection = None
  81. self.is_connected = False
  82. self.is_green_connected = False
  83. self.users = user.Users()
  84. self.active_user = None
  85. self.param = None
  86. self._proxy = proxy
  87. self._client_id = None
  88. self._bauth_key = None
  89. self._is_reconnected = False
  90. self._reconnect_delay = config.RECONNECT_DELAY
  91. self._init_time = time.time()
  92.  
  93. def console_write(self, color, message):
  94. """ Writes message to console.
  95.  
  96. :param color: the colorama color representation.
  97. :param message: str the message to write.
  98. """
  99. msg_encoded = message.encode('ascii', 'ignore')
  100. if config.USE_24HOUR:
  101. ts = time.strftime('%H:%M:%S')
  102. else:
  103. ts = time.strftime('%I:%M:%S:%p')
  104. if config.CONSOLE_COLORS:
  105. msg = COLOR['white'] + '[' + ts + '] ' + Style.RESET_ALL + color + msg_encoded
  106. else:
  107. msg = '[' + ts + '] ' + msg_encoded
  108. try:
  109. print(msg)
  110. except UnicodeEncodeError as ue:
  111. log.error(ue, exc_info=True)
  112. if config.DEBUG_MODE:
  113. traceback.print_exc()
  114.  
  115. if config.CHAT_LOGGING:
  116. write_to_log('[' + ts + '] ' + message, self.roomname)
  117.  
  118. def set_rtmp_parameters(self):
  119. """ Set the RTMP parameters before making a connect. """
  120. self.param = params.Params(room_name=self.roomname, room_pass=self.room_pass,
  121. swf_version=config.SWF_VERSION, proxy=self._proxy)
  122. self.param.get_config()
  123. if self.param.config_status == 3:
  124. if config.DEBUG_MODE:
  125. for k in self.param.config_dict:
  126. self.console_write(COLOR['white'], '%s: %s' % (k, self.param.config_dict[k]))
  127. if self.account:
  128. self.console_write(COLOR['white'], 'Account: %s' % self.account)
  129. return self.param.config_status
  130.  
  131. def login(self):
  132. """ Login to tinychat.
  133.  
  134. :return: True if logged in, else False.
  135. :rtype: bool
  136. """
  137. account = acc.Account(account=self.account, password=self.password, proxy=self._proxy)
  138. if self.account and self.password:
  139. if account.is_logged_in():
  140. return True
  141. account.login()
  142. return account.is_logged_in()
  143.  
  144. def connect(self):
  145. """ Make a connection with the remote RTMP server. """
  146. if not self.is_connected:
  147. log.info('connecting to: %s' % self.roomname)
  148. try:
  149. self.param.recaptcha()
  150. self.connection = rtmp.RtmpClient(
  151. ip=self.param.ip,
  152. port=self.param.port,
  153. tc_url=self.param.tc_url,
  154. app=self.param.app,
  155. page_url=self.param.embed_url,
  156. swf_url=self.param.swf_url,
  157. proxy=self._proxy,
  158. is_win=True
  159. )
  160. self.connection.connect(
  161. {
  162. 'account': self.account,
  163. 'type': self.param.roomtype,
  164. 'prefix': u'tinychat',
  165. 'room': self.roomname,
  166. 'version': self.param.desktop_version,
  167. 'cookie': self.param.cauth_cookie()
  168. }
  169. )
  170. self.is_connected = True
  171. except Exception as e:
  172. log.critical('connect error: %s' % e, exc_info=True)
  173. self.is_connected = False
  174. self.reconnect()
  175. if config.DEBUG_MODE:
  176. traceback.print_exc()
  177. finally:
  178. if config.RESET_INIT_TIME:
  179. self._init_time = time.time()
  180. if self.param.is_greenroom and not self.is_green_connected:
  181. threading.Thread(target=self.__connect_green).start()
  182. self.__callback()
  183.  
  184. def __connect_green(self):
  185. """ Make a connection to the greenroom application. """
  186. if not self.is_green_connected:
  187. try:
  188. self.green_connection = rtmp.RtmpClient(
  189. ip=self.param.ip,
  190. port=self.param.port,
  191. tc_url=self.param.tc_url,
  192. app=self.param.app,
  193. page_url=self.param.embed_url,
  194. swf_url=self.param.swf_url,
  195. proxy=self._proxy,
  196. is_win=True
  197. )
  198. self.green_connection.connect(
  199. {
  200. 'account': '',
  201. 'type': self.param.roomtype,
  202. 'prefix': u'greenroom',
  203. 'room': self.roomname,
  204. 'version': self.param.desktop_version,
  205. 'cookie': ''
  206. }
  207. )
  208. self.is_green_connected = True
  209. except Exception as e:
  210. log.critical('greenroom connect error: %s' % e, exc_info=True)
  211. self.is_green_connected = False
  212. self.reconnect(greenroom=True)
  213. if config.DEBUG_MODE:
  214. traceback.print_exc()
  215. finally:
  216. self.__green_callback()
  217.  
  218. def disconnect(self, greenroom=False):
  219. """ Close the connection with the remote RTMP server.
  220.  
  221. :param greenroom: True closes the greenroom connection,
  222. else close the normal connection.
  223. :type greenroom: bool
  224. """
  225. log.info('disconnecting from server.')
  226. try:
  227. if greenroom:
  228. log.info('disconnection from greenroom application')
  229. self.is_green_connected = False
  230. self.green_connection.shutdown()
  231. else:
  232. self.is_connected = False
  233. self._bauth_key = None
  234. self.users.clear()
  235. self.connection.shutdown()
  236. except Exception as e:
  237. log.error('disconnect error, greenroom: %s, error: %s' % (greenroom, e), exc_info=True)
  238. if config.DEBUG_MODE:
  239. traceback.print_exc()
  240.  
  241. def reconnect(self, greenroom=False):
  242. """ Reconnect to the application.
  243.  
  244. :param greenroom: True reconnect to the greenroom application else
  245. reconnect to the normal application(room)
  246. :type greenroom: bool
  247. """
  248. if greenroom:
  249. log.info('reconnecting to the greenroom application.')
  250. self.disconnect(greenroom=True)
  251. time.sleep(config.RECONNECT_DELAY)
  252. self.__connect_green()
  253. else:
  254. reconnect_msg = '============ RECONNECTING IN %s SECONDS ============' % self._reconnect_delay
  255. log.info('reconnecting: %s' % reconnect_msg)
  256. self.console_write(COLOR['bright_cyan'], reconnect_msg)
  257. self._is_reconnected = True
  258. self.disconnect()
  259. time.sleep(self._reconnect_delay)
  260.  
  261. # increase reconnect_delay after each reconnect.
  262. self._reconnect_delay *= 2
  263. if self._reconnect_delay > 900:
  264. self._reconnect_delay = config.RECONNECT_DELAY
  265.  
  266. if self.account and self.password:
  267. if not self.login():
  268. self.console_write(COLOR['bright_red'], 'Failed to login.')
  269. else:
  270. self.console_write(COLOR['bright_green'], 'Login okay.')
  271. self.set_rtmp_parameters()
  272. if self.param.config_status == 3:
  273. self.connect()
  274. else:
  275. msg = 'failed to set rtmp parameters, %s' % self.param.config_status
  276. log.error(msg)
  277. if config.DEBUG_MODE:
  278. self.console_write(COLOR['bright_red'], msg)
  279.  
  280. def __green_callback(self):
  281. """ Read packets from the greenroom RTMP application. """
  282. log.info('starting greenroom callback loop. is_green_connected: %s' % self.is_green_connected)
  283. fails = 0
  284. amf0_data_type = - 1
  285. amf0_data = None
  286. while self.is_green_connected:
  287. try:
  288. amf0_data = self.green_connection.amf()
  289. amf0_data_type = amf0_data['msg']
  290. except rtmp.AmfDataReadError as e:
  291. fails += 1
  292. log.error('greenroom amf read error: %s %s' % (fails, e), exc_info=True)
  293. if fails == 2:
  294. if config.DEBUG_MODE:
  295. traceback.print_exc()
  296. self.reconnect(greenroom=True)
  297. break
  298. else:
  299. fails = 0
  300. try:
  301. if amf0_data_type == rtmp.rtmp_type.DT_COMMAND:
  302.  
  303. amf0_cmd = amf0_data['command']
  304. cmd = amf0_cmd[0]
  305.  
  306. if cmd == '_result':
  307. self.on_result(amf0_cmd, greenroom=True)
  308.  
  309. elif cmd == '_error':
  310. self.on_error(amf0_cmd, greenroom=True)
  311.  
  312. elif cmd == 'notice':
  313. notice_msg = amf0_cmd[3]
  314. notice_msg_id = amf0_cmd[4]
  315. if notice_msg == 'avon':
  316. avon_name = amf0_cmd[5]
  317. self.on_avon(notice_msg_id, avon_name, greenroom=True)
  318. else:
  319. if config.DEBUG_MODE:
  320. self.console_write(COLOR['white'], 'ignoring greenroom command: %s' % cmd)
  321.  
  322. except Exception as gge:
  323. log.error('general greenroom callback error: %s' % gge, exc_info=True)
  324. if config.DEBUG_MODE:
  325. traceback.print_exc()
  326. self.reconnect(greenroom=True)
  327.  
  328. def __callback(self):
  329. """ Read packets from the RTMP application. """
  330. log.info('starting callback loop. is_connected: %s' % self.is_connected)
  331. fails = 0
  332. amf0_data_type = -1
  333. amf0_data = None
  334. while self.is_connected:
  335. try:
  336. amf0_data = self.connection.amf()
  337. amf0_data_type = amf0_data['msg']
  338. except rtmp.AmfDataReadError as e:
  339. fails += 1
  340. log.error('amf data read error count: %s %s' % (fails, e), exc_info=True)
  341. if fails == 2:
  342. if config.DEBUG_MODE:
  343. traceback.print_exc()
  344. self.reconnect()
  345. break
  346. else:
  347. fails = 0
  348. try:
  349. if amf0_data_type == rtmp.rtmp_type.DT_COMMAND:
  350.  
  351. create_stream_res = self.connection.is_create_stream_response(amf0_data)
  352. if create_stream_res:
  353. msg = 'create stream response, stream_id: %s' % self.connection.stream_id
  354. log.info(msg)
  355. self.connection.publish(self._client_id)
  356. if config.DEBUG_MODE:
  357. self.console_write(COLOR['white'], msg)
  358. continue
  359.  
  360. amf0_cmd = amf0_data['command']
  361. cmd = amf0_cmd[0]
  362. iparam0 = 0
  363.  
  364. if cmd == '_result':
  365. self.on_result(amf0_cmd)
  366.  
  367. elif cmd == '_error':
  368. self.on_error(amf0_cmd)
  369.  
  370. elif cmd == 'onBWDone':
  371. self.on_bwdone()
  372.  
  373. elif cmd == 'onStatus':
  374. self.on_status(amf0_cmd)
  375.  
  376. elif cmd == 'registered':
  377. client_info_dict = amf0_cmd[3]
  378. self.on_registered(client_info_dict)
  379.  
  380. elif cmd == 'join':
  381. join_info = amf0_cmd[3]
  382. threading.Thread(target=self.on_join, args=(join_info,)).start()
  383.  
  384. elif cmd == 'joins':
  385. current_room_users_info_list = amf0_cmd[3:]
  386. if len(current_room_users_info_list) is not 0:
  387. while iparam0 < len(current_room_users_info_list):
  388. self.on_joins(current_room_users_info_list[iparam0])
  389. iparam0 += 1
  390.  
  391. elif cmd == 'joinsdone':
  392. self.on_joinsdone()
  393.  
  394. elif cmd == 'oper':
  395. oper_id_name = amf0_cmd[3:]
  396. while iparam0 < len(oper_id_name):
  397. oper_id = str(int(oper_id_name[iparam0]))
  398. oper_name = oper_id_name[iparam0 + 1]
  399. if len(oper_id) == 1:
  400. self.on_oper(oper_id[0], oper_name)
  401. iparam0 += 2
  402.  
  403. elif cmd == 'deop':
  404. deop_id = amf0_cmd[3]
  405. deop_nick = amf0_cmd[4]
  406. self.on_deop(deop_id, deop_nick)
  407.  
  408. # elif cmd == 'owner':
  409. # self.on_owner()
  410.  
  411. elif cmd == 'avons':
  412. avons_id_name = amf0_cmd[4:]
  413. if len(avons_id_name) is not 0:
  414. while iparam0 < len(avons_id_name):
  415. avons_id = avons_id_name[iparam0]
  416. avons_name = avons_id_name[iparam0 + 1]
  417. self.on_avon(avons_id, avons_name)
  418. iparam0 += 2
  419.  
  420. elif cmd == 'pros':
  421. pro_ids = amf0_cmd[4:]
  422. if len(pro_ids) is not 0:
  423. for pro_id in pro_ids:
  424. pro_id = str(int(pro_id))
  425. self.on_pro(pro_id)
  426.  
  427. elif cmd == 'nick':
  428. old_nick = amf0_cmd[3]
  429. new_nick = amf0_cmd[4]
  430. nick_id = int(amf0_cmd[5])
  431. self.on_nick(old_nick, new_nick, nick_id)
  432.  
  433. elif cmd == 'nickinuse':
  434. self.on_nickinuse()
  435.  
  436. elif cmd == 'quit':
  437. quit_name = amf0_cmd[3]
  438. quit_id = amf0_cmd[4]
  439. self.on_quit(quit_id, quit_name)
  440.  
  441. elif cmd == 'kick':
  442. kick_id = amf0_cmd[3]
  443. kick_name = amf0_cmd[4]
  444. self.on_kick(kick_id, kick_name)
  445.  
  446. elif cmd == 'banned':
  447. self.on_banned()
  448.  
  449. elif cmd == 'banlist':
  450. banlist_id_nick = amf0_cmd[3:]
  451. if len(banlist_id_nick) is not 0:
  452. while iparam0 < len(banlist_id_nick):
  453. banned_id = banlist_id_nick[iparam0]
  454. banned_nick = banlist_id_nick[iparam0 + 1]
  455. self.on_banlist(banned_id, banned_nick)
  456. iparam0 += 2
  457.  
  458. elif cmd == 'startbanlist':
  459. # print(amf0_data)
  460. pass
  461.  
  462. elif cmd == 'topic':
  463. topic = amf0_cmd[3]
  464. self.on_topic(topic)
  465.  
  466. elif cmd == 'from_owner':
  467. owner_msg = amf0_cmd[3]
  468. self.on_from_owner(owner_msg)
  469.  
  470. elif cmd == 'doublesignon':
  471. self.on_doublesignon()
  472.  
  473. elif cmd == 'privmsg':
  474. raw_msg = amf0_cmd[4]
  475. msg_color = amf0_cmd[5]
  476. msg_sender = amf0_cmd[6]
  477. self.on_privmsg(msg_sender, raw_msg, msg_color)
  478.  
  479. elif cmd == 'notice':
  480. notice_msg = amf0_cmd[3]
  481. notice_msg_id = amf0_cmd[4]
  482. if notice_msg == 'avon':
  483. avon_name = amf0_cmd[5]
  484. self.on_avon(notice_msg_id, avon_name)
  485. elif notice_msg == 'pro':
  486. self.on_pro(notice_msg_id)
  487.  
  488. elif cmd == 'gift':
  489. gift_to = amf0_cmd[3]
  490. gift_sender = amf0_data[4]
  491. gift_info = amf0_data[5]
  492. self.on_gift(gift_sender, gift_to, gift_info)
  493.  
  494. else:
  495. self.console_write(COLOR['bright_red'], 'Unknown command: %s' % cmd)
  496.  
  497. except Exception as ex:
  498. log.error('general callback error: %s' % ex, exc_info=True)
  499. if config.DEBUG_MODE:
  500. traceback.print_exc()
  501.  
  502. # Callback Event Methods.
  503. def on_result(self, result_info, greenroom=False):
  504. """ Default NetConnection message containing info about the connection.
  505.  
  506. :param result_info: Info containing various information.
  507. :type result_info: dict
  508. :param greenroom: True if the info is from the greenroom connection.
  509. :type greenroom: bool
  510. """
  511. if config.DEBUG_MODE:
  512. if greenroom:
  513. self.console_write(COLOR['white'], '## Greenroom result..')
  514. for list_item in result_info:
  515. if type(list_item) is rtmp.pyamf.ASObject:
  516. for k in list_item:
  517. self.console_write(COLOR['white'], k + ': ' + str(list_item[k]))
  518. else:
  519. self.console_write(COLOR['white'], str(list_item))
  520.  
  521. def on_error(self, error_info, greenroom=False):
  522. """ Default NetConnection message containing error info about the connection.
  523.  
  524. :param error_info: Info containing various error information.
  525. :type error_info: dict
  526. :param greenroom: True if the error info is from the greenroom connection.
  527. :type greenroom: bool
  528. """
  529. if config.DEBUG_MODE:
  530. if greenroom:
  531. self.console_write(COLOR['bright_red'], '## Greenroom error..')
  532. for list_item in error_info:
  533. if type(list_item) is rtmp.pyamf.ASObject:
  534. for k in list_item:
  535. self.console_write(COLOR['bright_red'], k + ': ' + str(list_item[k]))
  536. else:
  537. self.console_write(COLOR['bright_red'], str(list_item))
  538.  
  539. def on_status(self, status_info):
  540. """ Default NetStream status updates message.
  541.  
  542. :param status_info: Info containing various information related to a NetStream
  543. :type status_info: dict
  544. """
  545. if config.DEBUG_MODE:
  546. for list_item in status_info:
  547. if type(list_item) is rtmp.pyamf.ASObject:
  548. for k in list_item:
  549. self.console_write(COLOR['white'], k + ': ' + str(list_item[k]))
  550. else:
  551. self.console_write(COLOR['white'], str(list_item))
  552.  
  553. def on_bwdone(self):
  554. """ Application specific message. """
  555. if not self._is_reconnected:
  556. if config.ENABLE_AUTO_JOB:
  557. self.start_auto_job_timer()
  558.  
  559. def on_registered(self, client_info):
  560. """ Application message containing client info.
  561.  
  562. :param client_info: Info containing client information.
  563. :type client_info: dict
  564. """
  565. self._client_id = client_info['id']
  566. self.is_client_mod = client_info['mod']
  567. self.is_client_owner = client_info['own']
  568. client = self.users.add(client_info)
  569. client.user_level = 0
  570.  
  571. self.console_write(COLOR['bright_green'], 'registered with ID: %d' % self._client_id)
  572.  
  573. key = self.param.get_captcha_key(self._client_id)
  574. if key is None:
  575. self.console_write(COLOR['bright_red'],
  576. 'There was a problem obtaining the captcha key. Key=%s' % str(key))
  577. else:
  578. self.console_write(COLOR['bright_green'], 'Captcha key: %s' % key)
  579. self.send_cauth_msg(key)
  580. self.set_nick()
  581.  
  582. def on_join(self, join_info):
  583. """ Application message received when a user joins the room.
  584.  
  585. :param join_info: Information about the user joining.
  586. :type join_info: dict
  587. """
  588. _user = self.users.add(join_info)
  589. if _user is not None:
  590. if _user.account:
  591. tc_info = apis.tinychat.user_info(_user.account)
  592. if tc_info is not None:
  593. _user.tinychat_id = tc_info['tinychat_id']
  594. _user.last_login = tc_info['last_active']
  595. if _user.is_owner:
  596. _user.user_level = 1
  597. self.console_write(COLOR['red'], 'Room Owner %s:%d:%s' %
  598. (_user.nick, _user.id, _user.account))
  599. elif _user.is_mod:
  600. _user.user_level = 3
  601. self.console_write(COLOR['bright_red'], 'Moderator %s:%d:%s' %
  602. (_user.nick, _user.id, _user.account))
  603. else:
  604. self.console_write(COLOR['bright_yellow'], '%s:%d has account: %s' %
  605. (_user.nick, _user.id, _user.account))
  606. else:
  607. if _user.id is not self._client_id:
  608. self.console_write(COLOR['cyan'], '%s:%d joined the room.' % (_user.nick, _user.id))
  609. else:
  610. log.warning('user join: %s' % _user)
  611.  
  612. def on_joins(self, joins_info):
  613. """ Application message received for every user in the room when the client joins the room.
  614.  
  615. :param joins_info: Information about a user.
  616. :type joins_info: dict
  617. """
  618. _user = self.users.add(joins_info)
  619. if _user is not None:
  620. if _user.account:
  621. if _user.is_owner:
  622. _user.user_level = 1
  623. self.console_write(COLOR['red'], 'Joins Room Owner %s:%d:%s' %
  624. (_user.nick, _user.id, _user.account))
  625. elif _user.is_mod:
  626. _user.user_level = 3
  627. self.console_write(COLOR['bright_red'], 'Joins Moderator %s:%d:%s' %
  628. (_user.nick, _user.id, _user.account))
  629. else:
  630. self.console_write(COLOR['bright_yellow'], 'Joins %s:%d:%s' %
  631. (_user.nick, _user.id, _user.account))
  632. else:
  633. if joins_info['id'] is not self._client_id:
  634. self.console_write(COLOR['bright_cyan'], 'Joins %s:%d' % (_user.nick, _user.id))
  635. else:
  636. log.warning('user joins: %s' % _user)
  637.  
  638. def on_joinsdone(self):
  639. """ Application message received when all room users information have been received. """
  640. if self.is_client_mod:
  641. self.send_banlist_msg()
  642.  
  643. def on_oper(self, uid, nick):
  644. """ Application message received when a user is oper(moderator)
  645.  
  646. NOTE: This message is most likely deprecated.
  647. :param uid: str the Id of the oper.
  648. :param nick: str the nick of the user
  649. """
  650. _user = self.users.search(nick)
  651. _user.is_mod = True
  652. if uid != self._client_id:
  653. self.console_write(COLOR['bright_red'], '%s:%s is moderator.' % (nick, uid))
  654.  
  655. def on_deop(self, uid, nick):
  656. """ Application message received when a moderator is de-moderated(deop)
  657.  
  658. NOTE: This message is most likely deprecated.
  659. :param uid: str the Id of the moderator.
  660. :param nick: str the nick of the moderator.
  661. """
  662. _user = self.users.search(nick)
  663. _user.is_mod = False
  664. self.console_write(COLOR['red'], '%s:%s was deoped.' % (nick, uid))
  665.  
  666. def on_avon(self, uid, name, greenroom=False):
  667. """ Application message received when a user starts broadcasting.
  668.  
  669. :param uid: The Id of the user.
  670. :type uid: str
  671. :param name: The nick name of the user.
  672. :type name: str
  673. :param greenroom: True the user is waiting in the greenroom.
  674. :type greenroom: bool
  675. """
  676. if greenroom:
  677. _user = self.users.search_by_id(name)
  678. if _user is not None:
  679. _user.is_waiting = True
  680. self.console_write(COLOR['bright_yellow'], '%s:%s is waiting in the greenroom' %
  681. (_user.nick, _user.id))
  682. else:
  683. _user = self.users.search(name)
  684. if _user is not None and _user.is_waiting:
  685. _user.is_waiting = False
  686. self.console_write(COLOR['cyan'], '%s:%s is broadcasting.' % (name, uid))
  687.  
  688. def on_pro(self, uid):
  689. """ Application message received when a user is using a PRO account.
  690.  
  691. :param uid: The Id of the pro user.
  692. :type uid: str
  693. """
  694. _user = self.users.search_by_id(uid)
  695. if _user is not None:
  696. self.console_write(COLOR['bright_magenta'], '%s:%s is pro.' % (_user.nick, uid))
  697. else:
  698. self.console_write(COLOR['bright_magenta'], '%s is pro.' % uid)
  699.  
  700. def on_nick(self, old, new, uid):
  701. """ Application message received when a user changes nick name.
  702.  
  703. :param old: The old nick name of the user.
  704. :type old: str
  705. :param new: The new nick name of the user
  706. :type new: str
  707. :param uid: The Id of the user.
  708. :type uid: int
  709. """
  710. if uid == self._client_id:
  711. self.nickname = new
  712. old_info = self.users.search(old)
  713. old_info.nick = new
  714. if not self.users.change(old, new, old_info):
  715. log.error('failed to change nick for user: %s' % new)
  716. self.console_write(COLOR['bright_cyan'], '%s:%s changed nick to: %s' % (old, uid, new))
  717.  
  718. def on_nickinuse(self):
  719. """ Application message received when the client nick name chosen is already in use."""
  720. self.nickname += string_util.create_random_string(1, 5)
  721. self.console_write(COLOR['white'], 'Nick already taken. Changing nick to: %s' % self.nickname)
  722. self.set_nick()
  723.  
  724. def on_quit(self, uid, name):
  725. """ Application message received when a user leaves the room.
  726.  
  727. :param uid: The Id of the user leaving.
  728. :type uid: str
  729. :param name: The nick name of the user leaving.
  730. :type name: str
  731. """
  732. log.debug('%s:%s left the room' % (name, uid))
  733. if self.users.delete(name):
  734. self.console_write(COLOR['cyan'], '%s:%s left the room.' % (name, uid))
  735. else:
  736. msg = 'failed to delete user: %s' % name
  737. if config.DEBUG_MODE:
  738. self.console_write(COLOR['bright_red'], msg)
  739. log.debug(msg)
  740.  
  741. def on_kick(self, uid, name):
  742. """ Application message received when a user gets banned.
  743.  
  744. :param uid: The Id of the user getting banned.
  745. :type uid: str
  746. :param name: The nick name of the user getting banned.
  747. :type name: str
  748. """
  749. self.console_write(COLOR['bright_red'], '%s:%s was banned.' % (name, uid))
  750. self.send_banlist_msg()
  751.  
  752. def on_banned(self):
  753. """ Application message received when the client is banned from a room. """
  754. self.console_write(COLOR['red'], 'You are banned from this room.')
  755.  
  756. def on_banlist(self, uid, nick):
  757. """ Application message containing a user currently on the banlist.
  758.  
  759. :param uid: Id of the banned user.
  760. :type uid: str
  761. :param nick: nick name of the banned user.
  762. :type nick: str
  763. """
  764. self.console_write(COLOR['bright_red'], 'Banned user: %s:%s' % (nick, uid))
  765.  
  766. def on_topic(self, topic):
  767. """ Application message containing the rooms topic(if any)
  768.  
  769. :param topic: The topic of the room.
  770. :type topic: str
  771. """
  772. topic_msg = topic.encode('utf-8', 'replace')
  773. self.console_write(COLOR['cyan'], 'room topic: ' + topic_msg)
  774.  
  775. def on_from_owner(self, owner_msg):
  776. """ Application message received when a user's broadcast gets closed by a moderator.
  777.  
  778. NOTE: There are other types of this message kind..
  779. :param owner_msg: url encoded string.
  780. :type owner_msg: str
  781. """
  782. msg = str(owner_msg).replace('notice', '')
  783. msg = string_util.unquote(msg.encode('utf-8'))
  784. self.console_write(COLOR['bright_red'], msg)
  785.  
  786. def on_doublesignon(self):
  787. """ Application message received when the client account is already in the room. """
  788. self.console_write(COLOR['bright_red'], 'Double account sign on. Aborting!')
  789. self.is_connected = False
  790.  
  791. def on_reported(self, nick, uid):
  792. """ Application message received when the client gets reported by another user in the room.
  793.  
  794. :param nick: The nick name of the user reporting the client.
  795. :type nick: str
  796. :param uid: The Id of the user reporting the client.
  797. :type uid: str
  798. """
  799. self.console_write(COLOR['bright_red'], 'You were reported by: %s:%s' % (nick, uid))
  800.  
  801. def on_gift(self, gift_sender, gift_receiver, gift_info):
  802. """ Application message received when a user sends another user a gift.
  803.  
  804. NOTE: This is not fully tested/discovered
  805. :param gift_sender: The gift senders information.
  806. :type gift_sender: dict
  807. :param gift_receiver: The gift receiver information.
  808. :type gift_receiver: dict
  809. :param gift_info: The gift information.
  810. :type gift_info: dict
  811. """
  812. sender_nick = gift_sender['name']
  813. receiver_nick = gift_receiver['name']
  814. receiver_gp = int(gift_receiver['points'])
  815. gift_name = gift_info['name']
  816. gift_comment = gift_info['comment']
  817.  
  818. self.console_write(COLOR['bright_magenta'], '%s sent gift %s to %s points: %s, comment: %s' %
  819. (sender_nick, gift_name, receiver_nick, receiver_gp, gift_comment))
  820.  
  821. def on_privmsg(self, msg_sender, raw_msg, msg_color):
  822. """ Application message received when a user sends a chat message among other.
  823.  
  824. Several commands are sent through this type of message, this method
  825. sorts the different commands out, and sends them of to the correct event handlers(methods).
  826.  
  827. :param msg_sender: The nick name of the message sender.
  828. :type msg_sender: str
  829. :param raw_msg: The message as comma seperated decimals.
  830. :type raw_msg: str
  831. :param msg_color: The chat message color.
  832. :type msg_color: str
  833. """
  834.  
  835. # Get user info object of the user sending the message..
  836. self.active_user = self.users.search(msg_sender)
  837.  
  838. # decode the message from comma separated decimal to normal text
  839. decoded_msg = self._decode_msg(u'' + raw_msg)
  840.  
  841. if decoded_msg.startswith('/'):
  842. msg_cmd = decoded_msg.split(' ')
  843. if msg_cmd[0] == '/msg':
  844. private_msg = ' '.join(msg_cmd[2:])
  845. self.private_message_handler(private_msg.strip())
  846.  
  847. elif msg_cmd[0] == '/reported':
  848. self.on_reported(self.active_user.nick, self.active_user.id)
  849.  
  850. elif msg_cmd[0] == '/mbs':
  851. if self.active_user.is_mod:
  852. if len(msg_cmd) == 4:
  853. media_type = msg_cmd[1]
  854. media_id = msg_cmd[2]
  855. threading.Thread(target=self.on_media_broadcast_start,
  856. args=(media_type, media_id, msg_sender,)).start()
  857.  
  858. elif msg_cmd[0] == '/mbc':
  859. if self.active_user.is_mod:
  860. if len(msg_cmd) == 2:
  861. media_type = msg_cmd[1]
  862. self.on_media_broadcast_close(media_type, msg_sender)
  863.  
  864. elif msg_cmd[0] == '/mbpa':
  865. if self.active_user.is_mod:
  866. if len(msg_cmd) == 2:
  867. media_type = msg_cmd[1]
  868. self.on_media_broadcast_paused(media_type, msg_sender)
  869.  
  870. elif msg_cmd[0] == '/mbpl':
  871. if self.active_user.is_mod:
  872. if len(msg_cmd) == 3:
  873. media_type = msg_cmd[1]
  874. time_point = int(msg_cmd[2])
  875. self.on_media_broadcast_play(media_type, time_point, msg_sender)
  876.  
  877. elif msg_cmd[0] == '/mbsk':
  878. if self.active_user.is_mod:
  879. if len(msg_cmd) == 3:
  880. media_type = msg_cmd[1]
  881. time_point = int(msg_cmd[2])
  882. self.on_media_broadcast_skip(media_type, time_point, msg_sender)
  883. else:
  884. if len(msg_color) == 10:
  885. self.message_handler(decoded_msg.strip())
  886. else:
  887. log.warning('rejecting chat msg from: %s:%s with unusual msg color: %s' %
  888. (self.active_user.nick, self.active_user.id, msg_color))
  889.  
  890. # Message Handler.
  891. def message_handler(self, decoded_msg):
  892. """ Message handler.
  893.  
  894. :param decoded_msg: The decoded msg(text).
  895. :type decoded_msg: str
  896. """
  897. self.console_write(COLOR['green'], '%s: %s ' % (self.active_user.nick, decoded_msg))
  898.  
  899. # Private Message Handler.
  900. def private_message_handler(self, private_msg):
  901. """ A user private message the client.
  902.  
  903. :param private_msg: The private message.
  904. :type private_msg: str
  905. """
  906. self.console_write(COLOR['white'], 'Private message from %s: %s' % (self.active_user.nick, private_msg))
  907.  
  908. # Media Events Handlers.
  909. def on_media_broadcast_start(self, media_type, video_id, usr_nick):
  910. """ A user started a media broadcast.
  911.  
  912. :param media_type: The type of media. youTube or soundCloud.
  913. :type media_type: str
  914. :param video_id: The youtube ID or souncloud trackID.
  915. :type video_id: str
  916. :param usr_nick: The user name of the user playing media.
  917. :type usr_nick: str
  918. """
  919. self.console_write(COLOR['bright_magenta'], '%s is playing %s %s' % (usr_nick, media_type, video_id))
  920.  
  921. def on_media_broadcast_close(self, media_type, usr_nick):
  922. """ A user closed a media broadcast.
  923.  
  924. :param media_type: The type of media. youTube or soundCloud.
  925. :type media_type: str
  926. :param usr_nick: The user name of the user closing the media.
  927. :type usr_nick: str
  928. """
  929. self.console_write(COLOR['bright_magenta'], '%s closed the %s' % (usr_nick, media_type))
  930.  
  931. def on_media_broadcast_paused(self, media_type, usr_nick):
  932. """ A user paused the media broadcast.
  933.  
  934. :param media_type: The type of media being paused. youTube or soundCloud.
  935. :type media_type: str
  936. :param usr_nick: The user name of the user pausing the media.
  937. :type usr_nick: str
  938. """
  939. self.console_write(COLOR['bright_magenta'], '%s paused the %s' % (usr_nick, media_type))
  940.  
  941. def on_media_broadcast_play(self, media_type, time_point, usr_nick):
  942. """ A user resumed playing a media broadcast.
  943.  
  944. :param media_type: The media type. youTube or soundCloud.
  945. :type media_type: str
  946. :param time_point: The time point in the tune in milliseconds.
  947. :type time_point: int
  948. :param usr_nick: The user resuming the tune.
  949. :type usr_nick: str
  950. """
  951. self.console_write(COLOR['bright_magenta'], '%s resumed the %s at: %s' %
  952. (usr_nick, media_type, time_point))
  953.  
  954. def on_media_broadcast_skip(self, media_type, time_point, usr_nick):
  955. """ A user time searched a tune.
  956.  
  957. :param media_type: The media type. youTube or soundCloud.
  958. :type media_type: str
  959. :param time_point: The time point in the tune in milliseconds.
  960. :type time_point: int
  961. :param usr_nick: The user time searching the tune.
  962. :type usr_nick: str
  963. """
  964. self.console_write(COLOR['bright_magenta'], '%s time searched the %s at: %s ' %
  965. (usr_nick, media_type, time_point))
  966.  
  967. # Message Methods.
  968. def send_bauth_msg(self):
  969. """ Get and send the bauth key needed before we can start a broadcast. """
  970. if self._bauth_key is not None:
  971. self.connection.call('bauth', [u'' + self._bauth_key])
  972. else:
  973. _token = self.param.get_broadcast_token(self.nickname, self._client_id)
  974. if _token != 'PW':
  975. self._bauth_key = _token
  976. self.connection.call('bauth', [u'' + _token])
  977.  
  978. def send_cauth_msg(self, cauthkey):
  979. """ Send the cauth message, we need to send this before we can chat.
  980.  
  981. :param cauthkey: The cauth key.
  982. :type cauthkey: str
  983. """
  984. self.connection.call('cauth', [u'' + cauthkey])
  985.  
  986. def send_owner_run_msg(self, msg):
  987. """ Send owner run message.
  988.  
  989. :param msg: The message to send.
  990. :type msg: str
  991. """
  992. if self.is_client_mod:
  993. msg = string_util.quote_str(msg)
  994. self.connection.call('owner_run', [u'notice' + msg])
  995.  
  996. def send_cam_approve_msg(self, nick, uid=None):
  997. """ Send cam approval message.
  998.  
  999. NOTE: if no uid is provided, we try and look up the user by nick.
  1000. :param nick: The nick to be approved.
  1001. :type nick: str
  1002. :param uid: (optional) The user id.
  1003. :type uid: int | str
  1004. """
  1005. if self.is_client_mod and self.param.bpassword is not None:
  1006. msg = '/allowbroadcast %s' % self.param.bpassword
  1007. if uid is None:
  1008. _user = self.users.search(nick)
  1009. if _user is not None:
  1010. self.connection.call('privmsg', [u'' + self._encode_msg(msg), u'#0,en',
  1011. u'n' + str(_user.id) + '-' + nick])
  1012. else:
  1013. self.connection.call('privmsg',
  1014. [u'' + self._encode_msg(msg), u'#0,en', u'n' + str(uid) + '-' + nick])
  1015.  
  1016. def send_chat_msg(self, msg):
  1017. """ Send a chat room message.
  1018.  
  1019. :param msg: The message to send.
  1020. :type msg: str
  1021. """
  1022. self.connection.call('privmsg', [u'' + self._encode_msg(msg), u'#262626,en'])
  1023.  
  1024. def send_private_msg(self, msg, nick):
  1025. """ Send a private message.
  1026.  
  1027. :param msg: The private message to send.
  1028. :type msg: str
  1029. :param nick: The user name to receive the message.
  1030. :type nick: str
  1031. """
  1032. _user = self.users.search(nick)
  1033. if _user is not None:
  1034. self.connection.call('privmsg', [u'' + self._encode_msg('/msg ' + nick + ' ' + msg),
  1035. u'#262626,en', u'n' + str(_user.id) + '-' + nick])
  1036. self.connection.call('privmsg', [u'' + self._encode_msg('/msg ' + nick + ' ' + msg),
  1037. u'#262626,en', u'b' + str(_user.id) + '-' + nick])
  1038.  
  1039. def send_userinfo_request_msg(self, user_id):
  1040. """ Send user info request to a user.
  1041.  
  1042. :param user_id: User id of the user we want info from.
  1043. :type user_id: str
  1044. """
  1045. self.connection.call('account', [u'' + str(user_id)])
  1046.  
  1047. def send_undercover_msg(self, nick, msg, use_b=True, use_n=True):
  1048. """ Send a 'undercover' message.
  1049.  
  1050. This is a special message that appears in the main chat, but is only visible to the user it is sent to.
  1051. It can also be used to play 'private' youtubes/soundclouds with.
  1052.  
  1053. :param nick: The user name to send the message to.
  1054. :type nick: str
  1055. :param msg: The message to send.
  1056. :type msg: str
  1057. :param use_b:
  1058. :param use_n:
  1059. """
  1060. _user = self.users.search(nick)
  1061. if _user is not None:
  1062. if use_b:
  1063. self.connection.call('privmsg', [u'' + self._encode_msg(msg),
  1064. '#0,en', u'b' + str(_user.id) + '-' + nick])
  1065. if use_n:
  1066. self.connection.call('privmsg', [u'' + self._encode_msg(msg),
  1067. '#0,en', u'n' + str(_user.id) + '-' + nick])
  1068.  
  1069. def set_nick(self):
  1070. """ Send the nick message. """
  1071. if not self.nickname:
  1072. self.nickname = string_util.create_random_string(5, 25)
  1073. self.console_write(COLOR['bright_magenta'], 'Setting nick: %s' % self.nickname)
  1074. self.connection.call('nick', [u'' + self.nickname])
  1075.  
  1076. def send_ban_msg(self, nick, uid=None):
  1077. """ Send ban message.
  1078.  
  1079. :param nick: The nick name of the user to ban.
  1080. :type nick: str
  1081. :param uid: The Id of the user to ban (optional)
  1082. :type uid: int | str
  1083. """
  1084. if self.is_client_mod:
  1085. if uid is None:
  1086. _user = self.users.search(nick)
  1087. if _user is not None:
  1088. self.connection.call('kick', [u'' + nick, str(_user.id)])
  1089. else:
  1090. self.connection.call('kick', [u'' + nick, str(uid)])
  1091.  
  1092. def send_forgive_msg(self, uid):
  1093. """ Send forgive message.
  1094.  
  1095. :param uid: The ID of the user we want to forgive.
  1096. :type uid: int | str
  1097. """
  1098. if self.is_client_mod:
  1099. self.connection.call('forgive', [u'' + str(uid)])
  1100. # get the updated ban list.
  1101. self.send_banlist_msg()
  1102.  
  1103. def send_banlist_msg(self):
  1104. """ Send ban list message. """
  1105. if self.is_client_mod:
  1106. self.connection.call('banlist')
  1107.  
  1108. def send_topic_msg(self, topic):
  1109. """ Send a room topic message.
  1110.  
  1111. :param topic: The room topic.
  1112. :type topic: str
  1113. """
  1114. if self.is_client_mod:
  1115. self.connection.call('topic', [u'' + topic])
  1116.  
  1117. def send_close_user_msg(self, nick):
  1118. """ Send close user broadcast message.
  1119.  
  1120. :param nick: The user name of the user we want to close.
  1121. :type nick: str
  1122. """
  1123. if self.is_client_mod:
  1124. self.connection.call('owner_run', [u'_close' + nick])
  1125.  
  1126. # Media Message Functions
  1127. def send_media_broadcast_start(self, media_type, video_id, time_point=0, private_nick=None):
  1128. """ Starts a media broadcast.
  1129.  
  1130. :param media_type: 'youTube' or 'soundCloud'
  1131. :type media_type: str
  1132. :param video_id: The media video ID.
  1133. :type video_id: str
  1134. :param time_point: Where to start the media from in milliseconds.
  1135. :type time_point: int
  1136. :param private_nick: If not None, start the media broadcast for this username only.
  1137. :type private_nick: str
  1138. """
  1139. mbs_msg = '/mbs %s %s %s' % (media_type, video_id, time_point)
  1140. if private_nick is not None:
  1141. self.send_undercover_msg(private_nick, mbs_msg)
  1142. else:
  1143. self.send_chat_msg(mbs_msg)
  1144.  
  1145. def send_media_broadcast_close(self, media_type, private_nick=None):
  1146. """ Close a media broadcast.
  1147.  
  1148. :param media_type: 'youTube' or 'soundCloud'
  1149. :type media_type: str
  1150. :param private_nick If not None, send this message to this username only.
  1151. :type private_nick: str
  1152. """
  1153. mbc_msg = '/mbc %s' % media_type
  1154. if private_nick is not None:
  1155. self.send_undercover_msg(private_nick, mbc_msg)
  1156. else:
  1157. self.send_chat_msg(mbc_msg)
  1158.  
  1159. def send_media_broadcast_play(self, media_type, time_point, private_nick=None):
  1160. """ Play a currently paused media broadcast.
  1161.  
  1162. :param media_type: 'youTube' or 'soundCloud'
  1163. :type media_type: str
  1164. :param time_point: Where to play the media from in milliseconds.
  1165. :type time_point: int
  1166. :param private_nick: If not None, send this message to this username only.
  1167. :type private_nick: str
  1168. """
  1169. mbpl_msg = '/mbpl %s %s' % (media_type, time_point)
  1170. if private_nick is not None:
  1171. self.send_undercover_msg(private_nick, mbpl_msg)
  1172. else:
  1173. self.send_chat_msg(mbpl_msg)
  1174.  
  1175. def send_media_broadcast_pause(self, media_type, private_nick=None):
  1176. """ Pause a currently playing media broadcast.
  1177.  
  1178. :param media_type: 'youTube' or 'soundCloud'
  1179. :type media_type: str
  1180. :param private_nick: If not None, send this message to this username only.
  1181. :type private_nick: str
  1182. """
  1183. mbpa_msg = '/mbpa %s' % media_type
  1184. if private_nick is not None:
  1185. self.send_undercover_msg(private_nick, mbpa_msg)
  1186. else:
  1187. self.send_chat_msg(mbpa_msg)
  1188.  
  1189. def send_media_broadcast_skip(self, media_type, time_point, private_nick=None):
  1190. """ Time search a currently playing/paused media broadcast.
  1191.  
  1192. :param media_type: 'youTube' or 'soundCloud'
  1193. :type media_type: str
  1194. :param time_point: The time point to skip to.
  1195. :type time_point: int
  1196. :param private_nick: if not None, send this message to this username only.
  1197. :type private_nick: str
  1198. """
  1199. mbsk_msg = '/mbsk %s %s' % (media_type, time_point)
  1200. if private_nick is not None:
  1201. self.send_undercover_msg(private_nick, mbsk_msg)
  1202. else:
  1203. self.send_chat_msg(mbsk_msg)
  1204.  
  1205. # Helper Methods
  1206. def get_runtime(self, milliseconds=True):
  1207. """ Get the time the connection has been alive.
  1208.  
  1209. :param milliseconds: True return the time as milliseconds, False return seconds.
  1210. :type milliseconds: bool
  1211. :return: Milliseconds or seconds.
  1212. :rtype: int
  1213. """
  1214. up = int(time.time() - self._init_time)
  1215. if milliseconds:
  1216. return up * 1000
  1217. return up
  1218.  
  1219. @staticmethod
  1220. def _decode_msg(msg):
  1221. """ Decode str from comma separated decimal to normal text str.
  1222.  
  1223. :param msg: The encoded message.
  1224. :return: A normal text.
  1225. :rtype: str
  1226. """
  1227. chars = msg.split(',')
  1228. msg = ''
  1229. for i in chars:
  1230. try:
  1231. msg += unichr(int(i))
  1232. except ValueError as ve:
  1233. log.error('%s' % ve, exc_info=True)
  1234. return msg
  1235.  
  1236. @staticmethod
  1237. def _encode_msg(msg):
  1238. """ Encode normal text str to comma separated decimal.
  1239.  
  1240. :param msg: The normal text to encode
  1241. :type msg: str
  1242. :return: Comma separated decimal string.
  1243. :rtype: str
  1244. """
  1245. return ','.join(str(ord(char)) for char in msg)
  1246.  
  1247. # Timed Auto Method.
  1248. def auto_job_handler(self):
  1249. """ The event handler for auto_job_timer. """
  1250. if self.is_connected:
  1251. self.param.get_config()
  1252. if self.param.config_status is 3:
  1253. if self.is_client_mod:
  1254. if self.param.is_greenroom and not self.is_green_connected:
  1255. # if the greenroom has been enabled after we joined the room.
  1256. threading.Thread(target=self.__connect_green).start()
  1257. elif not self.param.is_greenroom and self.is_green_connected:
  1258. # no need to keep the greenroom connection open
  1259. # if it is not enabled anymore.
  1260. self.disconnect(greenroom=True)
  1261. log.debug('recv configuration: %s' % self.param.config_dict)
  1262. self.start_auto_job_timer()
  1263.  
  1264. def start_auto_job_timer(self):
  1265. """
  1266. Just like using tinychat with a browser, this method will
  1267. fetch the room config from tinychat API every 5 minute(300 seconds)(default).
  1268. See line 228 at http://tinychat.com/embed/chat.js
  1269. """
  1270. threading.Timer(config.AUTO_JOB_INTERVAL, self.auto_job_handler).start()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement