Skip to content

Instantly share code, notes, and snippets.

@mjohnson9
Created November 3, 2012 19:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mjohnson9/4008498 to your computer and use it in GitHub Desktop.
Save mjohnson9/4008498 to your computer and use it in GitHub Desktop.
Python Chatango
################################################################
# File: ch.py
# Title: Chatango Library
# Author: Lumirayz/Lumz <lumirayz@gmail.com>
# Version: 1.31
# Description:
# An event-based library for connecting to one or multiple Chatango rooms, has
# support for several things including: messaging, message font,
# name color, deleting, banning, recent history, 2 userlist modes,
# flagging, avoiding flood bans, detecting flags.
################################################################
################################################################
# License
################################################################
# Copyright 2011 Lumirayz
# This program is distributed under the terms of the GNU GPL.
################################################################
# Imports
################################################################
import socket
import threading
import time
import random
import re
import sys
import select
################################################################
# Python 2 compatibility
################################################################
if sys.version_info[0] < 3:
class urllib:
parse = __import__("urllib")
request = __import__("urllib2")
input = raw_input
else:
import urllib.request
import urllib.parse
################################################################
# Constants
################################################################
Userlist_Recent = 0
Userlist_All = 1
BigMessage_Multiple = 0
BigMessage_Cut = 1
################################################################
# Tagserver stuff
################################################################
specials = {'mitvcanal': 56, 'magicc666': 22, 'livenfree': 18, 'eplsiite': 56, 'soccerjumbo2': 21, 'bguk': 22, 'animachat20': 34, 'pokemonepisodeorg': 55, 'sport24lt': 56, 'mywowpinoy': 5, 'phnoytalk': 21, 'flowhot-chat-online': 12, 'watchanimeonn': 26, 'cricvid-hitcric-': 51, 'fullsportshd2': 18, 'chia-anime': 12, 'narutochatt': 52, 'ttvsports': 56, 'futboldirectochat': 22, 'portalsports': 18, 'stream2watch3': 56, 'proudlypinoychat': 51, 'ver-anime': 34, 'iluvpinas': 53, 'vipstand': 21, 'eafangames': 56, 'worldfootballusch2': 18, 'soccerjumbo': 21, 'myfoxdfw': 22, 'animelinkz': 20, 'rgsmotrisport': 51, 'bateriafina-8': 8, 'as-chatroom': 10, 'dbzepisodeorg': 12, 'tvanimefreak': 54, 'watch-dragonball': 19, 'narutowire': 10, 'leeplarp': 27}
tsweights = [['5', 61], ['6', 61], ['7', 61], ['8', 61], ['16', 61], ['17', 61], ['9', 90], ['11', 90], ['13', 90], ['14', 90], ['15', 90], ['23', 110], ['24', 110], ['25', 110], ['28', 104], ['29', 104], ['30', 104], ['31', 104], ['32', 104], ['33', 104], ['35', 101], ['36', 101], ['37', 101], ['38', 101], ['39', 101], ['40', 101], ['41', 101], ['42', 101], ['43', 101], ['44', 101], ['45', 101], ['46', 101], ['47', 101], ['48', 101], ['49', 101], ['50', 101], ['57', 110], ['58', 110], ['59', 110], ['60', 110], ['61', 110], ['62', 110], ['63', 110], ['64', 110], ['65', 110], ['66', 110]]
def getServer(group):
"""
Get the server host for a certain room.
@type group: str
@param group: room name
@rtype: str
@return: the server's hostname
"""
try:
sn = specials[group]
except KeyError:
group = group.replace("_", "q")
group = group.replace("-", "q")
fnv = float(int(group[0:min(5, len(group))], 36))
lnv = group[6: (6 + min(3, len(group) - 5))]
if(lnv):
lnv = float(int(lnv, 36))
if(lnv <= 1000):
lnv = 1000
else:
lnv = 1000
num = (fnv % lnv) / lnv
maxnum = sum(map(lambda x: x[1], tsweights))
cumfreq = 0
sn = 0
for wgt in tsweights:
cumfreq += float(wgt[1]) / maxnum
if(num <= cumfreq):
sn = int(wgt[0])
break
return "s" + str(sn) + ".chatango.com"
################################################################
# Uid
################################################################
def genUid():
return str(random.randrange(10 ** 15, 10 ** 16))
################################################################
# Message stuff
################################################################
def clean_message(msg):
"""
Clean a message and return the message, n tag and f tag.
@type msg: str
@param msg: the message
@rtype: str, str, str
@returns: cleaned message, n tag contents, f tag contents
"""
n = re.search("<n(.*?)/>", msg)
if n: n = n.group(1)
f = re.search("<f(.*?)>", msg)
if f: f = f.group(1)
msg = re.sub("<n.*?/>", "", msg)
msg = re.sub("<f.*?>", "", msg)
msg = strip_html(msg)
msg = msg.replace("&lt;", "<")
msg = msg.replace("&gt;", ">")
msg = msg.replace("&quot;", "\"")
msg = msg.replace("&apos;", "'")
msg = msg.replace("&amp;", "&")
return msg, n, f
def strip_html(msg):
"""Strip HTML."""
li = msg.split("<")
if len(li) == 1:
return li[0]
else:
ret = list()
for data in li:
data = data.split(">", 1)
if len(data) == 1:
ret.append(data[0])
elif len(data) == 2:
ret.append(data[1])
return "".join(ret)
def parseNameColor(n):
"""This just returns its argument, should return the name color."""
#probably is already the name
return n
def parseFont(f):
"""Parses the contents of a f tag and returns color, face and size."""
#' xSZCOL="FONT"'
try: #TODO: remove quick hack
sizecolor, fontface = f.split("=", 1)
sizecolor = sizecolor.strip()
size = int(sizecolor[1:3])
col = sizecolor[3:6]
if col == "": col = None
face = f.split("\"", 2)[1]
return col, face, size
except:
return None, None, None
################################################################
# Anon id
################################################################
def getAnonId(n, ssid):
"""Gets the anon's id."""
if n == None: n = "5504"
try:
return "".join(list(
map(lambda x: str(x[0] + x[1])[-1], list(zip(
list(map(lambda x: int(x), n)),
list(map(lambda x: int(x), ssid[4:]))
)))
))
except ValueError:
return "NNNN"
################################################################
# PM Auth
################################################################
auth_re = re.compile(r"auth\.chatango\.com ?= ?([^;]*)", re.IGNORECASE)
def _getAuth(name, password):
"""
Request an auid using name and password.
@type name: str
@param name: name
@type password: str
@param password: password
@rtype: str
@return: auid
"""
data = urllib.parse.urlencode({
"user_id": name,
"password": password,
"storecookie": "on",
"checkerrors": "yes"
}).encode()
try:
resp = urllib.request.urlopen("http://chatango.com/login", data)
headers = resp.headers
except Exception:
return None
for header, value in headers.items():
if header.lower() == "set-cookie":
m = auth_re.search(value)
if m:
auth = m.group(1)
if auth == "":
return None
return auth
return None
################################################################
# PM class
################################################################
class PM:
"""Manages a connection with Chatango PM."""
####
# Init
####
def __init__(self, mgr):
self._connected = False
self._mgr = mgr
self._auid = None
self._blocklist = set()
self._contacts = set()
self._wlock = False
self._firstCommand = True
self._wbuf = b""
self._wlockbuf = b""
self._rbuf = b""
self._pingTask = None
self._connect()
####
# Connections
####
def _connect(self):
self._wbuf = b"b"
self._sock = socket.socket()
self._sock.connect((self._mgr._PMHost, self._mgr._PMPort))
self._sock.setblocking(False)
self._firstCommand = True
if not self._auth(): return
self._pingTask = self.mgr.setInterval(self._mgr._pingDelay, self.ping)
self._connected = True
def _auth(self):
self._auid = _getAuth(self._mgr.name, self._mgr.password)
if self._auid == None:
self._sock.close()
self._callEvent("onLoginFail")
self._sock = None
return False
self._sendCommand("tlogin", self._auid, "2")
self._setWriteLock(True)
return True
def disconnect(self):
self._disconnect()
self._callEvent("onPMDisconnect")
def _disconnect(self):
self._connected = False
self._sock.close()
self._sock = None
####
# Feed
####
def _feed(self, data):
"""
Feed data to the connection.
@type data: bytes
@param data: data to be fed
"""
self._rbuf += data
while self._rbuf.find(b"\x00") != -1:
data = self._rbuf.split(b"\x00")
for food in data[:-1]:
self._process(food.decode().rstrip("\r\n")) #numnumz ;3
self._rbuf = data[-1]
def _process(self, data):
"""
Process a command string.
@type data: str
@param data: the command string
"""
self._callEvent("onRaw", data)
data = data.split(":")
cmd, args = data[0], data[1:]
func = "rcmd_" + cmd
if hasattr(self, func):
getattr(self, func)(args)
####
# Properties
####
def getManager(self): return self._mgr
def getContacts(self): return self._contacts
def getBlocklist(self): return self._blocklist
mgr = property(getManager)
contacts = property(getContacts)
blocklist = property(getBlocklist)
####
# Received Commands
####
def rcmd_OK(self, args):
self._setWriteLock(False)
self._sendCommand("wl")
self._sendCommand("getblock")
self._callEvent("onPMConnect")
def rcmd_wl(self, args):
self._contacts = set()
for i in range(len(args) // 4):
name, last_on, is_on, idle = args[i * 4: i * 4 + 4]
user = User(name)
self._contacts.add(user)
self._callEvent("onPMContactlistReceive")
def rcmd_block_list(self, args):
self._blocklist = set()
for name in args:
if name == "": continue
self._blocklist.add(User(name))
def rcmd_DENIED(self, args):
self._disconnect()
self._callEvent("onLoginFail")
def rcmd_msg(self, args):
user = User(args[0])
body = strip_html(":".join(args[5:]))
self._callEvent("onPMMessage", user, body)
def rcmd_msgoff(self, args):
user = User(args[0])
body = strip_html(":".join(args[5:]))
self._callEvent("onPMOfflineMessage", user, body)
def rcmd_wlonline(self, args):
self._callEvent("onPMContactOnline", User(args[0]))
def rcmd_wloffline(self, args):
self._callEvent("onPMContactOffline", User(args[0]))
def rcmd_kickingoff(self, args):
self.disconnect()
####
# Commands
####
def ping(self):
self._sendCommand("")
self._callEvent("onPMPing")
def message(self, user, msg):
if msg==None:
self._sendCommand("msg", user.name, msg)
def addContact(self, user):
if user not in self._contacts:
self._sendCommand("wladd", user.name)
self._contacts.add(user)
self._callEvent("onPMContactAdd", user)
def removeContact(self, user):
if user in self._contacts:
self._sendCommand("wldelete", user.name)
self._contacts.remove(user)
self._callEvent("onPMContactRemove", user)
def block(self, user):
if user not in self._blocklist:
self._sendCommand("block", user.name)
self._block.remove(user)
self._callEvent("onPMBlock", user)
def unblock(self, user):
if user in self._blocklist:
self._sendCommand("unblock", user.name)
self._block.remove(user)
self._callEvent("onPMUnblock", user)
####
# Util
####
def _callEvent(self, evt, *args, **kw):
getattr(self.mgr, evt)(self, *args, **kw)
self.mgr.onEventCalled(self, evt, *args, **kw)
def _write(self, data):
if self._wlock:
self._wlockbuf += data
else:
self.mgr._write(self, data)
def _setWriteLock(self, lock):
self._wlock = lock
if self._wlock == False:
self._write(self._wlockbuf)
self._wlockbuf = b""
def _sendCommand(self, *args):
"""
Send a command.
@type args: [str, str, ...]
@param args: command and list of arguments
"""
if self._firstCommand:
terminator = b"\x00"
self._firstCommand = False
else:
terminator = b"\r\n\x00"
self._write(":".join(args).encode() + terminator)
################################################################
# Room class
################################################################
class Room:
"""Manages a connection with a Chatango room."""
####
# Init
####
def __init__(self, room, uid = None, server = None, port = None, mgr = None):
# Basic stuff
self._name = room
self._server = server or getServer(room)
self._port = port or 443
self._mgr = mgr
# Under the hood
self._connected = False
self._reconnecting = False
self._uid = uid or genUid()
self._rbuf = b""
self._wbuf = b""
self._wlockbuf = b""
self._owner = None
self._mods = set()
self._mqueue = dict()
self._history = list()
self._userlist = list()
self._firstCommand = True
self._connectAmmount = 0
self._premium = False
self._userCount = 0
self._pingTask = None
self._users = dict()
self._msgs = dict()
self._wlock = False
self._silent = False
self._banlist = list()
# Inited vars
if self._mgr: self._connect()
####
# User and Message management
####
def getMessage(self, mid):
return self._msgs.get(mid)
def createMessage(self, msgid, **kw):
if msgid not in self._msgs:
msg = Message(msgid = msgid, **kw)
self._msgs[msgid] = msg
else:
msg = self._msgs[msgid]
return msg
####
# Connect/disconnect
####
def _connect(self):
"""Connect to the server."""
self._sock = socket.socket()
self._sock.connect((self._server, self._port))
self._sock.setblocking(False)
self._firstCommand = True
self._wbuf = b""
self._auth()
self._pingTask = self.mgr.setInterval(self.mgr._pingDelay, self.ping)
if not self._reconnecting: self.connected = True
def reconnect(self):
"""Reconnect."""
self._reconnect()
def _reconnect(self):
"""Reconnect."""
self._reconnecting = True
if self.connected:
self._disconnect()
self._uid = genUid()
self._connect()
self._reconnecting = False
def disconnect(self):
"""Disconnect."""
self._disconnect()
self._callEvent("onDisconnect")
def _disconnect(self):
"""Disconnect from the server."""
if not self._reconnecting: self.connected = False
for user in self._userlist:
user.clearSessionIds(self)
self._userlist = list()
self._pingTask.cancel()
self._sock.close()
if not self._reconnecting: del self.mgr._rooms[self.name]
def _auth(self):
"""Authenticate."""
self._sendCommand("bauth", self.name, self._uid, self.mgr.name, self.mgr.password)
self._setWriteLock(True)
####
# Properties
####
def getName(self): return self._name
def getManager(self): return self._mgr
def getUserlist(self, mode = None, unique = None, memory = None):
ul = None
if mode == None: mode = self.mgr._userlistMode
if unique == None: unique = self.mgr._userlistUnique
if memory == None: memory = self.mgr._userlistMemory
if mode == Userlist_Recent:
ul = map(lambda x: x.user, self._history[-memory:])
elif mode == Userlist_All:
ul = self._userlist
if unique:
return list(set(ul))
else:
return ul
def getUserNames(self):
ul = self.userlist
return list(map(lambda x: x.name, ul))
def getUser(self): return self.mgr.user
def getOwner(self): return self._owner
def getOwnerName(self): return self._owner.name
def getMods(self):
newset = set()
for mod in self._mods:
newset.add(mod)
return newset
def getModNames(self):
mods = self.getMods()
return [x.name for x in mods]
def getUserCount(self): return self._userCount
def getSilent(self): return self._silent
def setSilent(self, val): self._silent = val
def getBanlist(self): return [record[2] for record in self._banlist]
name = property(getName)
mgr = property(getManager)
userlist = property(getUserlist)
usernames = property(getUserNames)
user = property(getUser)
owner = property(getOwner)
ownername = property(getOwnerName)
mods = property(getMods)
modnames = property(getModNames)
usercount = property(getUserCount)
silent = property(getSilent, setSilent)
banlist = property(getBanlist)
####
# Feed/process
####
def _feed(self, data):
"""
Feed data to the connection.
@type data: bytes
@param data: data to be fed
"""
self._rbuf += data
while self._rbuf.find(b"\x00") != -1:
data = self._rbuf.split(b"\x00")
for food in data[:-1]:
self._process(food.decode('utf-8').rstrip("\r\n")) #numnumz ;3
self._rbuf = data[-1]
def _process(self, data):
"""
Process a command string.
@type data: str
@param data: the command string
"""
self._callEvent("onRaw", data)
data = data.split(":")
cmd, args = data[0], data[1:]
func = "rcmd_" + cmd
if hasattr(self, func):
getattr(self, func)(args)
####
# Received Commands
####
def rcmd_ok(self, args):
if args[2] != "M": #unsuccesful login
self._callEvent("onLoginFail")
self.disconnect()
self._owner = User(args[0])
self._uid = args[1]
self._aid = args[1][4:8]
self._mods = set(map(lambda x: User(x), args[6].split(";")))
self._i_log = list()
def rcmd_denied(self, args):
self._disconnect()
self._callEvent("onConnectFail")
def rcmd_inited(self, args):
self._sendCommand("g_participants", "start")
self._sendCommand("getpremium", "1")
self.requestBanlist()
if self._connectAmmount == 0:
self._callEvent("onConnect")
for msg in reversed(self._i_log):
user = msg.user
self._callEvent("onHistoryMessage", user, msg)
self._addHistory(msg)
del self._i_log
else:
self._callEvent("onReconnect")
self._connectAmmount += 1
self._setWriteLock(False)
def rcmd_premium(self, args):
if float(args[1]) > time.time():
self._premium = True
if self.user._mbg: self.setBgMode(1)
if self.user._mrec: self.setRecordingMode(1)
else:
self._premium = False
def rcmd_mods(self, args):
modnames = args
mods = set(map(lambda x: User(x), modnames))
premods = self._mods
for user in mods - premods: #modded
self._mods.add(user)
self._callEvent("onModAdd", user)
for user in premods - mods: #demodded
self._mods.remove(user)
self._callEvent("onModRemove", user)
self._callEvent("onModChange")
def rcmd_b(self, args):
mtime = float(args[0])
puid = args[3]
ip = args[6]
name = args[1]
rawmsg = ":".join(args[8:])
msg, n, f = clean_message(rawmsg)
if name == "":
nameColor = None
name = "#" + args[2]
if name == "#":
name = "!anon" + getAnonId(n, puid)
else:
if n: nameColor = parseNameColor(n)
else: nameColor = None
i = args[5]
unid = args[4]
#Create an anonymous message and queue it because msgid is unknown.
if f: fontColor, fontFace, fontSize = parseFont(f)
else: fontColor, fontFace, fontSize = None, None, None
msg = Message(
time = mtime,
user = User(name),
body = msg[1:],
raw = rawmsg[1:],
ip = ip,
nameColor = nameColor,
fontColor = fontColor,
fontFace = fontFace,
fontSize = fontSize,
unid = unid,
room = self
)
self._mqueue[i] = msg
def rcmd_u(self, args):
msg = self._mqueue[args[0]]
if msg.user != self.user:
msg.user._fontColor = msg.fontColor
msg.user._fontFace = msg.fontFace
msg.user._fontSize = msg.fontSize
msg.user._nameColor = msg.nameColor
del self._mqueue[args[0]]
msg.attach(self, args[1])
self._addHistory(msg)
self._callEvent("onMessage", msg.user, msg)
def rcmd_i(self, args):
mtime = float(args[0])
puid = args[3]
ip = args[6]
if ip == "": ip = None
name = args[1]
rawmsg = ":".join(args[8:])
msg, n, f = clean_message(rawmsg)
msgid = args[5]
if name == "":
nameColor = None
name = "#" + args[2]
if name == "#":
name = "!anon" + getAnonId(n, puid)
else:
if n: nameColor = parseNameColor(n)
else: nameColor = None
if f: fontColor, fontFace, fontSize = parseFont(f)
else: fontColor, fontFace, fontSize = None, None, None
msg = self.createMessage(
msgid = msgid,
time = mtime,
user = User(name),
body = msg[1:],
raw = rawmsg,
ip = args[6],
unid = args[4],
nameColor = nameColor,
fontColor = fontColor,
fontFace = fontFace,
fontSize = fontSize,
room = self
)
if msg.user != self.user:
msg.user._fontColor = msg.fontColor
msg.user._fontFace = msg.fontFace
msg.user._fontSize = msg.fontSize
msg.user._nameColor = msg.nameColor
self._i_log.append(msg)
def rcmd_g_participants(self, args):
args = ":".join(args)
args = args.split(";")
for data in args:
data = data.split(":")
name = data[3].lower()
if name == "none": continue
user = User(
name = name,
room = self
)
user.addSessionId(self, data[0])
self._userlist.append(user)
def rcmd_participant(self, args):
if args[0] == "0": #leave
name = args[3].lower()
if name == "none": return
user = User(name)
user.removeSessionId(self, args[1])
self._userlist.remove(user)
if user not in self._userlist or not self.mgr._userlistEventUnique:
self._callEvent("onLeave", user)
else: #join
name = args[3].lower()
if name == "none": return
user = User(
name = name,
room = self
)
user.addSessionId(self, args[1])
if user not in self._userlist: doEvent = True
else: doEvent = False
self._userlist.append(user)
if doEvent or not self.mgr._userlistEventUnique:
self._callEvent("onJoin", user)
def rcmd_show_fw(self, args):
self._callEvent("onFloodWarning")
def rcmd_show_tb(self, args):
self._callEvent("onFloodBan")
def rcmd_tb(self, args):
self._callEvent("onFloodBanRepeat")
def rcmd_delete(self, args):
msg = self.getMessage(args[0])
if msg:
if msg in self._history:
self._history.remove(msg)
self._callEvent("onMessageDelete", msg.user, msg)
msg.detach()
def rcmd_deleteall(self, args):
for msgid in args:
self.rcmd_delete([msgid])
def rcmd_n(self, args):
self._userCount = int(args[0], 16)
self._callEvent("onUserCountChange")
def rcmd_blocklist(self, args):
self._banlist = list()
sections = ":".join(args).split(";")
for section in sections:
params = section.split(":")
if len(params) != 5: continue
if params[2] == "": continue
self._banlist.append((
params[0], #unid
params[1], #ip
User(params[2]), #target
float(params[3]), #time
User(params[4]) #src
))
self._callEvent("onBanlistUpdate")
def rcmd_blocked(self, args):
if args[2] == "": return
target = User(args[2])
user = User(args[3])
self._banlist.append((args[0], args[1], target, float(args[4]), user))
self._callEvent("onBan", user, target)
self.requestBanlist()
def rcmd_unblocked(self, args):
if args[2] == "": return
target = User(args[2])
user = User(args[3])
self._callEvent("onUnban", user, target)
self.requestBanlist()
####
# Commands
####
def ping(self):
"""Send a ping."""
self._sendCommand("")
self._callEvent("onPing")
def rawMessage(self, msg):
"""
Send a message without n and f tags.
@type msg: str
@param msg: message
"""
if not self._silent:
self._sendCommand("bmsg:tl2r", msg)
def message(self, msg, html = False):
"""
Send a message.
@type msg: str
@param msg: message
"""
if msg==None:
return
if not html:
msg = msg.replace("<", "&lt;").replace(">", "&gt;")
if len(msg) > self.mgr._maxLength:
if self.mgr._tooBigMessage == BigMessage_Cut:
self.message(msg[:self.mgr._maxLength], html = html)
elif self.mgr._tooBigMessage == BigMessage_Multiple:
while len(msg) > 0:
sect = msg[:self.mgr._maxLength]
msg = msg[self.mgr._maxLength:]
self.message(sect, html = html)
return
msg = "<n" + self.user.nameColor + "/>" + msg
msg = "<f x%0.2i%s=\"%s\">" %(self.user.fontSize, self.user.fontColor, self.user.fontFace) + msg
self.rawMessage(msg)
def setBgMode(self, mode):
self._sendCommand("msgbg", str(mode))
def setRecordingMode(self, mode):
self._sendCommand("msgmedia", str(mode))
def addMod(self, user):
"""
Add a moderator.
@type user: User
@param user: User to mod.
"""
if self.getLevel(self.user) == 2:
self._sendCommand("addmod", user.name)
def removeMod(self, user):
"""
Remove a moderator.
@type user: User
@param user: User to demod.
"""
if self.getLevel(self.user) == 2:
self._sendCommand("removemod", user.name)
def flag(self, message):
"""
Flag a message.
@type message: Message
@param message: message to flag
"""
self._sendCommand("g_flag", message.msgid)
def flagUser(self, user):
"""
Flag a user.
@type user: User
@param user: user to flag
@rtype: bool
@return: whether a message to flag was found
"""
msg = self.getLastMessage(user)
if msg:
self.flag(msg)
return True
return False
def delete(self, message):
"""
Delete a message. (Moderator only)
@type message: Message
@param message: message to delete
"""
if self.getLevel(self.user) > 0:
self._sendCommand("delmsg", message.msgid)
def rawClearUser(self, unid):
self._sendCommand("delallmsg", unid)
def clearUser(self, user):
"""
Clear all of a user's messages. (Moderator only)
@type user: User
@param user: user to delete messages of
@rtype: bool
@return: whether a message to delete was found
"""
if self.getLevel(self.user) > 0:
msg = self.getLastMessage(user)
if msg:
self.rawClearUser(msg.unid)
return True
return False
def clearall(self):
"""Clear all messages. (Owner only)"""
if self.getLevel(self.user) == 2:
self._sendCommand("clearall")
def rawBan(self, name, ip, unid):
"""
Execute the block command using specified arguments.
(For advanced usage)
@type name: str
@param name: name
@type ip: str
@param ip: ip address
@type unid: str
@param unid: unid
"""
self._sendCommand("block", unid, ip, name)
def ban(self, msg):
"""
Ban a message's sender. (Moderator only)
@type message: Message
@param message: message to ban sender of
"""
if self.getLevel(self.user) > 0:
self.rawBan(msg.user.name, msg.ip, msg.unid)
def banUser(self, user):
"""
Ban a user. (Moderator only)
@type user: User
@param user: user to ban
@rtype: bool
@return: whether a message to ban the user was found
"""
msg = self.getLastMessage(user)
if msg:
self.ban(msg)
return True
return False
def requestBanlist(self):
"""Request an updated banlist."""
self._sendCommand("blocklist", "block", "", "next", "500")
def rawUnban(self, name, ip, unid):
"""
Execute the unblock command using specified arguments.
(For advanced usage)
@type name: str
@param name: name
@type ip: str
@param ip: ip address
@type unid: str
@param unid: unid
"""
self._sendCommand("removeblock", unid, ip, name)
def unban(self, user):
"""
Unban a user. (Moderator only)
@type user: User
@param user: user to unban
@rtype: bool
@return: whether it succeeded
"""
rec = self._getBanRecord(user)
if rec:
self.rawUnban(rec[2].name, rec[1], rec[0])
return True
else:
return False
####
# Util
####
def _getBanRecord(self, user):
for record in self._banlist:
if record[2] == user:
return record
return None
def _callEvent(self, evt, *args, **kw):
getattr(self.mgr, evt)(self, *args, **kw)
self.mgr.onEventCalled(self, evt, *args, **kw)
def _write(self, data):
if self._wlock:
self._wlockbuf += data
else:
self.mgr._write(self, data)
def _setWriteLock(self, lock):
self._wlock = lock
if self._wlock == False:
self._write(self._wlockbuf)
self._wlockbuf = b""
def _sendCommand(self, *args):
"""
Send a command.
@type args: [str, str, ...]
@param args: command and list of arguments
"""
if self._firstCommand:
terminator = b"\x00"
self._firstCommand = False
else:
terminator = b"\r\n\x00"
self._write(":".join(args).encode('utf-8') + terminator)
def getLevel(self, user):
if user == self._owner: return 2
if user in self._mods: return 1
return 0
def getLastMessage(self, user = None):
if user:
try:
i = 1
while True:
msg = self._history[-i]
if msg.user == user:
return msg
i += 1
except IndexError:
return None
else:
try:
return self._history[-1]
except IndexError:
return None
return None
def findUser(self, name):
name = name.lower()
ul = self.getUserlist()
udi = dict(zip([u.name for u in ul], ul))
cname = None
for n in udi.keys():
if n.find(name) != -1:
if cname: return None #ambigious!!
cname = n
if cname: return udi[cname]
else: return None
####
# History
####
def _addHistory(self, msg):
"""
Add a message to history.
@type msg: Message
@param msg: message
"""
self._history.append(msg)
if len(self._history) > self.mgr._maxHistoryLength:
rest, self._history = self._history[:-self.mgr._maxHistoryLength], self._history[-self.mgr._maxHistoryLength:]
for msg in rest: msg.detach()
################################################################
# RoomManager class
################################################################
class RoomManager:
"""Class that manages multiple connections."""
####
# Config
####
_Room = Room
_PM = PM
_PMHost = "c1.chatango.com"
_PMPort = 5222
_TimerResolution = 0.2 #at least x times per second
_pingDelay = 20
_userlistMode = Userlist_Recent
_userlistUnique = True
_userlistMemory = 50
_userlistEventUnique = False
_tooBigMessage = BigMessage_Multiple
_maxLength = 1800
_maxHistoryLength = 150
####
# Init
####
def __init__(self, name = None, password = None, pm = True):
self._name = name
self._password = password
self._running = False
self._tasks = set()
self._rooms = dict()
if pm:
self._pm = self._PM(mgr = self)
else:
self._pm = None
####
# Join/leave
####
def joinRoom(self, room):
"""
Join a room or return None if already joined.
@type room: str
@param room: room to join
@rtype: Room or None
@return: the room or nothing
"""
room = room.lower()
if room not in self._rooms:
con = self._Room(room, mgr = self)
self._rooms[room] = con
return con
else:
return None
def leaveRoom(self, room):
"""
Leave a room.
@type room: str
@param room: room to leave
"""
room = room.lower()
if room in self._rooms:
con = self._rooms[room]
con.disconnect()
def getRoom(self, room):
"""
Get room with a name, or None if not connected to this room.
@type room: str
@param room: room
@rtype: Room
@return: the room
"""
room = room.lower()
if room in self._rooms:
return self._rooms[room]
else:
return None
####
# Properties
####
def getUser(self): return User(self._name)
def getName(self): return self._name
def getPassword(self): return self._password
def getRooms(self): return set(self._rooms.values())
def getRoomNames(self): return set(self._rooms.keys())
def getPM(self): return self._pm
user = property(getUser)
name = property(getName)
password = property(getPassword)
rooms = property(getRooms)
roomnames = property(getRoomNames)
pm = property(getPM)
####
# Virtual methods
####
def onInit(self):
"""Called on init."""
pass
def onConnect(self, room):
"""
Called when connected to the room.
@type room: Room
@param room: room where the event occured
"""
pass
def onReconnect(self, room):
"""
Called when reconnected to the room.
@type room: Room
@param room: room where the event occured
"""
pass
def onConnectFail(self, room):
"""
Called when the connection failed.
@type room: Room
@param room: room where the event occured
"""
pass
def onDisconnect(self, room):
"""
Called when the client gets disconnected.
@type room: Room
@param room: room where the event occured
"""
pass
def onLoginFail(self, room):
"""
Called on login failure, disconnects after.
@type room: Room
@param room: room where the event occured
"""
pass
def onFloodBan(self, room):
"""
Called when either flood banned or flagged.
@type room: Room
@param room: room where the event occured
"""
pass
def onFloodBanRepeat(self, room):
"""
Called when trying to send something when floodbanned.
@type room: Room
@param room: room where the event occured
"""
pass
def onFloodWarning(self, room):
"""
Called when an overflow warning gets received.
@type room: Room
@param room: room where the event occured
"""
pass
def onMessageDelete(self, room, user, message):
"""
Called when a message gets deleted.
@type room: Room
@param room: room where the event occured
@type user: User
@param user: owner of deleted message
@type message: Message
@param message: message that got deleted
"""
pass
def onModChange(self, room):
"""
Called when the moderator list changes.
@type room: Room
@param room: room where the event occured
"""
pass
def onModAdd(self, room, user):
"""
Called when a moderator gets added.
@type room: Room
@param room: room where the event occured
"""
pass
def onModRemove(self, room, user):
"""
Called when a moderator gets removed.
@type room: Room
@param room: room where the event occured
"""
pass
def onMessage(self, room, user, message):
"""
Called when a message gets received.
@type room: Room
@param room: room where the event occured
@type user: User
@param user: owner of message
@type message: Message
@param message: received message
"""
pass
def onHistoryMessage(self, room, user, message):
"""
Called when a message gets received from history.
@type room: Room
@param room: room where the event occured
@type user: User
@param user: owner of message
@type message: Message
@param message: the message that got added
"""
pass
def onJoin(self, room, user):
"""
Called when a user joins. Anonymous users get ignored here.
@type room: Room
@param room: room where the event occured
@type user: User
@param user: the user that has joined
"""
pass
def onLeave(self, room, user):
"""
Called when a user leaves. Anonymous users get ignored here.
@type room: Room
@param room: room where the event occured
@type user: User
@param user: the user that has left
"""
pass
def onRaw(self, room, raw):
"""
Called before any command parsing occurs.
@type room: Room
@param room: room where the event occured
@type raw: str
@param raw: raw command data
"""
pass
def onPing(self, room):
"""
Called when a ping gets sent.
@type room: Room
@param room: room where the event occured
"""
pass
def onUserCountChange(self, room):
"""
Called when the user count changes.
@type room: Room
@param room: room where the event occured
"""
pass
def onBan(self, room, user, target):
"""
Called when a user gets banned.
@type room: Room
@param room: room where the event occured
@type user: User
@param user: user that banned someone
@type target: User
@param target: user that got banned
"""
pass
def onUnban(self, room, user, target):
"""
Called when a user gets unbanned.
@type room: Room
@param room: room where the event occured
@type user: User
@param user: user that unbanned someone
@type target: User
@param target: user that got unbanned
"""
pass
def onBanlistUpdate(self, room):
"""
Called when a banlist gets updated.
@type room: Room
@param room: room where the event occured
"""
pass
def onPMConnect(self, pm):
pass
def onPMDisconnect(self, pm):
pass
def onPMPing(self, pm):
pass
def onPMMessage(self, pm, user, body):
pass
def onPMOfflineMessage(self, pm, user, body):
pass
def onPMContactlistReceive(self, pm):
pass
def onPMBlocklistReceive(self, pm):
pass
def onPMContactAdd(self, pm, user):
pass
def onPMContactRemove(self, pm, user):
pass
def onPMBlock(self, pm, user):
pass
def onPMUnblock(self, pm, user):
pass
def onPMContactOnline(self, pm, user):
pass
def onPMContactOffline(self, pm, user):
pass
def onEventCalled(self, room, evt, *args, **kw):
"""
Called on every room-based event.
@type room: Room
@param room: room where the event occured
@type evt: str
@param evt: the event
"""
pass
####
# Deferring
####
def deferToThread(self, callback, func, *args, **kw):
"""
Defer a function to a thread and callback the return value.
@type callback: function
@param callback: function to call on completion
@type cbargs: tuple or list
@param cbargs: arguments to get supplied to the callback
@type func: function
@param func: function to call
"""
def f(func, callback, *args, **kw):
ret = func(*args, **kw)
self.setTimeout(0, callback, ret)
threading._start_new_thread(f, (func, callback) + args, kw)
####
# Scheduling
####
class _Task:
def cancel(self):
"""Sugar for removeTask."""
self.mgr.removeTask(self)
def _tick(self):
now = time.time()
for task in set(self._tasks):
if task.target <= now:
task.func(*task.args, **task.kw)
if task.isInterval:
task.target = now + task.timeout
else:
self._tasks.remove(task)
def setTimeout(self, timeout, func, *args, **kw):
"""
Call a function after at least timeout seconds with specified arguments.
@type timeout: int
@param timeout: timeout
@type func: function
@param func: function to call
@rtype: _Task
@return: object representing the task
"""
task = self._Task()
task.mgr = self
task.target = time.time() + timeout
task.timeout = timeout
task.func = func
task.isInterval = False
task.args = args
task.kw = kw
self._tasks.add(task)
return task
def setInterval(self, timeout, func, *args, **kw):
"""
Call a function at least every timeout seconds with specified arguments.
@type timeout: int
@param timeout: timeout
@type func: function
@param func: function to call
@rtype: _Task
@return: object representing the task
"""
task = self._Task()
task.mgr = self
task.target = time.time() + timeout
task.timeout = timeout
task.func = func
task.isInterval = True
task.args = args
task.kw = kw
self._tasks.add(task)
return task
def removeTask(self, task):
"""
Cancel a task.
@type task: _Task
@param task: task to cancel
"""
self._tasks.remove(task)
####
# Util
####
def _write(self, room, data):
room._wbuf += data
def getConnections(self):
li = list(self._rooms.values())
if self._pm:
li.append(self._pm)
return [c for c in li if c._sock != None]
####
# Main
####
def main(self):
self.onInit()
self._running = True
while self._running:
conns = self.getConnections()
socks = [x._sock for x in conns]
wsocks = [x._sock for x in conns if x._wbuf != b""]
rd, wr, sp = select.select(socks, wsocks, [], self._TimerResolution)
for sock in rd:
con = [c for c in conns if c._sock == sock][0]
try:
data = sock.recv(1024)
if(len(data) > 0):
con._feed(data)
else:
con.disconnect()
except socket.error:
pass
for sock in wr:
con = [c for c in conns if c._sock == sock][0]
try:
size = sock.send(con._wbuf)
con._wbuf = con._wbuf[size:]
except socket.error:
pass
self._tick()
@classmethod
def easy_start(cl, rooms = None, name = None, password = None, pm = True):
"""
Prompts the user for missing info, then starts.
@type rooms: list
@param room: rooms to join
@type name: str
@param name: name to join as ("" = None, None = unspecified)
@type password: str
@param password: password to join with ("" = None, None = unspecified)
"""
if not rooms: rooms = str(input("Room names separated by semicolons: ")).split(";")
if len(rooms) == 1 and rooms[0] == "": rooms = []
if not name: name = str(input("User name: "))
if name == "": name = None
if not password: password = str(input("User password: "))
if password == "": password = None
self = cl(name, password, pm = pm)
for room in rooms:
self.joinRoom(room)
self.main()
def stop(self):
for conn in list(self._rooms.values()):
conn.disconnect()
self._running = False
####
# Commands
####
def enableBg(self):
"""Enable background if available."""
self.user._mbg = True
for room in self.rooms:
room.setBgMode(1)
def disableBg(self):
"""Disable background."""
self.user._mbg = False
for room in self.rooms:
room.setBgMode(0)
def enableRecording(self):
"""Enable recording if available."""
self.user._mrec = True
for room in self.rooms:
room.setRecordingMode(1)
def disableRecording(self):
"""Disable recording."""
self.user._mrec = False
for room in self.rooms:
room.setRecordingMode(0)
def setNameColor(self, color3x):
"""
Set name color.
@type color3x: str
@param color3x: a 3-char RGB hex code for the color
"""
self.user._nameColor = color3x
def setFontColor(self, color3x):
"""
Set font color.
@type color3x: str
@param color3x: a 3-char RGB hex code for the color
"""
self.user._fontColor = color3x
def setFontFace(self, face):
"""
Set font face/family.
@type face: str
@param face: the font face
"""
self.user._fontFace = face
def setFontSize(self, size):
"""
Set font size.
@type size: int
@param size: the font size (limited: 9 to 22)
"""
if size < 9: size = 9
if size > 22: size = 22
self.user._fontSize = size
################################################################
# User class (well, yeah, i lied, it's actually _User)
################################################################
_users = dict()
def User(name, *args, **kw):
name = name.lower()
user = _users.get(name)
if not user:
user = _User(name = name, *args, **kw)
_users[name] = user
return user
class _User:
"""Class that represents a user."""
####
# Init
####
def __init__(self, name, **kw):
self._name = name.lower()
self._sids = dict()
self._msgs = list()
self._nameColor = "000"
self._fontSize = 12
self._fontFace = "0"
self._fontColor = "000"
self._mbg = False
self._mrec = False
for attr, val in kw.items():
if val == None: continue
setattr(self, "_" + attr, val)
####
# Properties
####
def getName(self): return self._name
def getSessionIds(self, room = None):
if room:
return self._sids.get(room, set())
else:
return set.union(*self._sids.values())
def getRooms(self): return self._sids.keys()
def getRoomNames(self): return [room.name for room in self.getRooms()]
def getFontColor(self): return self._fontColor
def getFontFace(self): return self._fontFace
def getFontSize(self): return self._fontSize
def getNameColor(self): return self._nameColor
name = property(getName)
sessionids = property(getSessionIds)
rooms = property(getRooms)
roomnames = property(getRoomNames)
fontColor = property(getFontColor)
fontFace = property(getFontFace)
fontSize = property(getFontSize)
nameColor = property(getNameColor)
####
# Util
####
def addSessionId(self, room, sid):
if room not in self._sids:
self._sids[room] = set()
self._sids[room].add(sid)
def removeSessionId(self, room, sid):
try:
self._sids[room].remove(sid)
if len(self._sids[room]) == 0:
del self._sids[room]
except KeyError:
pass
def clearSessionIds(self, room):
try:
del self._sids[room]
except KeyError:
pass
def hasSessionId(self, room, sid):
try:
if sid in self._sids[room]:
return True
else:
return False
except KeyError:
return False
####
# Repr
####
def __repr__(self):
return "<User: %s>" %(self.name)
################################################################
# Message class
################################################################
class Message:
"""Class that represents a message."""
####
# Attach/detach
####
def attach(self, room, msgid):
"""
Attach the Message to a message id.
@type msgid: str
@param msgid: message id
"""
if self._msgid == None:
self._room = room
self._msgid = msgid
self._room._msgs[msgid] = self
def detach(self):
"""Detach the Message."""
if self._msgid != None and self._msgid in self._room._msgs:
del self._room._msgs[self._msgid]
self._msgid = None
####
# Init
####
def __init__(self, **kw):
self._msgid = None
self._time = None
self._user = None
self._body = None
self._room = None
self._raw = ""
self._ip = None
self._unid = ""
self._nameColor = "000"
self._fontSize = 12
self._fontFace = "0"
self._fontColor = "000"
for attr, val in kw.items():
if val == None:
continue
setattr(self, "_" + attr, val)
####
# Properties
####
def getId(self): return self._msgid
def getTime(self): return self._time
def getUser(self): return self._user
def getBody(self): return self._body
def getIP(self): return self._ip
def getFontColor(self): return self._fontColor
def getFontFace(self): return self._fontFace
def getFontSize(self): return self._fontSize
def getNameColor(self): return self._nameColor
def getRoom(self): return self._room
def getRaw(self): return self._raw
def getUnid(self): return self._unid
msgid = property(getId)
time = property(getTime)
user = property(getUser)
body = property(getBody)
room = property(getRoom)
ip = property(getIP)
fontColor = property(getFontColor)
fontFace = property(getFontFace)
fontSize = property(getFontSize)
raw = property(getRaw)
nameColor = property(getNameColor)
unid = property(getUnid)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment