basesupport.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. #
  4. """Instance Messenger base classes for protocol support.
  5. You will find these useful if you're adding a new protocol to IM.
  6. """
  7. from typing import Type
  8. from twisted.internet import error
  9. from twisted.internet.protocol import Protocol, connectionDone
  10. from twisted.persisted import styles
  11. from twisted.python.failure import Failure
  12. from twisted.python.reflect import prefixedMethods
  13. from twisted.words.im.locals import OFFLINE, OfflineError
  14. # Abstract representation of chat "model" classes
  15. class AbstractGroup:
  16. def __init__(self, name, account):
  17. self.name = name
  18. self.account = account
  19. def getGroupCommands(self):
  20. """finds group commands
  21. these commands are methods on me that start with imgroup_; they are
  22. called with no arguments
  23. """
  24. return prefixedMethods(self, "imgroup_")
  25. def getTargetCommands(self, target):
  26. """finds group commands
  27. these commands are methods on me that start with imgroup_; they are
  28. called with a user present within this room as an argument
  29. you may want to override this in your group in order to filter for
  30. appropriate commands on the given user
  31. """
  32. return prefixedMethods(self, "imtarget_")
  33. def join(self):
  34. if not self.account.client:
  35. raise OfflineError
  36. self.account.client.joinGroup(self.name)
  37. def leave(self):
  38. if not self.account.client:
  39. raise OfflineError
  40. self.account.client.leaveGroup(self.name)
  41. def __repr__(self) -> str:
  42. return f"<{self.__class__} {self.name!r}>"
  43. def __str__(self) -> str:
  44. return f"{self.name}@{self.account.accountName}"
  45. class AbstractPerson:
  46. def __init__(self, name, baseAccount):
  47. self.name = name
  48. self.account = baseAccount
  49. self.status = OFFLINE
  50. def getPersonCommands(self):
  51. """finds person commands
  52. these commands are methods on me that start with imperson_; they are
  53. called with no arguments
  54. """
  55. return prefixedMethods(self, "imperson_")
  56. def getIdleTime(self):
  57. """
  58. Returns a string.
  59. """
  60. return "--"
  61. def __repr__(self) -> str:
  62. return f"<{self.__class__} {self.name!r}/{self.status}>"
  63. def __str__(self) -> str:
  64. return f"{self.name}@{self.account.accountName}"
  65. class AbstractClientMixin:
  66. """Designed to be mixed in to a Protocol implementing class.
  67. Inherit from me first.
  68. @ivar _logonDeferred: Fired when I am done logging in.
  69. """
  70. _protoBase: Type[Protocol] = None # type: ignore[assignment]
  71. def __init__(self, account, chatui, logonDeferred):
  72. for base in self.__class__.__bases__:
  73. if issubclass(base, Protocol):
  74. self.__class__._protoBase = base
  75. break
  76. else:
  77. pass
  78. self.account = account
  79. self.chat = chatui
  80. self._logonDeferred = logonDeferred
  81. def connectionMade(self):
  82. self._protoBase.connectionMade(self)
  83. def connectionLost(self, reason: Failure = connectionDone) -> None:
  84. self.account._clientLost(self, reason)
  85. self.unregisterAsAccountClient()
  86. return self._protoBase.connectionLost(self, reason) # type: ignore[arg-type]
  87. def unregisterAsAccountClient(self):
  88. """Tell the chat UI that I have `signed off'."""
  89. self.chat.unregisterAccountClient(self)
  90. class AbstractAccount(styles.Versioned):
  91. """Base class for Accounts.
  92. I am the start of an implementation of L{IAccount<interfaces.IAccount>}, I
  93. implement L{isOnline} and most of L{logOn}, though you'll need to implement
  94. L{_startLogOn} in a subclass.
  95. @cvar _groupFactory: A Callable that will return a L{IGroup} appropriate
  96. for this account type.
  97. @cvar _personFactory: A Callable that will return a L{IPerson} appropriate
  98. for this account type.
  99. @type _isConnecting: boolean
  100. @ivar _isConnecting: Whether I am in the process of establishing a
  101. connection to the server.
  102. @type _isOnline: boolean
  103. @ivar _isOnline: Whether I am currently on-line with the server.
  104. @ivar accountName:
  105. @ivar autoLogin:
  106. @ivar username:
  107. @ivar password:
  108. @ivar host:
  109. @ivar port:
  110. """
  111. _isOnline = 0
  112. _isConnecting = 0
  113. client = None
  114. _groupFactory = AbstractGroup
  115. _personFactory = AbstractPerson
  116. persistanceVersion = 2
  117. def __init__(self, accountName, autoLogin, username, password, host, port):
  118. self.accountName = accountName
  119. self.autoLogin = autoLogin
  120. self.username = username
  121. self.password = password
  122. self.host = host
  123. self.port = port
  124. self._groups = {}
  125. self._persons = {}
  126. def upgrateToVersion2(self):
  127. # Added in CVS revision 1.16.
  128. for k in ("_groups", "_persons"):
  129. if not hasattr(self, k):
  130. setattr(self, k, {})
  131. def __getstate__(self):
  132. state = styles.Versioned.__getstate__(self)
  133. for k in ("client", "_isOnline", "_isConnecting"):
  134. try:
  135. del state[k]
  136. except KeyError:
  137. pass
  138. return state
  139. def isOnline(self):
  140. return self._isOnline
  141. def logOn(self, chatui):
  142. """Log on to this account.
  143. Takes care to not start a connection if a connection is
  144. already in progress. You will need to implement
  145. L{_startLogOn} for this to work, and it would be a good idea
  146. to override L{_loginFailed} too.
  147. @returntype: Deferred L{interfaces.IClient}
  148. """
  149. if (not self._isConnecting) and (not self._isOnline):
  150. self._isConnecting = 1
  151. d = self._startLogOn(chatui)
  152. d.addCallback(self._cb_logOn)
  153. # if chatui is not None:
  154. # (I don't particularly like having to pass chatUI to this function,
  155. # but we haven't factored it out yet.)
  156. d.addCallback(chatui.registerAccountClient)
  157. d.addErrback(self._loginFailed)
  158. return d
  159. else:
  160. raise error.ConnectError("Connection in progress")
  161. def getGroup(self, name):
  162. """Group factory.
  163. @param name: Name of the group on this account.
  164. @type name: string
  165. """
  166. group = self._groups.get(name)
  167. if group is None:
  168. group = self._groupFactory(name, self)
  169. self._groups[name] = group
  170. return group
  171. def getPerson(self, name):
  172. """Person factory.
  173. @param name: Name of the person on this account.
  174. @type name: string
  175. """
  176. person = self._persons.get(name)
  177. if person is None:
  178. person = self._personFactory(name, self)
  179. self._persons[name] = person
  180. return person
  181. def _startLogOn(self, chatui):
  182. """Start the sign on process.
  183. Factored out of L{logOn}.
  184. @returntype: Deferred L{interfaces.IClient}
  185. """
  186. raise NotImplementedError()
  187. def _cb_logOn(self, client):
  188. self._isConnecting = 0
  189. self._isOnline = 1
  190. self.client = client
  191. return client
  192. def _loginFailed(self, reason):
  193. """Errorback for L{logOn}.
  194. @type reason: Failure
  195. @returns: I{reason}, for further processing in the callback chain.
  196. @returntype: Failure
  197. """
  198. self._isConnecting = 0
  199. self._isOnline = 0 # just in case
  200. return reason
  201. def _clientLost(self, client, reason):
  202. self.client = None
  203. self._isConnecting = 0
  204. self._isOnline = 0
  205. return reason
  206. def __repr__(self) -> str:
  207. return "<{}: {} ({}@{}:{})>".format(
  208. self.__class__,
  209. self.accountName,
  210. self.username,
  211. self.host,
  212. self.port,
  213. )