1# Copyright 2013 The Distro Tracker Developers 

2# See the COPYRIGHT file at the top-level directory of this distribution and 

3# at https://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 https://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""" 

11The module defining common functionality and base classes for all email control 

12commands. 

13""" 

14 

15import re 

16 

17from django.conf import settings 

18from django.core.exceptions import ValidationError 

19from django.core.validators import EmailValidator 

20 

21DISTRO_TRACKER_CONTROL_EMAIL = settings.DISTRO_TRACKER_CONTROL_EMAIL 

22 

23 

24class MetaCommand(type): 

25 """ 

26 Meta class for Distro Tracker Commands. 

27 

28 Transforms the :py:attr:`Command.REGEX_LIST` given in a Command sublclass 

29 to include all aliases of the command. When implementing a 

30 :py:class:`Command` subclass, it is not necessary to include a separate 

31 regex for each command alias or a long one listing every option. 

32 """ 

33 def __init__(cls, name, bases, dct): # noqa 

34 if not getattr(cls, 'META', None): 

35 return 

36 joined_aliases = '|'.join( 

37 alias 

38 for alias in [cls.META['name']] + cls.META.get('aliases', []) 

39 ) 

40 cls.REGEX_LIST = tuple( 

41 '^(?:' + joined_aliases + ')' + regex 

42 for regex in cls.REGEX_LIST 

43 ) 

44 

45 

46class Command(metaclass=MetaCommand): 

47 """ 

48 Base class for commands. Instances of this class can be used for no-op 

49 commands. 

50 """ 

51 __metaclass__ = MetaCommand 

52 

53 META = {} 

54 """ 

55 Meta information about the command, given as key/value pairs. Expected 

56 keys are: 

57 - ``description`` - Description of the command which will be shown in the 

58 help output 

59 - ``name`` - Name of the command. Makes it possible to match command lines 

60 in control messages to command classes since each command line starts 

61 with the name of the command. 

62 - ``aliases`` - List of alternative names for the command 

63 - ``position`` - Preferred position in the help output 

64 """ 

65 REGEX_LIST = () 

66 """ 

67 A list of regular expressions which, when matched to a string, identify 

68 a command. Additionally, any named group in the regular expression should 

69 exactly match the name of the parameter in the constructor of the command. 

70 If unnamed groups are used, their order must be the same as the order of 

71 parameters in the constructor of the command. 

72 This is very similar to how Django handles linking views and URLs. 

73 

74 It is only necessary to list the part of the command's syntax to 

75 capture the parameters, while the name and all aliases given in the META 

76 dict are automatically assumed when matching a string to the command. 

77 """ 

78 

79 def __init__(self, *args): 

80 self._sent_mails = [] 

81 self.out = [] 

82 

83 def __call__(self): 

84 """ 

85 The base class delegates execution to the appropriate :py:meth:`handle` 

86 method and handles the reply. 

87 """ 

88 self.handle() 

89 return self.render_reply() 

90 

91 def handle(self): 

92 """ 

93 Performs the necessary steps to execute the command. 

94 """ 

95 pass 

96 

97 def is_valid(self): 

98 return True 

99 

100 def get_command_text(self, *args): 

101 """ 

102 Returns a string representation of the command. 

103 """ 

104 return ' '.join((self.META.get('name', '#'), ) + args) 

105 

106 @classmethod 

107 def match_line(cls, line): 

108 """ 

109 Class method to check whether the given line matches the command. 

110 

111 :param line: The line to check whether it matches the command. 

112 """ 

113 for pattern in cls.REGEX_LIST: 

114 match = re.match(pattern, line, re.IGNORECASE) 

115 if match: 

116 return match 

117 

118 def render_reply(self): 

119 """ 

120 Returns a string representing the command's reply. 

121 """ 

122 return '\n'.join(self.out) 

123 

124 def reply(self, message, *args): 

125 """ 

126 Adds a message to the command's reply. 

127 

128 :param message: Message to include in the reply 

129 :type message: string 

130 """ 

131 self.out.append(message % args) 

132 

133 def warning(self, message, *args): 

134 """ 

135 Adds a warning to the command's reply. 

136 

137 :param message: Message to include in the reply 

138 :type message: string 

139 """ 

140 self.out.append('Warning: ' + message % args) 

141 

142 def error(self, message, *args): 

143 """ 

144 Adds an error message to the command's reply. 

145 

146 :param message: Message to include in the reply 

147 :type message: string 

148 """ 

149 self.out.append("Error: " + message % args) 

150 

151 def list_reply(self, items, bullet='*'): 

152 """ 

153 Includes a list of items in the reply. Each item is converted to a 

154 string before being output. 

155 

156 :param items: An iterable of items to be included in the form of a list 

157 in the reply. 

158 :param bullet: The character to be used as the "bullet" of the list. 

159 """ 

160 for item in items: 

161 self.reply(bullet + ' ' + str(item)) 

162 

163 @staticmethod 

164 def validate_email(email): 

165 validate = EmailValidator() 

166 try: 

167 validate(email) 

168 return True 

169 except ValidationError: 

170 return False