mirror of git://git.psyced.org/git/pypsyc
350 lines
11 KiB
Python
350 lines
11 KiB
Python
"""
|
|
pypsyc.client.model
|
|
~~~~~~~~~~~~~~~~~~~
|
|
|
|
:copyright: 2010 by Manuel Jacob
|
|
:license: MIT
|
|
"""
|
|
from collections import namedtuple, defaultdict
|
|
from datetime import datetime
|
|
from json import dump, load
|
|
|
|
from twisted.internet import reactor
|
|
from twisted.internet.protocol import ClientFactory
|
|
|
|
from pypsyc.core.mmp import Circuit, Uni
|
|
from pypsyc.core.psyc import PSYCObject
|
|
from pypsyc.client.observable import ObsObj, ObsAttr, ObsList, ObsDict
|
|
from pypsyc.protocol import (UnknownTargetError, DeliveryFailedError,
|
|
AuthenticationError, LinkingClient as LinkingProtocol,
|
|
RoutingErrorClient as RoutingErrorProtocol,
|
|
Messaging as MessagingProtocol,
|
|
ConferencingClient as ConferencingProtocol,
|
|
FriendshipClient as FriendshipProtocol,
|
|
ClientInterfaceClient as ClientInterfaceProtocol)
|
|
from pypsyc.util import (schedule, Event, resolve_hostname, connect,
|
|
key_intersection)
|
|
|
|
|
|
class Message(object):
|
|
def __init__(self, source, message):
|
|
self.source = source
|
|
self.message = message
|
|
self.time = datetime.now()
|
|
|
|
|
|
class Conversation(object):
|
|
def __init__(self, messaging, uni):
|
|
self.messaging = messaging
|
|
self.uni = uni
|
|
self.messages = ObsList()
|
|
self.unknown_target_evt = Event()
|
|
self.delivery_failed_evt = Event()
|
|
|
|
def send_message(self, message):
|
|
self.messaging.protocol.send_private_message(
|
|
self.uni, message, relay=self.messaging.account.uni)
|
|
self.messages.append(Message(self.messaging.account.uni, message))
|
|
|
|
|
|
Member = namedtuple('Member', ('uni', 'nick'))
|
|
|
|
|
|
class Conference(object):
|
|
def __init__(self, conferencing, uni):
|
|
self.conferencing = conferencing
|
|
self.uni = uni
|
|
self.members = ObsList()
|
|
self.messages = ObsList()
|
|
self.unknown_target_evt = Event()
|
|
self.delivery_failed_evt = Event()
|
|
|
|
def send_message(self, message):
|
|
self.conferencing.protocol.send_public_message(self.uni, message)
|
|
|
|
|
|
class Messaging(object):
|
|
def __init__(self, account):
|
|
self.account = account
|
|
self.protocol = MessagingProtocol(self)
|
|
|
|
def private_message(self, source, message):
|
|
conversation = self.account.get_conversation(source)
|
|
conversation.messages.append(Message(source, message))
|
|
|
|
def connected(self, circuit):
|
|
self.psyc = circuit.psyc
|
|
self.psyc.add_handler(self.protocol)
|
|
|
|
def disconnected(self):
|
|
pass
|
|
|
|
|
|
class Conferencing(object):
|
|
def __init__(self, account):
|
|
self.account = account
|
|
self.protocol = ConferencingProtocol(self)
|
|
|
|
def member_entered(self, place, uni, nick):
|
|
conference = self.account.get_conference(place)
|
|
conference.members.append(Member(uni, nick))
|
|
|
|
def member_left(self, place, uni):
|
|
conference = self.account.get_conference(place)
|
|
for i in conference.members:
|
|
if i.uni == uni:
|
|
conference.members.remove(i)
|
|
|
|
def public_message(self, place, source, message):
|
|
conference = self.account.get_conference(place)
|
|
conference.messages.append(Message(source, message))
|
|
|
|
def connected(self, circuit):
|
|
self.psyc = circuit.psyc
|
|
self.psyc.add_handler(self.protocol)
|
|
circuit.add_pvar_handler(self.protocol)
|
|
|
|
def disconnected(self):
|
|
pass
|
|
|
|
|
|
Presence = namedtuple('Presence', ('availability',))
|
|
|
|
|
|
class Friend(object):
|
|
def __init__(self, account, uni):
|
|
self.account = account
|
|
self.uni = uni
|
|
self.presence = Presence(0)
|
|
|
|
|
|
class FriendList(object):
|
|
def __init__(self, account):
|
|
self.account = account
|
|
self.protocol = FriendshipProtocol(self)
|
|
|
|
def friendship(self, uni, state):
|
|
friend = self.account.friends.get(uni) or Friend(self.account, uni)
|
|
friend.state = state
|
|
if uni in self.account.friends:
|
|
self.account.friends.updated_item(uni)
|
|
else:
|
|
self.account.friends[uni] = friend
|
|
|
|
def friendship_removed(self, uni):
|
|
del self.account.friends[uni]
|
|
|
|
def presence(self, context, value):
|
|
self.account.friends[context].presence = Presence(value)
|
|
self.account.friends.updated_item(context)
|
|
|
|
def connected(self, circuit):
|
|
self.psyc = circuit.psyc
|
|
self.psyc.add_handler(self.protocol)
|
|
circuit.add_pvar_handler(self.protocol)
|
|
|
|
def disconnected(self):
|
|
self.account.friends.clear()
|
|
|
|
|
|
class Account(ObsObj):
|
|
_active = False
|
|
circuit = ObsAttr('circuit')
|
|
|
|
def __init__(self, client, server, person, password, save_password,
|
|
active):
|
|
ObsObj.__init__(self)
|
|
self.no_password_evt = Event()
|
|
self.connection_error_evt = Event()
|
|
self.no_such_user_evt = Event()
|
|
self.auth_error_evt = Event()
|
|
|
|
self.client = client
|
|
self.server = server
|
|
self.person = person
|
|
self.resource = "*Resource"
|
|
self.password = password
|
|
self.save_password = save_password
|
|
schedule(setattr, self, 'active', active)
|
|
|
|
self.conversations = ObsDict()
|
|
self.conferences = ObsDict()
|
|
self.conferences.delitem_evt += self._del_conference
|
|
self.friends = ObsDict()
|
|
self.messaging = Messaging(self)
|
|
self.conferencing = Conferencing(self)
|
|
self.friend_list = FriendList(self)
|
|
|
|
@property
|
|
def uni(self):
|
|
return Uni.from_parts([self.server, '~' + self.person])
|
|
|
|
def get_conversation(self, uni):
|
|
if not uni in self.conversations:
|
|
self.conversations[uni] = Conversation(self.messaging, uni)
|
|
return self.conversations[uni]
|
|
|
|
def get_conference(self, uni, subscribe=False):
|
|
if not uni in self.conferences:
|
|
self.conferences[uni] = Conference(self.conferencing, uni)
|
|
if subscribe:
|
|
schedule(self._subscribe, uni)
|
|
return self.conferences[uni]
|
|
|
|
def _subscribe(self, uni):
|
|
try:
|
|
self.client_interface.subscribe(uni)
|
|
except UnknownTargetError:
|
|
self.conferences[uni].unknown_target_evt()
|
|
except DeliveryFailedError as e:
|
|
self.conferences[uni].delivery_failed_evt(e.args[0])
|
|
|
|
def _del_conference(self, uni, old):
|
|
schedule(self.client_interface.unsubscribe, uni)
|
|
|
|
def add_friend(self, uni):
|
|
self.client_interface.add_friend(uni)
|
|
|
|
def remove_friend(self, uni):
|
|
self.client_interface.remove_friend(uni)
|
|
|
|
def unknown_target_error(self, uni):
|
|
conversation = self.conversations.get(uni) or self.conferences.get(uni)
|
|
if conversation:
|
|
conversation.unknown_target_evt()
|
|
|
|
def delivery_failed_error(self, uni, message):
|
|
conversation = self.conversations.get(uni) or self.conferences.get(uni)
|
|
if conversation:
|
|
conversation.delivery_failed_evt(message)
|
|
|
|
@property
|
|
def active(self):
|
|
return self._active
|
|
|
|
@active.setter
|
|
def active(self, active):
|
|
if active != self._active:
|
|
self._active = active
|
|
if active:
|
|
schedule(self._activate)
|
|
elif hasattr(self, 'circuit'):
|
|
self._deactivate()
|
|
|
|
def _activate(self):
|
|
if not self.save_password and not self.password:
|
|
self.active = False
|
|
self.no_password_evt()
|
|
return
|
|
try:
|
|
ip, port = resolve_hostname(self.server)
|
|
circuit = self.circuit = connect(ip, port, self.client)
|
|
except Exception as e:
|
|
return self._error(self.connection_error_evt, e)
|
|
try:
|
|
linking_protocol = LinkingProtocol(circuit)
|
|
linking_protocol.link(self.uni, self.resource, self.password)
|
|
self.psyc = circuit.psyc
|
|
self.psyc.uni = self.uni.chain(self.resource)
|
|
except UnknownTargetError as e:
|
|
return self._error(self.no_such_user_evt, e)
|
|
except AuthenticationError as e:
|
|
return self._error(self.auth_error_evt, e)
|
|
|
|
circuit.psyc.add_handler(RoutingErrorProtocol(self))
|
|
self.client_interface = ClientInterfaceProtocol(self)
|
|
self.messaging.connected(circuit)
|
|
self.conferencing.connected(circuit)
|
|
self.friend_list.connected(circuit)
|
|
|
|
def _deactivate(self):
|
|
self.circuit.transport.loseConnection()
|
|
del self.circuit
|
|
|
|
self.messaging.disconnected()
|
|
self.conferencing.disconnected()
|
|
self.friend_list.disconnected()
|
|
|
|
def _error(self, event, error):
|
|
self.active = False
|
|
self.client.accounts.updated_item(self)
|
|
event(error)
|
|
|
|
def connection_error(self, error):
|
|
self._error(self.connection_error_evt, error)
|
|
|
|
|
|
class ClientCircuit(Circuit):
|
|
def __init__(self):
|
|
self.dump_evt = Event()
|
|
Circuit.__init__(self)
|
|
self.psyc = PSYCObject(self.send)
|
|
self.pvar_handlers = defaultdict(dict)
|
|
|
|
def connectionMade(self):
|
|
if hasattr(self.transport, 'write'):
|
|
self.orig_write = self.transport.write
|
|
self.transport.write = self.dump_write
|
|
Circuit.connectionMade(self)
|
|
|
|
def dump_write(self, data):
|
|
for line in data.strip('\n').split('\n'):
|
|
self.dump_evt('o', line)
|
|
self.orig_write(data)
|
|
|
|
def lineReceived(self, line):
|
|
self.dump_evt('i', line)
|
|
return Circuit.lineReceived(self, line)
|
|
|
|
def packet_received(self, header, content):
|
|
if '_source_relay' in header:
|
|
assert header.source.is_ancestor_of(self.psyc.uni)
|
|
header.source = Uni(header['_source_relay'])
|
|
packet = self.psyc.handle_packet(header, content)
|
|
|
|
for modifier in key_intersection(self.pvar_handlers, packet.modifiers):
|
|
handlers = self.pvar_handlers[modifier]
|
|
vars = packet.modifiers[modifier]
|
|
for var in key_intersection(handlers, vars):
|
|
handlers[var](packet.header.context, vars)
|
|
|
|
def add_pvar_handler(self, handler):
|
|
for i in dir(handler):
|
|
if i.startswith('state_'):
|
|
_, operation, var = i.split('_', 2)
|
|
modifier = {'set': '=', 'add': '+', 'remove': '-'}[operation]
|
|
self.pvar_handlers[modifier]['_' + var] = getattr(handler, i)
|
|
|
|
|
|
class Client(ClientFactory, object):
|
|
protocol = ClientCircuit
|
|
|
|
def __init__(self, accounts_file=None):
|
|
self.accounts = ObsList()
|
|
self.accounts_file = accounts_file
|
|
|
|
def load_accounts(self):
|
|
with open(self.accounts_file, 'r') as f:
|
|
self.accounts.extend(Account(self, d['server'].encode('utf-8'),
|
|
d['person'].encode('utf-8'),
|
|
d['password'].encode('utf-8'),
|
|
d['save_password'], d['active'])
|
|
for d in load(f))
|
|
|
|
def save_accounts(self):
|
|
with open(self.accounts_file, 'w') as f:
|
|
dump(self.accounts, f, indent=4, default=lambda a:
|
|
{'server': a.server, 'person': a.person,
|
|
'password': a.password if a.save_password else '',
|
|
'save_password': a.save_password, 'active': a.active})
|
|
|
|
def connection_lost(self, circuit, error):
|
|
for account in self.accounts:
|
|
if getattr(account, 'circuit', None) is circuit:
|
|
account.connection_error(Exception(str(error)))
|
|
|
|
def quit(self):
|
|
for account in self.accounts:
|
|
account.active = False
|
|
reactor.stop()
|