Initial Commit
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Instance Messenger, Pan-protocol chat client.
|
||||
"""
|
||||
@@ -0,0 +1,69 @@
|
||||
# -*- Python -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
|
||||
class AccountManager:
|
||||
"""I am responsible for managing a user's accounts.
|
||||
|
||||
That is, remembering what accounts are available, their settings,
|
||||
adding and removal of accounts, etc.
|
||||
|
||||
@ivar accounts: A collection of available accounts.
|
||||
@type accounts: mapping of strings to L{Account<interfaces.IAccount>}s.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.accounts = {}
|
||||
|
||||
def getSnapShot(self):
|
||||
"""A snapshot of all the accounts and their status.
|
||||
|
||||
@returns: A list of tuples, each of the form
|
||||
(string:accountName, boolean:isOnline,
|
||||
boolean:autoLogin, string:gatewayType)
|
||||
"""
|
||||
data = []
|
||||
for account in self.accounts.values():
|
||||
data.append(
|
||||
(
|
||||
account.accountName,
|
||||
account.isOnline(),
|
||||
account.autoLogin,
|
||||
account.gatewayType,
|
||||
)
|
||||
)
|
||||
return data
|
||||
|
||||
def isEmpty(self):
|
||||
return len(self.accounts) == 0
|
||||
|
||||
def getConnectionInfo(self):
|
||||
connectioninfo = []
|
||||
for account in self.accounts.values():
|
||||
connectioninfo.append(account.isOnline())
|
||||
return connectioninfo
|
||||
|
||||
def addAccount(self, account):
|
||||
self.accounts[account.accountName] = account
|
||||
|
||||
def delAccount(self, accountName):
|
||||
del self.accounts[accountName]
|
||||
|
||||
def connect(self, accountName, chatui):
|
||||
"""
|
||||
@returntype: Deferred L{interfaces.IClient}
|
||||
"""
|
||||
return self.accounts[accountName].logOn(chatui)
|
||||
|
||||
def disconnect(self, accountName):
|
||||
pass
|
||||
# self.accounts[accountName].logOff() - not yet implemented
|
||||
|
||||
def quit(self):
|
||||
pass
|
||||
# for account in self.accounts.values():
|
||||
# account.logOff() - not yet implemented
|
||||
@@ -0,0 +1,486 @@
|
||||
# -*- test-case-name: twisted.words.test.test_basechat -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Base classes for Instance Messenger clients.
|
||||
"""
|
||||
|
||||
from twisted.words.im.locals import AWAY, OFFLINE, ONLINE
|
||||
|
||||
|
||||
class ContactsList:
|
||||
"""
|
||||
A GUI object that displays a contacts list.
|
||||
|
||||
@ivar chatui: The GUI chat client associated with this contacts list.
|
||||
@type chatui: L{ChatUI}
|
||||
|
||||
@ivar contacts: The contacts.
|
||||
@type contacts: C{dict} mapping C{str} to a L{IPerson<interfaces.IPerson>}
|
||||
provider
|
||||
|
||||
@ivar onlineContacts: The contacts who are currently online (have a status
|
||||
that is not C{OFFLINE}).
|
||||
@type onlineContacts: C{dict} mapping C{str} to a
|
||||
L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@ivar clients: The signed-on clients.
|
||||
@type clients: C{list} of L{IClient<interfaces.IClient>} providers
|
||||
"""
|
||||
|
||||
def __init__(self, chatui):
|
||||
"""
|
||||
@param chatui: The GUI chat client associated with this contacts list.
|
||||
@type chatui: L{ChatUI}
|
||||
"""
|
||||
self.chatui = chatui
|
||||
self.contacts = {}
|
||||
self.onlineContacts = {}
|
||||
self.clients = []
|
||||
|
||||
def setContactStatus(self, person):
|
||||
"""
|
||||
Inform the user that a person's status has changed.
|
||||
|
||||
@param person: The person whose status has changed.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
"""
|
||||
if person.name not in self.contacts:
|
||||
self.contacts[person.name] = person
|
||||
if person.name not in self.onlineContacts and (
|
||||
person.status == ONLINE or person.status == AWAY
|
||||
):
|
||||
self.onlineContacts[person.name] = person
|
||||
if person.name in self.onlineContacts and person.status == OFFLINE:
|
||||
del self.onlineContacts[person.name]
|
||||
|
||||
def registerAccountClient(self, client):
|
||||
"""
|
||||
Notify the user that an account client has been signed on to.
|
||||
|
||||
@param client: The client being added to your list of account clients.
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
"""
|
||||
if client not in self.clients:
|
||||
self.clients.append(client)
|
||||
|
||||
def unregisterAccountClient(self, client):
|
||||
"""
|
||||
Notify the user that an account client has been signed off or
|
||||
disconnected from.
|
||||
|
||||
@param client: The client being removed from the list of account
|
||||
clients.
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
"""
|
||||
if client in self.clients:
|
||||
self.clients.remove(client)
|
||||
|
||||
def contactChangedNick(self, person, newnick):
|
||||
"""
|
||||
Update your contact information to reflect a change to a contact's
|
||||
nickname.
|
||||
|
||||
@param person: The person in your contacts list whose nickname is
|
||||
changing.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@param newnick: The new nickname for this person.
|
||||
@type newnick: C{str}
|
||||
"""
|
||||
oldname = person.name
|
||||
if oldname in self.contacts:
|
||||
del self.contacts[oldname]
|
||||
person.name = newnick
|
||||
self.contacts[newnick] = person
|
||||
if oldname in self.onlineContacts:
|
||||
del self.onlineContacts[oldname]
|
||||
self.onlineContacts[newnick] = person
|
||||
|
||||
|
||||
class Conversation:
|
||||
"""
|
||||
A GUI window of a conversation with a specific person.
|
||||
|
||||
@ivar person: The person who you're having this conversation with.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@ivar chatui: The GUI chat client associated with this conversation.
|
||||
@type chatui: L{ChatUI}
|
||||
"""
|
||||
|
||||
def __init__(self, person, chatui):
|
||||
"""
|
||||
@param person: The person who you're having this conversation with.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@param chatui: The GUI chat client associated with this conversation.
|
||||
@type chatui: L{ChatUI}
|
||||
"""
|
||||
self.chatui = chatui
|
||||
self.person = person
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Display the ConversationWindow.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def hide(self):
|
||||
"""
|
||||
Hide the ConversationWindow.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def sendText(self, text):
|
||||
"""
|
||||
Send text to the person with whom the user is conversing.
|
||||
|
||||
@param text: The text to be sent.
|
||||
@type text: C{str}
|
||||
"""
|
||||
self.person.sendMessage(text, None)
|
||||
|
||||
def showMessage(self, text, metadata=None):
|
||||
"""
|
||||
Display a message sent from the person with whom the user is conversing.
|
||||
|
||||
@param text: The sent message.
|
||||
@type text: C{str}
|
||||
|
||||
@param metadata: Metadata associated with this message.
|
||||
@type metadata: C{dict}
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def contactChangedNick(self, person, newnick):
|
||||
"""
|
||||
Change a person's name.
|
||||
|
||||
@param person: The person whose nickname is changing.
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
|
||||
@param newnick: The new nickname for this person.
|
||||
@type newnick: C{str}
|
||||
"""
|
||||
self.person.name = newnick
|
||||
|
||||
|
||||
class GroupConversation:
|
||||
"""
|
||||
A GUI window of a conversation with a group of people.
|
||||
|
||||
@ivar chatui: The GUI chat client associated with this conversation.
|
||||
@type chatui: L{ChatUI}
|
||||
|
||||
@ivar group: The group of people that are having this conversation.
|
||||
@type group: L{IGroup<interfaces.IGroup>} provider
|
||||
|
||||
@ivar members: The names of the people in this conversation.
|
||||
@type members: C{list} of C{str}
|
||||
"""
|
||||
|
||||
def __init__(self, group, chatui):
|
||||
"""
|
||||
@param chatui: The GUI chat client associated with this conversation.
|
||||
@type chatui: L{ChatUI}
|
||||
|
||||
@param group: The group of people that are having this conversation.
|
||||
@type group: L{IGroup<interfaces.IGroup>} provider
|
||||
"""
|
||||
self.chatui = chatui
|
||||
self.group = group
|
||||
self.members = []
|
||||
|
||||
def show(self):
|
||||
"""
|
||||
Display the GroupConversationWindow.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def hide(self):
|
||||
"""
|
||||
Hide the GroupConversationWindow.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def sendText(self, text):
|
||||
"""
|
||||
Send text to the group.
|
||||
|
||||
@param text: The text to be sent.
|
||||
@type text: C{str}
|
||||
"""
|
||||
self.group.sendGroupMessage(text, None)
|
||||
|
||||
def showGroupMessage(self, sender, text, metadata=None):
|
||||
"""
|
||||
Display to the user a message sent to this group from the given sender.
|
||||
|
||||
@param sender: The person sending the message.
|
||||
@type sender: C{str}
|
||||
|
||||
@param text: The sent message.
|
||||
@type text: C{str}
|
||||
|
||||
@param metadata: Metadata associated with this message.
|
||||
@type metadata: C{dict}
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def setGroupMembers(self, members):
|
||||
"""
|
||||
Set the list of members in the group.
|
||||
|
||||
@param members: The names of the people that will be in this group.
|
||||
@type members: C{list} of C{str}
|
||||
"""
|
||||
self.members = members
|
||||
|
||||
def setTopic(self, topic, author):
|
||||
"""
|
||||
Change the topic for the group conversation window and display this
|
||||
change to the user.
|
||||
|
||||
@param topic: This group's topic.
|
||||
@type topic: C{str}
|
||||
|
||||
@param author: The person changing the topic.
|
||||
@type author: C{str}
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this method")
|
||||
|
||||
def memberJoined(self, member):
|
||||
"""
|
||||
Add the given member to the list of members in the group conversation
|
||||
and displays this to the user.
|
||||
|
||||
@param member: The person joining the group conversation.
|
||||
@type member: C{str}
|
||||
"""
|
||||
if member not in self.members:
|
||||
self.members.append(member)
|
||||
|
||||
def memberChangedNick(self, oldnick, newnick):
|
||||
"""
|
||||
Change the nickname for a member of the group conversation and displays
|
||||
this change to the user.
|
||||
|
||||
@param oldnick: The old nickname.
|
||||
@type oldnick: C{str}
|
||||
|
||||
@param newnick: The new nickname.
|
||||
@type newnick: C{str}
|
||||
"""
|
||||
if oldnick in self.members:
|
||||
self.members.remove(oldnick)
|
||||
self.members.append(newnick)
|
||||
|
||||
def memberLeft(self, member):
|
||||
"""
|
||||
Delete the given member from the list of members in the group
|
||||
conversation and displays the change to the user.
|
||||
|
||||
@param member: The person leaving the group conversation.
|
||||
@type member: C{str}
|
||||
"""
|
||||
if member in self.members:
|
||||
self.members.remove(member)
|
||||
|
||||
|
||||
class ChatUI:
|
||||
"""
|
||||
A GUI chat client.
|
||||
|
||||
@type conversations: C{dict} of L{Conversation}
|
||||
@ivar conversations: A cache of all the direct windows.
|
||||
|
||||
@type groupConversations: C{dict} of L{GroupConversation}
|
||||
@ivar groupConversations: A cache of all the group windows.
|
||||
|
||||
@type persons: C{dict} with keys that are a C{tuple} of (C{str},
|
||||
L{IAccount<interfaces.IAccount>} provider) and values that are
|
||||
L{IPerson<interfaces.IPerson>} provider
|
||||
@ivar persons: A cache of all the users associated with this client.
|
||||
|
||||
@type groups: C{dict} with keys that are a C{tuple} of (C{str},
|
||||
L{IAccount<interfaces.IAccount>} provider) and values that are
|
||||
L{IGroup<interfaces.IGroup>} provider
|
||||
@ivar groups: A cache of all the groups associated with this client.
|
||||
|
||||
@type onlineClients: C{list} of L{IClient<interfaces.IClient>} providers
|
||||
@ivar onlineClients: A list of message sources currently online.
|
||||
|
||||
@type contactsList: L{ContactsList}
|
||||
@ivar contactsList: A contacts list.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.conversations = {}
|
||||
self.groupConversations = {}
|
||||
self.persons = {}
|
||||
self.groups = {}
|
||||
self.onlineClients = []
|
||||
self.contactsList = ContactsList(self)
|
||||
|
||||
def registerAccountClient(self, client):
|
||||
"""
|
||||
Notify the user that an account has been signed on to.
|
||||
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
@param client: The client account for the person who has just signed on.
|
||||
|
||||
@rtype: L{IClient<interfaces.IClient>} provider
|
||||
@return: The client, so that it may be used in a callback chain.
|
||||
"""
|
||||
self.onlineClients.append(client)
|
||||
self.contactsList.registerAccountClient(client)
|
||||
return client
|
||||
|
||||
def unregisterAccountClient(self, client):
|
||||
"""
|
||||
Notify the user that an account has been signed off or disconnected.
|
||||
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
@param client: The client account for the person who has just signed
|
||||
off.
|
||||
"""
|
||||
self.onlineClients.remove(client)
|
||||
self.contactsList.unregisterAccountClient(client)
|
||||
|
||||
def getContactsList(self):
|
||||
"""
|
||||
Get the contacts list associated with this chat window.
|
||||
|
||||
@rtype: L{ContactsList}
|
||||
@return: The contacts list associated with this chat window.
|
||||
"""
|
||||
return self.contactsList
|
||||
|
||||
def getConversation(self, person, Class=Conversation, stayHidden=False):
|
||||
"""
|
||||
For the given person object, return the conversation window or create
|
||||
and return a new conversation window if one does not exist.
|
||||
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
@param person: The person whose conversation window we want to get.
|
||||
|
||||
@type Class: L{IConversation<interfaces.IConversation>} implementor
|
||||
@param Class: The kind of conversation window we want. If the conversation
|
||||
window for this person didn't already exist, create one of this type.
|
||||
|
||||
@type stayHidden: C{bool}
|
||||
@param stayHidden: Whether or not the conversation window should stay
|
||||
hidden.
|
||||
|
||||
@rtype: L{IConversation<interfaces.IConversation>} provider
|
||||
@return: The conversation window.
|
||||
"""
|
||||
conv = self.conversations.get(person)
|
||||
if not conv:
|
||||
conv = Class(person, self)
|
||||
self.conversations[person] = conv
|
||||
if stayHidden:
|
||||
conv.hide()
|
||||
else:
|
||||
conv.show()
|
||||
return conv
|
||||
|
||||
def getGroupConversation(self, group, Class=GroupConversation, stayHidden=False):
|
||||
"""
|
||||
For the given group object, return the group conversation window or
|
||||
create and return a new group conversation window if it doesn't exist.
|
||||
|
||||
@type group: L{IGroup<interfaces.IGroup>} provider
|
||||
@param group: The group whose conversation window we want to get.
|
||||
|
||||
@type Class: L{IConversation<interfaces.IConversation>} implementor
|
||||
@param Class: The kind of conversation window we want. If the conversation
|
||||
window for this person didn't already exist, create one of this type.
|
||||
|
||||
@type stayHidden: C{bool}
|
||||
@param stayHidden: Whether or not the conversation window should stay
|
||||
hidden.
|
||||
|
||||
@rtype: L{IGroupConversation<interfaces.IGroupConversation>} provider
|
||||
@return: The group conversation window.
|
||||
"""
|
||||
conv = self.groupConversations.get(group)
|
||||
if not conv:
|
||||
conv = Class(group, self)
|
||||
self.groupConversations[group] = conv
|
||||
if stayHidden:
|
||||
conv.hide()
|
||||
else:
|
||||
conv.show()
|
||||
return conv
|
||||
|
||||
def getPerson(self, name, client):
|
||||
"""
|
||||
For the given name and account client, return an instance of a
|
||||
L{IGroup<interfaces.IPerson>} provider or create and return a new
|
||||
instance of a L{IGroup<interfaces.IPerson>} provider.
|
||||
|
||||
@type name: C{str}
|
||||
@param name: The name of the person of interest.
|
||||
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
@param client: The client account of interest.
|
||||
|
||||
@rtype: L{IPerson<interfaces.IPerson>} provider
|
||||
@return: The person with that C{name}.
|
||||
"""
|
||||
account = client.account
|
||||
p = self.persons.get((name, account))
|
||||
if not p:
|
||||
p = account.getPerson(name)
|
||||
self.persons[name, account] = p
|
||||
return p
|
||||
|
||||
def getGroup(self, name, client):
|
||||
"""
|
||||
For the given name and account client, return an instance of a
|
||||
L{IGroup<interfaces.IGroup>} provider or create and return a new instance
|
||||
of a L{IGroup<interfaces.IGroup>} provider.
|
||||
|
||||
@type name: C{str}
|
||||
@param name: The name of the group of interest.
|
||||
|
||||
@type client: L{IClient<interfaces.IClient>} provider
|
||||
@param client: The client account of interest.
|
||||
|
||||
@rtype: L{IGroup<interfaces.IGroup>} provider
|
||||
@return: The group with that C{name}.
|
||||
"""
|
||||
# I accept 'client' instead of 'account' in my signature for
|
||||
# backwards compatibility. (Groups changed to be Account-oriented
|
||||
# in CVS revision 1.8.)
|
||||
account = client.account
|
||||
g = self.groups.get((name, account))
|
||||
if not g:
|
||||
g = account.getGroup(name)
|
||||
self.groups[name, account] = g
|
||||
return g
|
||||
|
||||
def contactChangedNick(self, person, newnick):
|
||||
"""
|
||||
For the given C{person}, change the C{person}'s C{name} to C{newnick}
|
||||
and tell the contact list and any conversation windows with that
|
||||
C{person} to change as well.
|
||||
|
||||
@type person: L{IPerson<interfaces.IPerson>} provider
|
||||
@param person: The person whose nickname will get changed.
|
||||
|
||||
@type newnick: C{str}
|
||||
@param newnick: The new C{name} C{person} will take.
|
||||
"""
|
||||
oldnick = person.name
|
||||
if (oldnick, person.account) in self.persons:
|
||||
conv = self.conversations.get(person)
|
||||
if conv:
|
||||
conv.contactChangedNick(person, newnick)
|
||||
self.contactsList.contactChangedNick(person, newnick)
|
||||
del self.persons[oldnick, person.account]
|
||||
person.name = newnick
|
||||
self.persons[person.name, person.account] = person
|
||||
@@ -0,0 +1,275 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
#
|
||||
|
||||
"""Instance Messenger base classes for protocol support.
|
||||
|
||||
You will find these useful if you're adding a new protocol to IM.
|
||||
"""
|
||||
from typing import Type
|
||||
|
||||
from twisted.internet import error
|
||||
from twisted.internet.protocol import Protocol, connectionDone
|
||||
from twisted.persisted import styles
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.reflect import prefixedMethods
|
||||
from twisted.words.im.locals import OFFLINE, OfflineError
|
||||
|
||||
# Abstract representation of chat "model" classes
|
||||
|
||||
|
||||
class AbstractGroup:
|
||||
def __init__(self, name, account):
|
||||
self.name = name
|
||||
self.account = account
|
||||
|
||||
def getGroupCommands(self):
|
||||
"""finds group commands
|
||||
|
||||
these commands are methods on me that start with imgroup_; they are
|
||||
called with no arguments
|
||||
"""
|
||||
return prefixedMethods(self, "imgroup_")
|
||||
|
||||
def getTargetCommands(self, target):
|
||||
"""finds group commands
|
||||
|
||||
these commands are methods on me that start with imgroup_; they are
|
||||
called with a user present within this room as an argument
|
||||
|
||||
you may want to override this in your group in order to filter for
|
||||
appropriate commands on the given user
|
||||
"""
|
||||
return prefixedMethods(self, "imtarget_")
|
||||
|
||||
def join(self):
|
||||
if not self.account.client:
|
||||
raise OfflineError
|
||||
self.account.client.joinGroup(self.name)
|
||||
|
||||
def leave(self):
|
||||
if not self.account.client:
|
||||
raise OfflineError
|
||||
self.account.client.leaveGroup(self.name)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__} {self.name!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}@{self.account.accountName}"
|
||||
|
||||
|
||||
class AbstractPerson:
|
||||
def __init__(self, name, baseAccount):
|
||||
self.name = name
|
||||
self.account = baseAccount
|
||||
self.status = OFFLINE
|
||||
|
||||
def getPersonCommands(self):
|
||||
"""finds person commands
|
||||
|
||||
these commands are methods on me that start with imperson_; they are
|
||||
called with no arguments
|
||||
"""
|
||||
return prefixedMethods(self, "imperson_")
|
||||
|
||||
def getIdleTime(self):
|
||||
"""
|
||||
Returns a string.
|
||||
"""
|
||||
return "--"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__} {self.name!r}/{self.status}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.name}@{self.account.accountName}"
|
||||
|
||||
|
||||
class AbstractClientMixin:
|
||||
"""Designed to be mixed in to a Protocol implementing class.
|
||||
|
||||
Inherit from me first.
|
||||
|
||||
@ivar _logonDeferred: Fired when I am done logging in.
|
||||
"""
|
||||
|
||||
_protoBase: Type[Protocol] = None # type: ignore[assignment]
|
||||
|
||||
def __init__(self, account, chatui, logonDeferred):
|
||||
for base in self.__class__.__bases__:
|
||||
if issubclass(base, Protocol):
|
||||
self.__class__._protoBase = base
|
||||
break
|
||||
else:
|
||||
pass
|
||||
self.account = account
|
||||
self.chat = chatui
|
||||
self._logonDeferred = logonDeferred
|
||||
|
||||
def connectionMade(self):
|
||||
self._protoBase.connectionMade(self)
|
||||
|
||||
def connectionLost(self, reason: Failure = connectionDone):
|
||||
self.account._clientLost(self, reason)
|
||||
self.unregisterAsAccountClient()
|
||||
return self._protoBase.connectionLost(self, reason) # type: ignore[arg-type]
|
||||
|
||||
def unregisterAsAccountClient(self):
|
||||
"""Tell the chat UI that I have `signed off'."""
|
||||
self.chat.unregisterAccountClient(self)
|
||||
|
||||
|
||||
class AbstractAccount(styles.Versioned):
|
||||
"""Base class for Accounts.
|
||||
|
||||
I am the start of an implementation of L{IAccount<interfaces.IAccount>}, I
|
||||
implement L{isOnline} and most of L{logOn}, though you'll need to implement
|
||||
L{_startLogOn} in a subclass.
|
||||
|
||||
@cvar _groupFactory: A Callable that will return a L{IGroup} appropriate
|
||||
for this account type.
|
||||
@cvar _personFactory: A Callable that will return a L{IPerson} appropriate
|
||||
for this account type.
|
||||
|
||||
@type _isConnecting: boolean
|
||||
@ivar _isConnecting: Whether I am in the process of establishing a
|
||||
connection to the server.
|
||||
@type _isOnline: boolean
|
||||
@ivar _isOnline: Whether I am currently on-line with the server.
|
||||
|
||||
@ivar accountName:
|
||||
@ivar autoLogin:
|
||||
@ivar username:
|
||||
@ivar password:
|
||||
@ivar host:
|
||||
@ivar port:
|
||||
"""
|
||||
|
||||
_isOnline = 0
|
||||
_isConnecting = 0
|
||||
client = None
|
||||
|
||||
_groupFactory = AbstractGroup
|
||||
_personFactory = AbstractPerson
|
||||
|
||||
persistanceVersion = 2
|
||||
|
||||
def __init__(self, accountName, autoLogin, username, password, host, port):
|
||||
self.accountName = accountName
|
||||
self.autoLogin = autoLogin
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
self._groups = {}
|
||||
self._persons = {}
|
||||
|
||||
def upgrateToVersion2(self):
|
||||
# Added in CVS revision 1.16.
|
||||
for k in ("_groups", "_persons"):
|
||||
if not hasattr(self, k):
|
||||
setattr(self, k, {})
|
||||
|
||||
def __getstate__(self):
|
||||
state = styles.Versioned.__getstate__(self)
|
||||
for k in ("client", "_isOnline", "_isConnecting"):
|
||||
try:
|
||||
del state[k]
|
||||
except KeyError:
|
||||
pass
|
||||
return state
|
||||
|
||||
def isOnline(self):
|
||||
return self._isOnline
|
||||
|
||||
def logOn(self, chatui):
|
||||
"""Log on to this account.
|
||||
|
||||
Takes care to not start a connection if a connection is
|
||||
already in progress. You will need to implement
|
||||
L{_startLogOn} for this to work, and it would be a good idea
|
||||
to override L{_loginFailed} too.
|
||||
|
||||
@returntype: Deferred L{interfaces.IClient}
|
||||
"""
|
||||
if (not self._isConnecting) and (not self._isOnline):
|
||||
self._isConnecting = 1
|
||||
d = self._startLogOn(chatui)
|
||||
d.addCallback(self._cb_logOn)
|
||||
# if chatui is not None:
|
||||
# (I don't particularly like having to pass chatUI to this function,
|
||||
# but we haven't factored it out yet.)
|
||||
d.addCallback(chatui.registerAccountClient)
|
||||
d.addErrback(self._loginFailed)
|
||||
return d
|
||||
else:
|
||||
raise error.ConnectError("Connection in progress")
|
||||
|
||||
def getGroup(self, name):
|
||||
"""Group factory.
|
||||
|
||||
@param name: Name of the group on this account.
|
||||
@type name: string
|
||||
"""
|
||||
group = self._groups.get(name)
|
||||
if group is None:
|
||||
group = self._groupFactory(name, self)
|
||||
self._groups[name] = group
|
||||
return group
|
||||
|
||||
def getPerson(self, name):
|
||||
"""Person factory.
|
||||
|
||||
@param name: Name of the person on this account.
|
||||
@type name: string
|
||||
"""
|
||||
person = self._persons.get(name)
|
||||
if person is None:
|
||||
person = self._personFactory(name, self)
|
||||
self._persons[name] = person
|
||||
return person
|
||||
|
||||
def _startLogOn(self, chatui):
|
||||
"""Start the sign on process.
|
||||
|
||||
Factored out of L{logOn}.
|
||||
|
||||
@returntype: Deferred L{interfaces.IClient}
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _cb_logOn(self, client):
|
||||
self._isConnecting = 0
|
||||
self._isOnline = 1
|
||||
self.client = client
|
||||
return client
|
||||
|
||||
def _loginFailed(self, reason):
|
||||
"""Errorback for L{logOn}.
|
||||
|
||||
@type reason: Failure
|
||||
|
||||
@returns: I{reason}, for further processing in the callback chain.
|
||||
@returntype: Failure
|
||||
"""
|
||||
self._isConnecting = 0
|
||||
self._isOnline = 0 # just in case
|
||||
return reason
|
||||
|
||||
def _clientLost(self, client, reason):
|
||||
self.client = None
|
||||
self._isConnecting = 0
|
||||
self._isOnline = 0
|
||||
return reason
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<{}: {} ({}@{}:{})>".format(
|
||||
self.__class__,
|
||||
self.accountName,
|
||||
self.username,
|
||||
self.host,
|
||||
self.port,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,363 @@
|
||||
# -*- Python -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Pan-protocol chat client.
|
||||
"""
|
||||
|
||||
from zope.interface import Attribute, Interface
|
||||
|
||||
# (Random musings, may not reflect on current state of code:)
|
||||
#
|
||||
# Accounts have Protocol components (clients)
|
||||
# Persons have Conversation components
|
||||
# Groups have GroupConversation components
|
||||
# Persons and Groups are associated with specific Accounts
|
||||
# At run-time, Clients/Accounts are slaved to a User Interface
|
||||
# (Note: User may be a bot, so don't assume all UIs are built on gui toolkits)
|
||||
|
||||
|
||||
class IAccount(Interface):
|
||||
"""
|
||||
I represent a user's account with a chat service.
|
||||
"""
|
||||
|
||||
client = Attribute("The L{IClient} currently connecting to this account, if any.")
|
||||
gatewayType = Attribute(
|
||||
"A C{str} that identifies the protocol used by this account."
|
||||
)
|
||||
|
||||
def __init__(accountName, autoLogin, username, password, host, port):
|
||||
"""
|
||||
@type accountName: string
|
||||
@param accountName: A name to refer to the account by locally.
|
||||
@type autoLogin: boolean
|
||||
@type username: string
|
||||
@type password: string
|
||||
@type host: string
|
||||
@type port: integer
|
||||
"""
|
||||
|
||||
def isOnline():
|
||||
"""
|
||||
Am I online?
|
||||
|
||||
@rtype: boolean
|
||||
"""
|
||||
|
||||
def logOn(chatui):
|
||||
"""
|
||||
Go on-line.
|
||||
|
||||
@type chatui: Implementor of C{IChatUI}
|
||||
|
||||
@rtype: L{Deferred} with an eventual L{IClient} result.
|
||||
"""
|
||||
|
||||
def logOff():
|
||||
"""
|
||||
Sign off.
|
||||
"""
|
||||
|
||||
def getGroup(groupName):
|
||||
"""
|
||||
@rtype: L{Group<IGroup>}
|
||||
"""
|
||||
|
||||
def getPerson(personName):
|
||||
"""
|
||||
@rtype: L{Person<IPerson>}
|
||||
"""
|
||||
|
||||
|
||||
class IClient(Interface):
|
||||
|
||||
account = Attribute("The L{IAccount} I am a Client for")
|
||||
|
||||
def __init__(account, chatui, logonDeferred):
|
||||
"""
|
||||
@type account: L{IAccount}
|
||||
@type chatui: L{IChatUI}
|
||||
@param logonDeferred: Will be called back once I am logged on.
|
||||
@type logonDeferred: L{Deferred<twisted.internet.defer.Deferred>}
|
||||
"""
|
||||
|
||||
def joinGroup(groupName):
|
||||
"""
|
||||
@param groupName: The name of the group to join.
|
||||
@type groupName: string
|
||||
"""
|
||||
|
||||
def leaveGroup(groupName):
|
||||
"""
|
||||
@param groupName: The name of the group to leave.
|
||||
@type groupName: string
|
||||
"""
|
||||
|
||||
def getGroupConversation(name, hide=0):
|
||||
pass
|
||||
|
||||
def getPerson(name):
|
||||
pass
|
||||
|
||||
|
||||
class IPerson(Interface):
|
||||
def __init__(name, account):
|
||||
"""
|
||||
Initialize me.
|
||||
|
||||
@param name: My name, as the server knows me.
|
||||
@type name: string
|
||||
@param account: The account I am accessed through.
|
||||
@type account: I{Account}
|
||||
"""
|
||||
|
||||
def isOnline():
|
||||
"""
|
||||
Am I online right now?
|
||||
|
||||
@rtype: boolean
|
||||
"""
|
||||
|
||||
def getStatus():
|
||||
"""
|
||||
What is my on-line status?
|
||||
|
||||
@return: L{locals.StatusEnum}
|
||||
"""
|
||||
|
||||
def getIdleTime():
|
||||
"""
|
||||
@rtype: string (XXX: How about a scalar?)
|
||||
"""
|
||||
|
||||
def sendMessage(text, metadata=None):
|
||||
"""
|
||||
Send a message to this person.
|
||||
|
||||
@type text: string
|
||||
@type metadata: dict
|
||||
"""
|
||||
|
||||
|
||||
class IGroup(Interface):
|
||||
"""
|
||||
A group which you may have a conversation with.
|
||||
|
||||
Groups generally have a loosely-defined set of members, who may
|
||||
leave and join at any time.
|
||||
"""
|
||||
|
||||
name = Attribute("My C{str} name, as the server knows me.")
|
||||
account = Attribute("The L{Account<IAccount>} I am accessed through.")
|
||||
|
||||
def __init__(name, account):
|
||||
"""
|
||||
Initialize me.
|
||||
|
||||
@param name: My name, as the server knows me.
|
||||
@type name: str
|
||||
@param account: The account I am accessed through.
|
||||
@type account: L{Account<IAccount>}
|
||||
"""
|
||||
|
||||
def setTopic(text):
|
||||
"""
|
||||
Set this Groups topic on the server.
|
||||
|
||||
@type text: string
|
||||
"""
|
||||
|
||||
def sendGroupMessage(text, metadata=None):
|
||||
"""
|
||||
Send a message to this group.
|
||||
|
||||
@type text: str
|
||||
|
||||
@type metadata: dict
|
||||
@param metadata: Valid keys for this dictionary include:
|
||||
|
||||
- C{'style'}: associated with one of:
|
||||
- C{'emote'}: indicates this is an action
|
||||
"""
|
||||
|
||||
def join():
|
||||
"""
|
||||
Join this group.
|
||||
"""
|
||||
|
||||
def leave():
|
||||
"""
|
||||
Depart this group.
|
||||
"""
|
||||
|
||||
|
||||
class IConversation(Interface):
|
||||
"""
|
||||
A conversation with a specific person.
|
||||
"""
|
||||
|
||||
def __init__(person, chatui):
|
||||
"""
|
||||
@type person: L{IPerson}
|
||||
"""
|
||||
|
||||
def show():
|
||||
"""
|
||||
doesn't seem like it belongs in this interface.
|
||||
"""
|
||||
|
||||
def hide():
|
||||
"""
|
||||
nor this neither.
|
||||
"""
|
||||
|
||||
def sendText(text, metadata):
|
||||
pass
|
||||
|
||||
def showMessage(text, metadata):
|
||||
pass
|
||||
|
||||
def changedNick(person, newnick):
|
||||
"""
|
||||
@param person: XXX Shouldn't this always be Conversation.person?
|
||||
"""
|
||||
|
||||
|
||||
class IGroupConversation(Interface):
|
||||
def show():
|
||||
"""
|
||||
doesn't seem like it belongs in this interface.
|
||||
"""
|
||||
|
||||
def hide():
|
||||
"""
|
||||
nor this neither.
|
||||
"""
|
||||
|
||||
def sendText(text, metadata):
|
||||
pass
|
||||
|
||||
def showGroupMessage(sender, text, metadata):
|
||||
pass
|
||||
|
||||
def setGroupMembers(members):
|
||||
"""
|
||||
Sets the list of members in the group and displays it to the user.
|
||||
"""
|
||||
|
||||
def setTopic(topic, author):
|
||||
"""
|
||||
Displays the topic (from the server) for the group conversation window.
|
||||
|
||||
@type topic: string
|
||||
@type author: string (XXX: Not Person?)
|
||||
"""
|
||||
|
||||
def memberJoined(member):
|
||||
"""
|
||||
Adds the given member to the list of members in the group conversation
|
||||
and displays this to the user,
|
||||
|
||||
@type member: string (XXX: Not Person?)
|
||||
"""
|
||||
|
||||
def memberChangedNick(oldnick, newnick):
|
||||
"""
|
||||
Changes the oldnick in the list of members to C{newnick} and displays this
|
||||
change to the user,
|
||||
|
||||
@type oldnick: string (XXX: Not Person?)
|
||||
@type newnick: string
|
||||
"""
|
||||
|
||||
def memberLeft(member):
|
||||
"""
|
||||
Deletes the given member from the list of members in the group
|
||||
conversation and displays the change to the user.
|
||||
|
||||
@type member: string (XXX: Not Person?)
|
||||
"""
|
||||
|
||||
|
||||
class IChatUI(Interface):
|
||||
def registerAccountClient(client):
|
||||
"""
|
||||
Notifies user that an account has been signed on to.
|
||||
|
||||
@type client: L{Client<IClient>}
|
||||
"""
|
||||
|
||||
def unregisterAccountClient(client):
|
||||
"""
|
||||
Notifies user that an account has been signed off or disconnected.
|
||||
|
||||
@type client: L{Client<IClient>}
|
||||
"""
|
||||
|
||||
def getContactsList():
|
||||
"""
|
||||
@rtype: L{ContactsList}
|
||||
"""
|
||||
|
||||
# WARNING: You'll want to be polymorphed into something with
|
||||
# intrinsic stoning resistance before continuing.
|
||||
|
||||
def getConversation(person, Class, stayHidden=0):
|
||||
"""
|
||||
For the given person object, returns the conversation window
|
||||
or creates and returns a new conversation window if one does not exist.
|
||||
|
||||
@type person: L{Person<IPerson>}
|
||||
@type Class: L{Conversation<IConversation>} class
|
||||
@type stayHidden: boolean
|
||||
|
||||
@rtype: L{Conversation<IConversation>}
|
||||
"""
|
||||
|
||||
def getGroupConversation(group, Class, stayHidden=0):
|
||||
"""
|
||||
For the given group object, returns the group conversation window or
|
||||
creates and returns a new group conversation window if it doesn't exist.
|
||||
|
||||
@type group: L{Group<interfaces.IGroup>}
|
||||
@type Class: L{Conversation<interfaces.IConversation>} class
|
||||
@type stayHidden: boolean
|
||||
|
||||
@rtype: L{GroupConversation<interfaces.IGroupConversation>}
|
||||
"""
|
||||
|
||||
def getPerson(name, client):
|
||||
"""
|
||||
Get a Person for a client.
|
||||
|
||||
Duplicates L{IAccount.getPerson}.
|
||||
|
||||
@type name: string
|
||||
@type client: L{Client<IClient>}
|
||||
|
||||
@rtype: L{Person<IPerson>}
|
||||
"""
|
||||
|
||||
def getGroup(name, client):
|
||||
"""
|
||||
Get a Group for a client.
|
||||
|
||||
Duplicates L{IAccount.getGroup}.
|
||||
|
||||
@type name: string
|
||||
@type client: L{Client<IClient>}
|
||||
|
||||
@rtype: L{Group<IGroup>}
|
||||
"""
|
||||
|
||||
def contactChangedNick(oldnick, newnick):
|
||||
"""
|
||||
For the given person, changes the person's name to newnick, and
|
||||
tells the contact list and any conversation windows with that person
|
||||
to change as well.
|
||||
|
||||
@type oldnick: string
|
||||
@type newnick: string
|
||||
"""
|
||||
@@ -0,0 +1,273 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
IRC support for Instance Messenger.
|
||||
"""
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.internet.defer import succeed
|
||||
from twisted.words.im import basesupport, interfaces, locals
|
||||
from twisted.words.im.locals import ONLINE
|
||||
from twisted.words.protocols import irc
|
||||
|
||||
|
||||
class IRCPerson(basesupport.AbstractPerson):
|
||||
def imperson_whois(self):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
self.account.client.sendLine("WHOIS %s" % self.name)
|
||||
|
||||
### interface impl
|
||||
def isOnline(self):
|
||||
return ONLINE
|
||||
|
||||
def getStatus(self):
|
||||
return ONLINE
|
||||
|
||||
def setStatus(self, status):
|
||||
self.status = status
|
||||
self.chat.getContactsList().setContactStatus(self)
|
||||
|
||||
def sendMessage(self, text, meta=None):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
for line in text.split("\n"):
|
||||
if meta and meta.get("style", None) == "emote":
|
||||
self.account.client.ctcpMakeQuery(self.name, [("ACTION", line)])
|
||||
else:
|
||||
self.account.client.msg(self.name, line)
|
||||
return succeed(text)
|
||||
|
||||
|
||||
@implementer(interfaces.IGroup)
|
||||
class IRCGroup(basesupport.AbstractGroup):
|
||||
def imgroup_testAction(self):
|
||||
pass
|
||||
|
||||
def imtarget_kick(self, target):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
reason = "for great justice!"
|
||||
self.account.client.sendLine(f"KICK #{self.name} {target.name} :{reason}")
|
||||
|
||||
### Interface Implementation
|
||||
def setTopic(self, topic):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
self.account.client.topic(self.name, topic)
|
||||
|
||||
def sendGroupMessage(self, text, meta={}):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
if meta and meta.get("style", None) == "emote":
|
||||
self.account.client.ctcpMakeQuery(self.name, [("ACTION", text)])
|
||||
return succeed(text)
|
||||
# standard shmandard, clients don't support plain escaped newlines!
|
||||
for line in text.split("\n"):
|
||||
self.account.client.say(self.name, line)
|
||||
return succeed(text)
|
||||
|
||||
def leave(self):
|
||||
if self.account.client is None:
|
||||
raise locals.OfflineError
|
||||
self.account.client.leave(self.name)
|
||||
self.account.client.getGroupConversation(self.name, 1)
|
||||
|
||||
|
||||
class IRCProto(basesupport.AbstractClientMixin, irc.IRCClient):
|
||||
def __init__(self, account, chatui, logonDeferred=None):
|
||||
basesupport.AbstractClientMixin.__init__(self, account, chatui, logonDeferred)
|
||||
self._namreplies = {}
|
||||
self._ingroups = {}
|
||||
self._groups = {}
|
||||
self._topics = {}
|
||||
|
||||
def getGroupConversation(self, name, hide=0):
|
||||
name = name.lower()
|
||||
return self.chat.getGroupConversation(
|
||||
self.chat.getGroup(name, self), stayHidden=hide
|
||||
)
|
||||
|
||||
def getPerson(self, name):
|
||||
return self.chat.getPerson(name, self)
|
||||
|
||||
def connectionMade(self):
|
||||
# XXX: Why do I duplicate code in IRCClient.register?
|
||||
try:
|
||||
self.performLogin = True
|
||||
self.nickname = self.account.username
|
||||
self.password = self.account.password
|
||||
self.realname = "Twisted-IM user"
|
||||
|
||||
irc.IRCClient.connectionMade(self)
|
||||
|
||||
for channel in self.account.channels:
|
||||
self.joinGroup(channel)
|
||||
self.account._isOnline = 1
|
||||
if self._logonDeferred is not None:
|
||||
self._logonDeferred.callback(self)
|
||||
self.chat.getContactsList()
|
||||
except BaseException:
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
|
||||
def setNick(self, nick):
|
||||
self.name = nick
|
||||
self.accountName = "%s (IRC)" % nick
|
||||
irc.IRCClient.setNick(self, nick)
|
||||
|
||||
def kickedFrom(self, channel, kicker, message):
|
||||
"""
|
||||
Called when I am kicked from a channel.
|
||||
"""
|
||||
return self.chat.getGroupConversation(self.chat.getGroup(channel[1:], self), 1)
|
||||
|
||||
def userKicked(self, kickee, channel, kicker, message):
|
||||
pass
|
||||
|
||||
def noticed(self, username, channel, message):
|
||||
self.privmsg(username, channel, message, {"dontAutoRespond": 1})
|
||||
|
||||
def privmsg(self, username, channel, message, metadata=None):
|
||||
if metadata is None:
|
||||
metadata = {}
|
||||
username = username.split("!", 1)[0]
|
||||
if username == self.name:
|
||||
return
|
||||
if channel[0] == "#":
|
||||
group = channel[1:]
|
||||
self.getGroupConversation(group).showGroupMessage(
|
||||
username, message, metadata
|
||||
)
|
||||
return
|
||||
self.chat.getConversation(self.getPerson(username)).showMessage(
|
||||
message, metadata
|
||||
)
|
||||
|
||||
def action(self, username, channel, emote):
|
||||
username = username.split("!", 1)[0]
|
||||
if username == self.name:
|
||||
return
|
||||
meta = {"style": "emote"}
|
||||
if channel[0] == "#":
|
||||
group = channel[1:]
|
||||
self.getGroupConversation(group).showGroupMessage(username, emote, meta)
|
||||
return
|
||||
self.chat.getConversation(self.getPerson(username)).showMessage(emote, meta)
|
||||
|
||||
def irc_RPL_NAMREPLY(self, prefix, params):
|
||||
"""
|
||||
RPL_NAMREPLY
|
||||
>> NAMES #bnl
|
||||
<< :Arlington.VA.US.Undernet.Org 353 z3p = #bnl :pSwede Dan-- SkOyg AG
|
||||
"""
|
||||
group = params[2][1:].lower()
|
||||
users = params[3].split()
|
||||
for ui in range(len(users)):
|
||||
while users[ui][0] in ["@", "+"]: # channel modes
|
||||
users[ui] = users[ui][1:]
|
||||
if group not in self._namreplies:
|
||||
self._namreplies[group] = []
|
||||
self._namreplies[group].extend(users)
|
||||
for nickname in users:
|
||||
try:
|
||||
self._ingroups[nickname].append(group)
|
||||
except BaseException:
|
||||
self._ingroups[nickname] = [group]
|
||||
|
||||
def irc_RPL_ENDOFNAMES(self, prefix, params):
|
||||
group = params[1][1:]
|
||||
self.getGroupConversation(group).setGroupMembers(
|
||||
self._namreplies[group.lower()]
|
||||
)
|
||||
del self._namreplies[group.lower()]
|
||||
|
||||
def irc_RPL_TOPIC(self, prefix, params):
|
||||
self._topics[params[1][1:]] = params[2]
|
||||
|
||||
def irc_333(self, prefix, params):
|
||||
group = params[1][1:]
|
||||
self.getGroupConversation(group).setTopic(self._topics[group], params[2])
|
||||
del self._topics[group]
|
||||
|
||||
def irc_TOPIC(self, prefix, params):
|
||||
nickname = prefix.split("!")[0]
|
||||
group = params[0][1:]
|
||||
topic = params[1]
|
||||
self.getGroupConversation(group).setTopic(topic, nickname)
|
||||
|
||||
def irc_JOIN(self, prefix, params):
|
||||
nickname = prefix.split("!")[0]
|
||||
group = params[0][1:].lower()
|
||||
if nickname != self.nickname:
|
||||
try:
|
||||
self._ingroups[nickname].append(group)
|
||||
except BaseException:
|
||||
self._ingroups[nickname] = [group]
|
||||
self.getGroupConversation(group).memberJoined(nickname)
|
||||
|
||||
def irc_PART(self, prefix, params):
|
||||
nickname = prefix.split("!")[0]
|
||||
group = params[0][1:].lower()
|
||||
if nickname != self.nickname:
|
||||
if group in self._ingroups[nickname]:
|
||||
self._ingroups[nickname].remove(group)
|
||||
self.getGroupConversation(group).memberLeft(nickname)
|
||||
|
||||
def irc_QUIT(self, prefix, params):
|
||||
nickname = prefix.split("!")[0]
|
||||
if nickname in self._ingroups:
|
||||
for group in self._ingroups[nickname]:
|
||||
self.getGroupConversation(group).memberLeft(nickname)
|
||||
self._ingroups[nickname] = []
|
||||
|
||||
def irc_NICK(self, prefix, params):
|
||||
fromNick = prefix.split("!")[0]
|
||||
toNick = params[0]
|
||||
if fromNick not in self._ingroups:
|
||||
return
|
||||
for group in self._ingroups[fromNick]:
|
||||
self.getGroupConversation(group).memberChangedNick(fromNick, toNick)
|
||||
self._ingroups[toNick] = self._ingroups[fromNick]
|
||||
del self._ingroups[fromNick]
|
||||
|
||||
def irc_unknown(self, prefix, command, params):
|
||||
pass
|
||||
|
||||
# GTKIM calls
|
||||
def joinGroup(self, name):
|
||||
self.join(name)
|
||||
self.getGroupConversation(name)
|
||||
|
||||
|
||||
@implementer(interfaces.IAccount)
|
||||
class IRCAccount(basesupport.AbstractAccount):
|
||||
gatewayType = "IRC"
|
||||
|
||||
_groupFactory = IRCGroup
|
||||
_personFactory = IRCPerson
|
||||
|
||||
def __init__(
|
||||
self, accountName, autoLogin, username, password, host, port, channels=""
|
||||
):
|
||||
basesupport.AbstractAccount.__init__(
|
||||
self, accountName, autoLogin, username, password, host, port
|
||||
)
|
||||
self.channels = [channel.strip() for channel in channels.split(",")]
|
||||
if self.channels == [""]:
|
||||
self.channels = []
|
||||
|
||||
def _startLogOn(self, chatui):
|
||||
logonDeferred = defer.Deferred()
|
||||
cc = protocol.ClientCreator(reactor, IRCProto, self, chatui, logonDeferred)
|
||||
d = cc.connectTCP(self.host, self.port)
|
||||
d.addErrback(logonDeferred.errback)
|
||||
return logonDeferred
|
||||
|
||||
def logOff(self):
|
||||
# IAccount.logOff
|
||||
pass
|
||||
@@ -0,0 +1,30 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Enum:
|
||||
group: Optional[str] = None
|
||||
|
||||
def __init__(self, label: str) -> None:
|
||||
self.label = label
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.group}: {self.label}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.label
|
||||
|
||||
|
||||
class StatusEnum(Enum):
|
||||
group = "Status"
|
||||
|
||||
|
||||
OFFLINE = Enum("Offline")
|
||||
ONLINE = Enum("Online")
|
||||
AWAY = Enum("Away")
|
||||
|
||||
|
||||
class OfflineError(Exception):
|
||||
"""The requested action can't happen while offline."""
|
||||
@@ -0,0 +1,278 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
L{twisted.words} support for Instance Messenger.
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer, error
|
||||
from twisted.python import log
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.spread import pb
|
||||
from twisted.words.im import basesupport, interfaces
|
||||
from twisted.words.im.locals import AWAY, OFFLINE, ONLINE
|
||||
|
||||
|
||||
class TwistedWordsPerson(basesupport.AbstractPerson):
|
||||
"""I a facade for a person you can talk to through a twisted.words service."""
|
||||
|
||||
def __init__(self, name, wordsAccount):
|
||||
basesupport.AbstractPerson.__init__(self, name, wordsAccount)
|
||||
self.status = OFFLINE
|
||||
|
||||
def isOnline(self):
|
||||
return (self.status == ONLINE) or (self.status == AWAY)
|
||||
|
||||
def getStatus(self):
|
||||
return self.status
|
||||
|
||||
def sendMessage(self, text, metadata):
|
||||
"""Return a deferred..."""
|
||||
if metadata:
|
||||
d = self.account.client.perspective.directMessage(self.name, text, metadata)
|
||||
d.addErrback(self.metadataFailed, "* " + text)
|
||||
return d
|
||||
else:
|
||||
return self.account.client.perspective.callRemote(
|
||||
"directMessage", self.name, text
|
||||
)
|
||||
|
||||
def metadataFailed(self, result, text):
|
||||
print("result:", result, "text:", text)
|
||||
return self.account.client.perspective.directMessage(self.name, text)
|
||||
|
||||
def setStatus(self, status):
|
||||
self.status = status
|
||||
self.chat.getContactsList().setContactStatus(self)
|
||||
|
||||
|
||||
@implementer(interfaces.IGroup)
|
||||
class TwistedWordsGroup(basesupport.AbstractGroup):
|
||||
def __init__(self, name, wordsClient):
|
||||
basesupport.AbstractGroup.__init__(self, name, wordsClient)
|
||||
self.joined = 0
|
||||
|
||||
def sendGroupMessage(self, text, metadata=None):
|
||||
"""Return a deferred."""
|
||||
# for backwards compatibility with older twisted.words servers.
|
||||
if metadata:
|
||||
d = self.account.client.perspective.callRemote(
|
||||
"groupMessage", self.name, text, metadata
|
||||
)
|
||||
d.addErrback(self.metadataFailed, "* " + text)
|
||||
return d
|
||||
else:
|
||||
return self.account.client.perspective.callRemote(
|
||||
"groupMessage", self.name, text
|
||||
)
|
||||
|
||||
def setTopic(self, text):
|
||||
self.account.client.perspective.callRemote(
|
||||
"setGroupMetadata",
|
||||
{"topic": text, "topic_author": self.client.name},
|
||||
self.name,
|
||||
)
|
||||
|
||||
def metadataFailed(self, result, text):
|
||||
print("result:", result, "text:", text)
|
||||
return self.account.client.perspective.callRemote(
|
||||
"groupMessage", self.name, text
|
||||
)
|
||||
|
||||
def joining(self):
|
||||
self.joined = 1
|
||||
|
||||
def leaving(self):
|
||||
self.joined = 0
|
||||
|
||||
def leave(self):
|
||||
return self.account.client.perspective.callRemote("leaveGroup", self.name)
|
||||
|
||||
|
||||
class TwistedWordsClient(pb.Referenceable, basesupport.AbstractClientMixin):
|
||||
"""In some cases, this acts as an Account, since it a source of text
|
||||
messages (multiple Words instances may be on a single PB connection)
|
||||
"""
|
||||
|
||||
def __init__(self, acct, serviceName, perspectiveName, chatui, _logonDeferred=None):
|
||||
self.accountName = "{} ({}:{})".format(
|
||||
acct.accountName,
|
||||
serviceName,
|
||||
perspectiveName,
|
||||
)
|
||||
self.name = perspectiveName
|
||||
print("HELLO I AM A PB SERVICE", serviceName, perspectiveName)
|
||||
self.chat = chatui
|
||||
self.account = acct
|
||||
self._logonDeferred = _logonDeferred
|
||||
|
||||
def getPerson(self, name):
|
||||
return self.chat.getPerson(name, self)
|
||||
|
||||
def getGroup(self, name):
|
||||
return self.chat.getGroup(name, self)
|
||||
|
||||
def getGroupConversation(self, name):
|
||||
return self.chat.getGroupConversation(self.getGroup(name))
|
||||
|
||||
def addContact(self, name):
|
||||
self.perspective.callRemote("addContact", name)
|
||||
|
||||
def remote_receiveGroupMembers(self, names, group):
|
||||
print("received group members:", names, group)
|
||||
self.getGroupConversation(group).setGroupMembers(names)
|
||||
|
||||
def remote_receiveGroupMessage(self, sender, group, message, metadata=None):
|
||||
print("received a group message", sender, group, message, metadata)
|
||||
self.getGroupConversation(group).showGroupMessage(sender, message, metadata)
|
||||
|
||||
def remote_memberJoined(self, member, group):
|
||||
print("member joined", member, group)
|
||||
self.getGroupConversation(group).memberJoined(member)
|
||||
|
||||
def remote_memberLeft(self, member, group):
|
||||
print("member left")
|
||||
self.getGroupConversation(group).memberLeft(member)
|
||||
|
||||
def remote_notifyStatusChanged(self, name, status):
|
||||
self.chat.getPerson(name, self).setStatus(status)
|
||||
|
||||
def remote_receiveDirectMessage(self, name, message, metadata=None):
|
||||
self.chat.getConversation(self.chat.getPerson(name, self)).showMessage(
|
||||
message, metadata
|
||||
)
|
||||
|
||||
def remote_receiveContactList(self, clist):
|
||||
for name, status in clist:
|
||||
self.chat.getPerson(name, self).setStatus(status)
|
||||
|
||||
def remote_setGroupMetadata(self, dict_, groupName):
|
||||
if "topic" in dict_:
|
||||
self.getGroupConversation(groupName).setTopic(
|
||||
dict_["topic"], dict_.get("topic_author", None)
|
||||
)
|
||||
|
||||
def joinGroup(self, name):
|
||||
self.getGroup(name).joining()
|
||||
return self.perspective.callRemote("joinGroup", name).addCallback(
|
||||
self._cbGroupJoined, name
|
||||
)
|
||||
|
||||
def leaveGroup(self, name):
|
||||
self.getGroup(name).leaving()
|
||||
return self.perspective.callRemote("leaveGroup", name).addCallback(
|
||||
self._cbGroupLeft, name
|
||||
)
|
||||
|
||||
def _cbGroupJoined(self, result, name):
|
||||
groupConv = self.chat.getGroupConversation(self.getGroup(name))
|
||||
groupConv.showGroupMessage("sys", "you joined")
|
||||
self.perspective.callRemote("getGroupMembers", name)
|
||||
|
||||
def _cbGroupLeft(self, result, name):
|
||||
print("left", name)
|
||||
groupConv = self.chat.getGroupConversation(self.getGroup(name), 1)
|
||||
groupConv.showGroupMessage("sys", "you left")
|
||||
|
||||
def connected(self, perspective):
|
||||
print("Connected Words Client!", perspective)
|
||||
if self._logonDeferred is not None:
|
||||
self._logonDeferred.callback(self)
|
||||
self.perspective = perspective
|
||||
self.chat.getContactsList()
|
||||
|
||||
|
||||
pbFrontEnds = {"twisted.words": TwistedWordsClient, "twisted.reality": None}
|
||||
|
||||
|
||||
@implementer(interfaces.IAccount)
|
||||
class PBAccount(basesupport.AbstractAccount):
|
||||
gatewayType = "PB"
|
||||
_groupFactory = TwistedWordsGroup
|
||||
_personFactory = TwistedWordsPerson
|
||||
|
||||
def __init__(
|
||||
self, accountName, autoLogin, username, password, host, port, services=None
|
||||
):
|
||||
"""
|
||||
@param username: The name of your PB Identity.
|
||||
@type username: string
|
||||
"""
|
||||
basesupport.AbstractAccount.__init__(
|
||||
self, accountName, autoLogin, username, password, host, port
|
||||
)
|
||||
self.services = []
|
||||
if not services:
|
||||
services = [("twisted.words", "twisted.words", username)]
|
||||
for serviceType, serviceName, perspectiveName in services:
|
||||
self.services.append(
|
||||
[pbFrontEnds[serviceType], serviceName, perspectiveName]
|
||||
)
|
||||
|
||||
def logOn(self, chatui):
|
||||
"""
|
||||
@returns: this breaks with L{interfaces.IAccount}
|
||||
@returntype: DeferredList of L{interfaces.IClient}s
|
||||
"""
|
||||
# Overriding basesupport's implementation on account of the
|
||||
# fact that _startLogOn tends to return a deferredList rather
|
||||
# than a simple Deferred, and we need to do registerAccountClient.
|
||||
if (not self._isConnecting) and (not self._isOnline):
|
||||
self._isConnecting = 1
|
||||
d = self._startLogOn(chatui)
|
||||
d.addErrback(self._loginFailed)
|
||||
|
||||
def registerMany(results):
|
||||
for success, result in results:
|
||||
if success:
|
||||
chatui.registerAccountClient(result)
|
||||
self._cb_logOn(result)
|
||||
else:
|
||||
log.err(result)
|
||||
|
||||
d.addCallback(registerMany)
|
||||
return d
|
||||
else:
|
||||
raise error.ConnectionError("Connection in progress")
|
||||
|
||||
def logOff(self):
|
||||
# IAccount.logOff
|
||||
pass
|
||||
|
||||
def _startLogOn(self, chatui):
|
||||
print("Connecting...", end=" ")
|
||||
d = pb.getObjectAt(self.host, self.port)
|
||||
d.addCallbacks(self._cbConnected, self._ebConnected, callbackArgs=(chatui,))
|
||||
return d
|
||||
|
||||
def _cbConnected(self, root, chatui):
|
||||
print("Connected!")
|
||||
print("Identifying...", end=" ")
|
||||
d = pb.authIdentity(root, self.username, self.password)
|
||||
d.addCallbacks(self._cbIdent, self._ebConnected, callbackArgs=(chatui,))
|
||||
return d
|
||||
|
||||
def _cbIdent(self, ident, chatui):
|
||||
if not ident:
|
||||
print("falsely identified.")
|
||||
return self._ebConnected(
|
||||
Failure(Exception("username or password incorrect"))
|
||||
)
|
||||
print("Identified!")
|
||||
dl = []
|
||||
for handlerClass, sname, pname in self.services:
|
||||
d = defer.Deferred()
|
||||
dl.append(d)
|
||||
handler = handlerClass(self, sname, pname, chatui, d)
|
||||
ident.callRemote("attach", sname, pname, handler).addCallback(
|
||||
handler.connected
|
||||
)
|
||||
return defer.DeferredList(dl)
|
||||
|
||||
def _ebConnected(self, error):
|
||||
print("Not connected.")
|
||||
return error
|
||||
Reference in New Issue
Block a user