1616 Location
1717
1818
19+ def _require_message (func ):
20+ """Decorator which forces the object to have an attached message"""
21+ @utils .wraps (func )
22+ def __ (self , * args , ** kwargs ):
23+ if not hasattr (self , "_message" ) or self ._message is None :
24+ raise RuntimeError ("A message must be attached to this object" )
25+ return func (self , * args , ** kwargs )
26+ return __
27+
28+
29+ class ParsedTextEntity (BaseObject ):
30+ """Telegram API representation of an entity in a text message
31+
32+ This was originally called MessageEntity by Telegram
33+ https://core.telegram.org/bots/api#messageentity
34+ """
35+
36+ required = {
37+ "type" : str ,
38+ "offset" : int ,
39+ "length" : int ,
40+ }
41+ optional = {
42+ "url" : str ,
43+ }
44+ replace_keys = {
45+ "url" : "_url" , # Dynamically implemented
46+
47+ # Private attributes, use the ``text`` one
48+ "offset" : "_offset" ,
49+ "length" : "_length" ,
50+ }
51+
52+
53+ def __init__ (self , data , api = None , message = None ):
54+ super ().__init__ (data , api )
55+
56+ self ._message = message
57+
58+ def __str__ (self ):
59+ return self .text
60+
61+ def __repr__ (self ):
62+ if self ._message is not None :
63+ return '<ParsedTextEntity %s: "%s">' % (self .type , self .text )
64+ else :
65+ return '<ParsedTextEntity %s from %s to %s>' % (
66+ self .type ,
67+ self ._offset ,
68+ self ._offset + self ._length
69+ )
70+
71+ def __len__ (self ):
72+ return self ._length
73+
74+ def set_message (self , message ):
75+ """Set the message instance related to this object"""
76+ self ._message = message
77+
78+ @property
79+ @_require_message
80+ def text (self ):
81+ """Get the text of the message"""
82+ if self ._message .text is None :
83+ raise ValueError ("The message must have a text" )
84+
85+ start = self ._offset
86+ stop = start + self ._length
87+
88+ if stop > len (self ._message .text ):
89+ raise ValueError ("The message is too short!" )
90+
91+ return self ._message .text [start :stop ]
92+
93+ @property
94+ @_require_message
95+ def url (self ):
96+ """Get the URL attached to the message"""
97+ # Use the provided if available
98+ if self ._url is not None :
99+ return self ._url
100+
101+ if self .type == "url" :
102+ # Standard URLs
103+ return self .text
104+ elif self .type == "mention" :
105+ # telegram.me URLs
106+ return "https://telegram.me/%s" % self .text [1 :]
107+ elif self .type == "email" :
108+ # mailto: URL
109+ return "mailto:%s" % self .text
110+ else :
111+ # Sorry!
112+ return None
113+
114+ class ParsedText :
115+ """Collection of ParsedTextEntity.
116+
117+ This is a list-like object, and mimics the List<MessageEntity> Telegram
118+ object, but increases its functionalities.
119+ """
120+
121+ def __init__ (self , data , api = None , message = None ):
122+ self ._api = api
123+ # Accept only list of entites
124+ if not isinstance (data , list ):
125+ raise ValueError ("You must provide a list of ParsedTextEntity" )
126+
127+ # Create ParsedTextEntity instances from the data
128+ self ._original_entities = []
129+ for entity in data :
130+ parsed = ParsedTextEntity (entity , api , message )
131+ self ._original_entities .append (parsed )
132+
133+ # Original entities are separated from the exposed entities because
134+ # plaintext entities are calculated and added to the exposend entities
135+ self ._entities = None
136+
137+ self .set_message (message )
138+
139+ def __repr__ (self ):
140+ return '<ParsedText %s>' % repr (self ._calculate_entities ())
141+
142+ def set_api (self , api ):
143+ """Change the API instance"""
144+ self ._api = api
145+
146+ def set_message (self , message ):
147+ """Change the message instance"""
148+ if message is not None and message .text is None :
149+ raise ValueError ("The message must have some text" )
150+
151+ self ._message = message
152+ for entity in self ._original_entities :
153+ entity .set_message (message )
154+
155+ # Refresh the calculated entities list
156+ self ._entities = None
157+
158+ def serialize (self ):
159+ """Serialize this object"""
160+ result = []
161+ for entity in self ._original_entities :
162+ result .append (entity .serialize ())
163+
164+ return result
165+
166+ @_require_message
167+ def _calculate_entities (self ):
168+ """Calculate the correct list of entities"""
169+ # Return the cached result if possible; the cached result is nullified
170+ # when a new instance of Message is attached
171+ if self ._entities is not None :
172+ return self ._entities
173+
174+ offset = 0
175+ self ._entities = []
176+ for entity in self ._original_entities :
177+ # If there was some text before the current entity, add an extra
178+ # plaintext entity
179+ if offset < entity ._offset :
180+ self ._entities .append (ParsedTextEntity ({
181+ "type" : "plain" ,
182+ "offset" : offset ,
183+ "length" : entity ._offset - offset ,
184+ }, self ._api , self ._message ))
185+
186+ self ._entities .append (entity )
187+ offset = entity ._offset + entity ._length
188+
189+ # Then add the last few bits as plaintext if they're present
190+ if offset < len (self ._message .text ):
191+ self ._entities .append (ParsedTextEntity ({
192+ "type" : "plain" ,
193+ "offset" : offset ,
194+ "length" : len (self ._message .text ) - offset ,
195+ }, self ._api , self ._message ))
196+
197+ return self ._entities
198+
199+ def filter (self , * types , exclude = False ):
200+ """Get only some types of entities"""
201+ result = []
202+ for entity in self ._calculate_entities ():
203+ # If the entity type is in the allowed ones and exclude is False OR
204+ # if the entity type isn't in the allowed ones and exclude is True
205+ if (entity .type in types ) ^ exclude :
206+ result .append (entity )
207+
208+ return result
209+
210+ # Provide a basic list-like interface; you can always mutate this object to
211+ # a list with list(self) if you need more advanced methods
212+
213+ def __iter__ (self ):
214+ return iter (self ._calculate_entities ())
215+
216+ def __getitem__ (self , index ):
217+ return self ._calculate_entities ()[index ]
218+
219+ def __contains__ (self , key ):
220+ # This checks if a given type is in the entities list
221+ return key in (entity .type for entity in self ._entities )
222+
223+
19224class Message (BaseObject , mixins .MessageMixin ):
20225 """Telegram API representation of a message
21226
@@ -35,6 +240,7 @@ def from_(self):
35240 "chat" : Chat ,
36241 }
37242 optional = {
243+ "entities" : ParsedText ,
38244 "forward_from" : User ,
39245 "forward_date" : int ,
40246 "reply_to_message" : _itself ,
@@ -62,8 +268,22 @@ def from_(self):
62268 }
63269 replace_keys = {
64270 "from" : "sender" ,
271+ "entities" : "parsed_text" ,
65272 }
66273
274+ def __init__ (self , data , api = None ):
275+ super ().__init__ (data , api )
276+
277+ # Create the parsed_text instance even if there are no entities in the
278+ # current text
279+ if self .text is not None and self .parsed_text is None :
280+ self .parsed_text = ParsedText ([], api , self )
281+
282+ # Be sure to set this as the Message instance in the parsed text
283+ # The instance is needed to calculate the content of each entity
284+ if self .parsed_text is not None :
285+ self .parsed_text .set_message (self )
286+
67287 @property
68288 @utils .deprecated ("Message.new_chat_participant" , "1.0" ,
69289 "Rename property to Message.new_chat_member" )
0 commit comments