RAHHH
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Instance Messenger, Pan-protocol chat client.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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) -> None:
|
||||
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,362 @@
|
||||
# -*- 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