fields.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. # -*- coding: utf-8 -*-
  2. """
  3. wechatpy.fields
  4. ~~~~~~~~~~~~~~~~
  5. This module defines some useful field types for parse WeChat messages
  6. :copyright: (c) 2014 by messense.
  7. :license: MIT, see LICENSE for more details.
  8. """
  9. from __future__ import absolute_import, unicode_literals
  10. import time
  11. import datetime
  12. import base64
  13. import copy
  14. import six
  15. from library import to_text, to_binary, timezone, ObjectDict
  16. default_timezone = timezone('Asia/Shanghai')
  17. class FieldDescriptor(object):
  18. def __init__(self, field):
  19. self.field = field
  20. self.attr_name = field.name
  21. def __get__(self, instance, instance_type=None):
  22. if instance is not None:
  23. value = instance._data.get(self.attr_name)
  24. if value is None:
  25. value = copy.deepcopy(self.field.default)
  26. instance._data[self.attr_name] = value
  27. if isinstance(value, dict):
  28. value = ObjectDict(value)
  29. if value and not isinstance(value, (dict, list, tuple)) and \
  30. six.callable(self.field.converter):
  31. value = self.field.converter(value)
  32. return value
  33. return self.field
  34. def __set__(self, instance, value):
  35. instance._data[self.attr_name] = value
  36. class BaseField(object):
  37. converter = None
  38. def __init__(self, name, default=None):
  39. self.name = name
  40. self.default = default
  41. def to_xml(self, value):
  42. raise NotImplementedError()
  43. @classmethod
  44. def from_xml(cls, value):
  45. raise NotImplementedError()
  46. def __repr__(self):
  47. _repr = '{klass}({name})'.format(
  48. klass=self.__class__.__name__,
  49. name=repr(self.name)
  50. )
  51. if six.PY2:
  52. return to_binary(_repr)
  53. else:
  54. return to_text(_repr)
  55. def add_to_class(self, klass, name):
  56. self.klass = klass
  57. klass._fields[name] = self
  58. setattr(klass, name, FieldDescriptor(self))
  59. class StringField(BaseField):
  60. def __to_text(self, value):
  61. return to_text(value)
  62. converter = __to_text
  63. def to_xml(self, value):
  64. value = self.converter(value)
  65. tpl = '<{name}><![CDATA[{value}]]></{name}>'
  66. return tpl.format(name=self.name, value=value)
  67. @classmethod
  68. def from_xml(cls, value):
  69. return value
  70. class IntegerField(BaseField):
  71. converter = int
  72. def to_xml(self, value):
  73. value = self.converter(value) if value is not None else self.default
  74. tpl = '<{name}>{value}</{name}>'
  75. return tpl.format(name=self.name, value=value)
  76. @classmethod
  77. def from_xml(cls, value):
  78. return cls.converter(value)
  79. class DateTimeField(BaseField):
  80. def __converter(self, value):
  81. v = int(value)
  82. return datetime.datetime.fromtimestamp(v, tz=default_timezone)
  83. converter = __converter
  84. def to_xml(self, value):
  85. value = time.mktime(datetime.datetime.timetuple(value))
  86. value = int(value)
  87. tpl = '<{name}>{value}</{name}>'
  88. return tpl.format(name=self.name, value=value)
  89. @classmethod
  90. def from_xml(cls, value):
  91. return cls.converter(None, value)
  92. class FloatField(BaseField):
  93. converter = float
  94. def to_xml(self, value):
  95. value = self.converter(value) if value is not None else self.default
  96. tpl = '<{name}>{value}</{name}>'
  97. return tpl.format(name=self.name, value=value)
  98. @classmethod
  99. def from_xml(cls, value):
  100. return cls.converter(value)
  101. class ImageField(StringField):
  102. def to_xml(self, value):
  103. value = self.converter(value)
  104. tpl = """<Image>
  105. <MediaId><![CDATA[{value}]]></MediaId>
  106. </Image>"""
  107. return tpl.format(value=value)
  108. @classmethod
  109. def from_xml(cls, value):
  110. return value["MediaId"]
  111. class VoiceField(StringField):
  112. def to_xml(self, value):
  113. value = self.converter(value)
  114. tpl = """<Voice>
  115. <MediaId><![CDATA[{value}]]></MediaId>
  116. </Voice>"""
  117. return tpl.format(value=value)
  118. @classmethod
  119. def from_xml(cls, value):
  120. return value["MediaId"]
  121. class VideoField(StringField):
  122. def to_xml(self, value):
  123. kwargs = dict(media_id=self.converter(value['media_id']))
  124. content = '<MediaId><![CDATA[{media_id}]]></MediaId>'
  125. if 'title' in value:
  126. kwargs['title'] = self.converter(value['title'])
  127. content += '<Title><![CDATA[{title}]]></Title>'
  128. if 'description' in value:
  129. kwargs['description'] = self.converter(value['description'])
  130. content += '<Description><![CDATA[{description}]]></Description>'
  131. tpl = """<Video>
  132. {content}
  133. </Video>""".format(content=content)
  134. return tpl.format(**kwargs)
  135. @classmethod
  136. def from_xml(cls, value):
  137. rv = dict(media_id=value['MediaId'])
  138. if 'Title' in value:
  139. rv["title"] = value['Title']
  140. if 'Description' in value:
  141. rv['description'] = value['Description']
  142. return rv
  143. class MusicField(StringField):
  144. def to_xml(self, value):
  145. kwargs = dict(thumb_media_id=self.converter(value['thumb_media_id']))
  146. content = '<ThumbMediaId><![CDATA[{thumb_media_id}]]></ThumbMediaId>'
  147. if 'title' in value:
  148. kwargs['title'] = self.converter(value['title'])
  149. content += '<Title><![CDATA[{title}]]></Title>'
  150. if 'description' in value:
  151. kwargs['description'] = self.converter(value['description'])
  152. content += '<Description><![CDATA[{description}]]></Description>'
  153. if 'music_url' in value:
  154. kwargs['music_url'] = self.converter(value['music_url'])
  155. content += '<MusicUrl><![CDATA[{music_url}]]></MusicUrl>'
  156. if 'hq_music_url' in value:
  157. kwargs['hq_music_url'] = self.converter(value['hq_music_url'])
  158. content += '<HQMusicUrl><![CDATA[{hq_music_url}]]></HQMusicUrl>'
  159. tpl = """<Music>
  160. {content}
  161. </Music>""".format(content=content)
  162. return tpl.format(**kwargs)
  163. @classmethod
  164. def from_xml(cls, value):
  165. rv = dict(thumb_media_id=value['ThumbMediaId'])
  166. if 'Title' in value:
  167. rv['title'] = value['Title']
  168. if 'Description' in value:
  169. rv['description'] = value['Description']
  170. if 'MusicUrl' in value:
  171. rv['music_url'] = value['MusicUrl']
  172. if 'HQMusicUrl' in value:
  173. rv['hq_music_url'] = value['HQMusicUrl']
  174. return rv
  175. class ArticlesField(StringField):
  176. def to_xml(self, articles):
  177. article_count = len(articles)
  178. items = []
  179. for article in articles:
  180. title = self.converter(article.get('title', ''))
  181. description = self.converter(article.get('description', ''))
  182. image = self.converter(article.get('image', ''))
  183. url = self.converter(article.get('url', ''))
  184. item_tpl = """<item>
  185. <Title><![CDATA[{title}]]></Title>
  186. <Description><![CDATA[{description}]]></Description>
  187. <PicUrl><![CDATA[{image}]]></PicUrl>
  188. <Url><![CDATA[{url}]]></Url>
  189. </item>"""
  190. item = item_tpl.format(
  191. title=title,
  192. description=description,
  193. image=image,
  194. url=url
  195. )
  196. items.append(item)
  197. items_str = '\n'.join(items)
  198. tpl = """<ArticleCount>{article_count}</ArticleCount>
  199. <Articles>{items}</Articles>"""
  200. return tpl.format(
  201. article_count=article_count,
  202. items=items_str
  203. )
  204. @classmethod
  205. def from_xml(cls, value):
  206. return [dict(
  207. title=item["Title"],
  208. description=item["Description"],
  209. image=item["PicUrl"],
  210. url=item["Url"]
  211. ) for item in value["item"]]
  212. class Base64EncodeField(StringField):
  213. def __base64_encode(self, text):
  214. return to_text(base64.b64encode(to_binary(text)))
  215. converter = __base64_encode
  216. class Base64DecodeField(StringField):
  217. def __base64_decode(self, text):
  218. return to_text(base64.b64decode(to_binary(text)))
  219. converter = __base64_decode
  220. class HardwareField(StringField):
  221. def to_xml(self, value=None):
  222. value = value or {'view': 'myrank', 'action': 'ranklist'}
  223. tpl = """<{name}>
  224. <MessageView><![CDATA[{view}]]></MessageView>
  225. <MessageAction><![CDATA[{action}]]></MessageAction>
  226. </{name}>"""
  227. return tpl.format(
  228. name=self.name,
  229. view=value.get('view'),
  230. action=value.get('action')
  231. )