misc.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. # Copyright 2013-2015 The Distro Tracker Developers
  2. # See the COPYRIGHT file at the top-level directory of this distribution and
  3. # at http://deb.li/DTAuthors
  4. #
  5. # This file is part of Distro Tracker. It is subject to the license terms
  6. # in the LICENSE file found in the top-level directory of this
  7. # distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
  8. # including this file, may be copied, modified, propagated, or distributed
  9. # except according to the terms contained in the LICENSE file.
  10. """
  11. Implementation of miscellaneous commands.
  12. """
  13. from __future__ import unicode_literals
  14. from distro_tracker.core.utils import get_or_none
  15. from distro_tracker.core.utils import distro_tracker_render_to_string
  16. from distro_tracker.core.models import Subscription, UserEmail, EmailSettings, \
  17. PackageName, BinaryPackageName
  18. from distro_tracker.core.models import SourcePackageName, PseudoPackageName
  19. from distro_tracker.mail.control.commands.confirmation import needs_confirmation
  20. from distro_tracker.mail.control.commands.base import Command
  21. from django.core.exceptions import ValidationError
  22. from django.conf import settings
  23. DISTRO_TRACKER_FQDN = settings.DISTRO_TRACKER_FQDN
  24. @needs_confirmation
  25. class SubscribeCommand(Command):
  26. """
  27. A command which subscribes a user to a package so that he receives that
  28. package's email messages.
  29. .. note::
  30. This command requires confirmation.
  31. """
  32. META = {
  33. 'description': """subscribe <srcpackage> [<email>]
  34. Subscribes <email> to all messages regarding <srcpackage>. If
  35. <email> is not given, it subscribes the From address. If the
  36. <srcpackage> is not a valid source package, you'll get a warning.
  37. If it's a valid binary package, the mapping will automatically be
  38. done for you.""",
  39. 'name': 'subscribe',
  40. 'position': 1,
  41. }
  42. REGEX_LIST = (
  43. r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+))?$',
  44. )
  45. def __init__(self, package, email):
  46. super(SubscribeCommand, self).__init__()
  47. self.package = package
  48. self.user_email = email
  49. def get_command_text(self):
  50. return super(SubscribeCommand, self).get_command_text(
  51. self.package, self.user_email)
  52. def pre_confirm(self):
  53. """
  54. Implementation of a hook method which is executed instead of
  55. :py:meth:`handle` when the command is not confirmed.
  56. """
  57. settings = get_or_none(EmailSettings,
  58. user_email__email__iexact=self.user_email)
  59. if settings and settings.is_subscribed_to(self.package):
  60. self.warn('{email} is already subscribed to {package}'.format(
  61. email=self.user_email,
  62. package=self.package))
  63. return False
  64. if not SourcePackageName.objects.exists_with_name(self.package):
  65. if BinaryPackageName.objects.exists_with_name(self.package):
  66. binary_package = \
  67. BinaryPackageName.objects.get_by_name(self.package)
  68. self.warn('{package} is not a source package.'.format(
  69. package=self.package))
  70. self.reply('{package} is the source package '
  71. 'for the {binary} binary package'.format(
  72. package=binary_package.main_source_package_name,
  73. binary=binary_package.name))
  74. self.package = binary_package.main_source_package_name.name
  75. else:
  76. self.warn(
  77. '{package} is neither a source package '
  78. 'nor a binary package.'.format(package=self.package))
  79. if PseudoPackageName.objects.exists_with_name(self.package):
  80. self.warn('Package {package} is a pseudo package.'.format(
  81. package=self.package))
  82. else:
  83. self.warn('Package {package} is not even a pseudo '
  84. 'package.'.format(package=self.package))
  85. try:
  86. Subscription.objects.create_for(
  87. email=self.user_email,
  88. package_name=self.package,
  89. active=False)
  90. except ValidationError as e:
  91. self.warn(e.message)
  92. return False
  93. self.reply('A confirmation mail has been sent to ' + self.user_email)
  94. return True
  95. def handle(self):
  96. subscription = Subscription.objects.create_for(
  97. package_name=self.package,
  98. email=self.user_email,
  99. active=True)
  100. if subscription:
  101. self.reply('{email} has been subscribed to {package}'.format(
  102. email=self.user_email, package=self.package))
  103. else:
  104. self.error('Could not subscribe {email} to {package}'.format(
  105. email=self.user_email, package=self.package))
  106. def get_confirmation_message(self):
  107. """
  108. :returns: A message giving additional information about subscribing to
  109. a package.
  110. :rtype: string
  111. """
  112. return distro_tracker_render_to_string(
  113. 'control/email-subscription-confirmation.txt', {
  114. 'package': self.package,
  115. }
  116. )
  117. @needs_confirmation
  118. class UnsubscribeCommand(Command):
  119. """
  120. Command which unsubscribes the user from a package so that he no longer
  121. receives any email messages regarding this package.
  122. .. note::
  123. This command requires confirmation.
  124. """
  125. META = {
  126. 'description': """unsubscribe <srcpackage> [<email>]
  127. Unsubscribes <email> from <srcpackage>. Like the subscribe command,
  128. it will use the From address if <email> is not given.""",
  129. 'name': 'unsubscribe',
  130. 'position': 2,
  131. }
  132. REGEX_LIST = (
  133. r'\s+(?P<package>\S+)(?:\s+(?P<email>\S+))?$',
  134. )
  135. def __init__(self, package, email):
  136. super(UnsubscribeCommand, self).__init__()
  137. self.package = package
  138. self.user_email = email
  139. def get_command_text(self):
  140. return super(UnsubscribeCommand, self).get_command_text(
  141. self.package, self.user_email)
  142. def pre_confirm(self):
  143. """
  144. Implementation of a hook method which is executed instead of
  145. :py:meth:`handle` when the command is not confirmed.
  146. """
  147. if not SourcePackageName.objects.exists_with_name(self.package):
  148. if BinaryPackageName.objects.exists_with_name(self.package):
  149. binary_package = \
  150. BinaryPackageName.objects.get_by_name(self.package)
  151. self.warn('{package} is not a source package.'.format(
  152. package=self.package))
  153. self.reply('{package} is the source package '
  154. 'for the {binary} binary package'.format(
  155. package=binary_package.main_source_package_name,
  156. binary=binary_package.name))
  157. self.package = binary_package.main_source_package_name.name
  158. else:
  159. self.warn(
  160. '{package} is neither a source package '
  161. 'nor a binary package.'.format(package=self.package))
  162. settings = get_or_none(EmailSettings,
  163. user_email__email__iexact=self.user_email)
  164. if not settings or not settings.is_subscribed_to(self.package):
  165. self.error(
  166. "{email} is not subscribed, you can't unsubscribe.".format(
  167. email=self.user_email)
  168. )
  169. return False
  170. self.reply('A confirmation mail has been sent to ' + self.user_email)
  171. return True
  172. def handle(self):
  173. success = Subscription.objects.unsubscribe(self.package,
  174. self.user_email)
  175. if success:
  176. self.reply('{user} has been unsubscribed from {package}'.format(
  177. user=self.user_email,
  178. package=self.package))
  179. else:
  180. self.error('Could not unsubscribe {email} from {package}'.format(
  181. email=self.user_email,
  182. package=self.package))
  183. def get_confirmation_message(self):
  184. """
  185. :returns: A message giving additional information about unsubscribing
  186. from a package.
  187. :rtype: string
  188. """
  189. return distro_tracker_render_to_string(
  190. 'control/email-unsubscribe-confirmation.txt', {
  191. 'package': self.package,
  192. }
  193. )
  194. class WhichCommand(Command):
  195. """
  196. A command which returns a list of packages to which the given user is
  197. subscribed to.
  198. """
  199. META = {
  200. 'description': """which [<email>]
  201. Tells you which packages <email> is subscribed to.""",
  202. 'name': 'which',
  203. 'position': 4,
  204. }
  205. REGEX_LIST = (
  206. r'(?:\s+(?P<email>\S+))?$',
  207. )
  208. def __init__(self, email):
  209. super(WhichCommand, self).__init__()
  210. self.user_email = email
  211. def get_command_text(self):
  212. return super(WhichCommand, self).get_command_text(self.user_email)
  213. def handle(self):
  214. user_subscriptions = Subscription.objects.get_for_email(
  215. self.user_email)
  216. if not user_subscriptions:
  217. self.reply('No subscriptions found')
  218. return
  219. self.list_reply(sub.package for sub in user_subscriptions)
  220. class WhoCommand(Command):
  221. """
  222. A command which returns a list of users which are subscribed to the given
  223. package.
  224. """
  225. META = {
  226. 'description': """who <package>
  227. Outputs all the subscriber emails for the given package in
  228. an obfuscated form.""",
  229. 'name': 'who',
  230. 'position': 5,
  231. }
  232. REGEX_LIST = (
  233. r'(?:\s+(?P<package>\S+))$',
  234. )
  235. def __init__(self, package):
  236. super(WhoCommand, self).__init__()
  237. self.package_name = package
  238. def get_command_text(self):
  239. return super(WhoCommand, self).get_command_text(self.package_name)
  240. def handle(self):
  241. package = get_or_none(PackageName, name=self.package_name)
  242. if not package:
  243. self.error('Package {package} does not exist'.format(
  244. package=self.package_name))
  245. return
  246. if package.subscriptions.count() == 0:
  247. self.reply(
  248. 'Package {package} does not have any subscribers'.format(
  249. package=package.name))
  250. return
  251. self.reply(
  252. "Here's the list of subscribers to package {package}:".format(
  253. package=self.package_name))
  254. self.list_reply(
  255. self.obfuscate(subscriber)
  256. for subscriber in package.subscriptions.all()
  257. )
  258. def obfuscate(self, user_email):
  259. """
  260. Helper method which obfuscates the given email.
  261. :param user_email: The user whose email should be obfuscated.
  262. :type user_email:
  263. :py:class:`UserEmail <distro_tracker.core.models.UserEmail>`
  264. :returns: An obfuscated email address of the given user.
  265. :rtype: string
  266. """
  267. email = user_email.email
  268. local_part, domain = email.rsplit('@', 1)
  269. domain_parts = domain.split('.')
  270. obfuscated_domain = '.'.join(
  271. part[0] + '.' * (len(part) - 1)
  272. for part in domain_parts
  273. )
  274. return local_part + '@' + obfuscated_domain
  275. class QuitCommand(Command):
  276. """
  277. When this command is executed, the processing of further commands should
  278. stop.
  279. """
  280. META = {
  281. 'description': '''quit
  282. Stops processing commands''',
  283. 'name': 'quit',
  284. 'aliases': ['thanks', '--'],
  285. 'position': 6
  286. }
  287. REGEX_LIST = (
  288. r'$',
  289. )
  290. def handle(self):
  291. self.reply('Stopping processing here.')
  292. @needs_confirmation
  293. class UnsubscribeallCommand(Command):
  294. """
  295. Command which unsubscribes the user from all packages so that he no longer
  296. receives any email messages regarding any packages.
  297. .. note::
  298. This command requires confirmation.
  299. """
  300. META = {
  301. 'description': '''unsubscribeall [<email>]
  302. Cancel all subscriptions of <email>. Like the subscribe command,
  303. it will use the From address if <email> is not given.''',
  304. 'name': 'unsubscribeall',
  305. 'position': 7,
  306. }
  307. REGEX_LIST = (
  308. r'(?:\s+(?P<email>\S+))?$',
  309. )
  310. def __init__(self, email):
  311. super(UnsubscribeallCommand, self).__init__()
  312. self.user_email = email
  313. def get_command_text(self):
  314. return super(UnsubscribeallCommand, self).get_command_text(
  315. self.user_email)
  316. def pre_confirm(self):
  317. """
  318. Implementation of a hook method which is executed instead of
  319. :py:meth:`handle` when the command is not confirmed.
  320. """
  321. settings = get_or_none(EmailSettings,
  322. user_email__email__iexact=self.user_email)
  323. if not settings or settings.subscription_set.count() == 0:
  324. self.warn('User {email} is not subscribed to any packages'.format(
  325. email=self.user_email))
  326. return False
  327. self.reply('A confirmation mail has been sent to {email}'.format(
  328. email=self.user_email))
  329. return True
  330. def handle(self):
  331. user = get_or_none(UserEmail, email__iexact=self.user_email)
  332. email_settings = get_or_none(EmailSettings, user_email=user)
  333. if user is None or email_settings is None:
  334. return
  335. packages = [
  336. subscription.package.name
  337. for subscription in email_settings.subscription_set.all()
  338. ]
  339. email_settings.unsubscribe_all()
  340. self.reply('All your subscriptions have been terminated:')
  341. self.list_reply(
  342. '{email} has been unsubscribed from {package}@{fqdn}'.format(
  343. email=self.user_email,
  344. package=package,
  345. fqdn=DISTRO_TRACKER_FQDN)
  346. for package in sorted(packages))
  347. def get_confirmation_message(self):
  348. """
  349. :returns: A message giving additional information about unsubscribing
  350. from all packages.
  351. :rtype: string
  352. """
  353. return distro_tracker_render_to_string(
  354. 'control/email-unsubscribeall-confirmation.txt'
  355. )