中国本土应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

408 lines
15KB

  1. # -*- coding: utf-8 -*-
  2. import os
  3. import base64
  4. import platform
  5. import subprocess
  6. import logging
  7. import time
  8. from pydub import AudioSegment
  9. from datetime import datetime, timedelta
  10. import pytz
  11. from odoo import _, api, fields, models, tools
  12. from odoo.exceptions import UserError, ValidationError, Warning
  13. from odoo.addons.oec_im_wecom_api.api.wecom_abstract_api import ApiException
  14. from requests_toolbelt.multipart.encoder import MultipartEncoder
  15. _logger = logging.getLogger(__name__)
  16. extensions_and_size = {
  17. "image": {"extensions": [".jpg", ".png"], "size": [5, 2 * 1024 * 1024]},
  18. "voice": {"extensions": [".amr"], "size": [5, 2 * 1024 * 1024], "duration": 60},
  19. "video": {"extensions": [".mp4"], "size": [5, 10 * 1024 * 1024]},
  20. "file": {"extensions": [], "size": [5, 20 * 1024 * 1024]},
  21. }
  22. class WeComMaterial(models.Model):
  23. "Template for sending WeCom message"
  24. _name = "wecom.material"
  25. _description = "WeCom material"
  26. _order = "company_id,name"
  27. company_id = fields.Many2one(
  28. "res.company",
  29. string="Company",
  30. domain="[('is_wecom_organization', '=', True)]",
  31. copy=False,
  32. store=True,
  33. )
  34. name = fields.Char("Name", required=True, translate=True,)
  35. media_type = fields.Selection(
  36. [
  37. ("image", "Picture"),
  38. ("voice", "Voice"),
  39. ("video", "Video"),
  40. ("file", "Ordinary file"),
  41. ],
  42. string="Media file type",
  43. required=True,
  44. default="image",
  45. )
  46. temporary = fields.Boolean(string="Temporary material", default=False)
  47. img_url = fields.Char(
  48. string="Picture URL",
  49. readonly=True,
  50. help="The URL of the picture obtained after uploading. Permanently valid.",
  51. copy=False,
  52. )
  53. media_id = fields.Char(
  54. string="Media file identification",
  55. readonly=True,
  56. help="The unique identification obtained after uploading the media file is valid for 3 days",
  57. copy=False,
  58. )
  59. created_at = fields.Datetime(
  60. string="Upload time",
  61. readonly=True,
  62. help="Media file upload timestamp (Beijing time)",
  63. copy=False,
  64. )
  65. media_file = fields.Binary(
  66. string="Media files",
  67. help="Picture file size should be between 5B and 2MB;"
  68. "The voice file format supports AMR, and the file size should be between 5B and 2MB"
  69. "The video file shall not exceed 10m, the file format: MP4, and the file size shall be between 5B and 10MB"
  70. "Normal file size shall not exceed 20m",
  71. store=True,
  72. required=True,
  73. )
  74. media_filename = fields.Char()
  75. _sql_constraints = [
  76. (
  77. "name_company_uniq",
  78. "unique (name, company_id)",
  79. "The material name of each company must be unique !",
  80. ),
  81. ]
  82. @api.onchange("media_type")
  83. def _onchange_media_type(self):
  84. if self.media_type != "image":
  85. self.temporary = True
  86. else:
  87. self.temporary = False
  88. @api.onchange("media_file")
  89. def _onchange_media_file(self):
  90. if self.media_file:
  91. self._check_file_size_and_extension(
  92. self.media_type, self.media_file, self.media_filename
  93. )
  94. def upload_media(self):
  95. if self.img_url:
  96. # 存在 上传后得到的永久有效图片URL。
  97. raise UserError(
  98. _(
  99. "Already uploaded, please do not upload again! You can create a new record to upload the file."
  100. )
  101. )
  102. if self.created_at:
  103. # 存在 媒体文件上传时间戳(北京时间)
  104. created_time = self.created_at
  105. overdue = self.env["wecomapi.tools.datetime"].cheeck_days_overdue(
  106. created_time, 3
  107. )
  108. if overdue:
  109. pass
  110. else:
  111. raise UserError(
  112. _(
  113. "The temporary material has not expired, please do not upload it repeatedly."
  114. )
  115. )
  116. if self.media_file:
  117. # 存在媒体文件
  118. sys_params = self.env["ir.config_parameter"].sudo()
  119. if self.company_id:
  120. file_path = self._check_file_path(
  121. self.media_file, "material", self.media_filename
  122. )
  123. if self.temporary:
  124. """
  125. 素材上传得到media_id,该media_id仅三天内有效
  126. media_id在同一企业内应用之间可以共享
  127. """
  128. try:
  129. multipart_encoder = MultipartEncoder(
  130. fields={
  131. self.media_filename: (
  132. "file",
  133. open(file_path, "rb"),
  134. "text/plain",
  135. )
  136. },
  137. )
  138. headers = {"Content-Type": multipart_encoder.content_type}
  139. wxapi = self.env["wecom.service_api"].InitServiceApi(
  140. self.company_id.corpid,
  141. self.company_id.material_app_id.secret,
  142. )
  143. response = wxapi.httpPostFile(
  144. self.env["wecom.service_api_list"].get_server_api_call(
  145. "MEDIA_UPLOAD"
  146. ),
  147. {"type": self.media_type},
  148. multipart_encoder,
  149. headers,
  150. )
  151. if response["errcode"] == 0:
  152. self.media_id = response["media_id"]
  153. timeStamp = int(response["created_at"])
  154. timeArray = time.localtime(timeStamp)
  155. self.created_at = time.strftime(
  156. "%Y-%m-%d %H:%M:%S", timeArray
  157. )
  158. except ApiException as ex:
  159. self.env["wecomapi.tools.action"].ApiExceptionDialog(
  160. ex, ex, raise_exception=True
  161. )
  162. else:
  163. """
  164. 上传图片得到图片URL,该URL永久有效
  165. 返回的图片URL,仅能用于图文消息正文中的图片展示,或者给客户发送欢迎语等;若用于非企业微信环境下的页面,图片将被屏蔽。
  166. 每个企业每天最多可上传100张图片
  167. """
  168. try:
  169. wxapi = self.env["wecom.service_api"].InitServiceApi(
  170. self.company_id.corpid,
  171. self.company_id.material_app_id.secret,
  172. )
  173. # files = {"media": open(file_path, "rb")}
  174. multipart_encoder = MultipartEncoder(
  175. fields={
  176. self.media_filename: (
  177. "file",
  178. open(file_path, "rb"),
  179. "text/plain",
  180. )
  181. },
  182. )
  183. headers = {"Content-Type": multipart_encoder.content_type}
  184. response = wxapi.httpPostFile(
  185. self.env["wecom.service_api_list"].get_server_api_call(
  186. "MEDIA_UPLOADIMG"
  187. ),
  188. {"type": self.media_type},
  189. multipart_encoder,
  190. headers,
  191. )
  192. if response["errcode"] == 0:
  193. self.img_url = response["url"]
  194. self.created_at = fields.Datetime.now()
  195. except ApiException as ex:
  196. return self.env["wecomapi.tools.action"].ApiExceptionDialog(
  197. ex, raise_exception=True
  198. )
  199. else:
  200. raise UserError(_("Please upload files!"))
  201. return self
  202. @api.model
  203. def create(self, vals):
  204. if vals.get("media_type") and vals.get("media_file"):
  205. # 检查文件的大小和格式,语音文件检查时长
  206. self._check_file_size_and_extension(
  207. vals.get("media_type"),
  208. vals.get("media_file"),
  209. vals.get("media_filename"),
  210. )
  211. # print(vals.get("media_type"),vals.get("media_file"),vals.get("media_filename"))
  212. # 检查文件路径
  213. self._check_file_path(
  214. vals.get("media_file"), "material", vals.get("media_filename")
  215. )
  216. material = super(WeComMaterial, self).create(vals)
  217. return material
  218. def write(self, vals):
  219. # if vals.get("media_id") or vals.get("img_url"):
  220. # raise UserError(
  221. # _(
  222. # "Already uploaded, please do not upload again! You can create a new record to upload the file."
  223. # )
  224. # )
  225. res = super(WeComMaterial, self).write(vals)
  226. if vals.get("media_type") and vals.get("media_file"):
  227. # 检查文件的大小和格式,语音文件检查时长
  228. self._check_file_size_and_extension(
  229. vals.get("media_type"),
  230. vals.get("media_file"),
  231. vals.get("media_filename"),
  232. )
  233. # 检查文件路径
  234. self._check_file_path(
  235. vals.get("media_file"), "material", vals.get("media_filename")
  236. )
  237. return res
  238. @api.returns("self", lambda value: value.id)
  239. def copy(self, default=None):
  240. if default:
  241. if not default.get("company_id"):
  242. pass
  243. return super(WeComMaterial, self).copy(default=default)
  244. @api.model
  245. def _check_material_file_expiration(self):
  246. """[summary]
  247. 检查素材文件是否过期
  248. Args:
  249. material ([type]): [description]
  250. Returns:
  251. [type]: [description]
  252. """
  253. # media_id = ""
  254. if self.created_at:
  255. # 有创建日期
  256. created_time = self.created_at
  257. MAX_FAIL_TIME = 3
  258. # 检查是否超过3天
  259. overdue = self.env["wecomapi.tools.datetime"].cheeck_days_overdue(
  260. created_time, MAX_FAIL_TIME
  261. )
  262. if overdue:
  263. # 临时素材超期,重新上传
  264. self.upload_media()
  265. # media_id = self.media_id
  266. else:
  267. # 无创建日期,执行第一次上传临时素材
  268. self.upload_media()
  269. # media_id = self.media_id
  270. return self.media_id
  271. @api.model
  272. def _check_file_path(self, file, subpath, filename):
  273. sys_params = self.env["ir.config_parameter"].sudo()
  274. path = sys_params.get_param("wecom.resources_path")
  275. if path:
  276. pass
  277. else:
  278. raise UserError(_("WeCom storage path has not been configured yet!"))
  279. file_path = self.env["wecomapi.tools.file"].path_is_exists(path, subpath)
  280. full_path = file_path + filename
  281. if not os.path.exists(full_path):
  282. try:
  283. with open(full_path, "wb") as fp:
  284. fp.write(base64.b64decode(file))
  285. fp.close()
  286. # return full_path
  287. except IOError:
  288. raise UserError(_("Error saving file to path %s!"), full_path)
  289. if platform.system() == "Windows":
  290. full_path = full_path.replace("/", "\\")
  291. return full_path
  292. @api.model
  293. def _check_file_size_and_extension(self, filetype, file, filename):
  294. file_extension_list = []
  295. file_size_list = []
  296. file_extension = os.path.splitext(filename)[1]
  297. file_size = int(len(file) * 3 / 4) # 以字节为单位计算file_size
  298. if filetype == "image":
  299. file_extension_list = extensions_and_size["image"]["extensions"]
  300. file_size_list = extensions_and_size["image"]["size"]
  301. if filetype == "voice":
  302. file_extension_list = extensions_and_size["voice"]["extensions"]
  303. file_size_list = extensions_and_size["voice"]["size"]
  304. # 语音文件先上传到本地文件夹进行时长检查
  305. file_path = self._check_file_path(file, "material", filename)
  306. duration, path = self.get_amr_duration(file_path)
  307. if int(duration) > 60:
  308. # 语音文件时长超过了60秒,删除mp3文件
  309. os.remove(os.path.abspath(path))
  310. raise ValidationError(
  311. _("The duration of the voice file exceeds 60 seconds!")
  312. )
  313. if filetype == "video":
  314. file_extension_list = extensions_and_size["video"]["extensions"]
  315. file_size_list = extensions_and_size["video"]["size"]
  316. if filetype == "file":
  317. file_extension_list = extensions_and_size["file"]["extensions"]
  318. file_size_list = extensions_and_size["file"]["size"]
  319. if filetype is None:
  320. raise ValidationError(_("Unknown type of media file!"))
  321. if len(file_extension_list) > 0 and file_extension not in file_extension_list:
  322. raise ValidationError(
  323. _("Allowed file formats are %s")
  324. % (" or ".join(str(x) for x in file_extension_list))
  325. )
  326. # if file_size_list[0] != 0:
  327. if file_size > file_size_list[1] or file_size_list[1] < file_size_list[0]:
  328. raise ValidationError(
  329. _("Media file size must be between %sB and %sMB")
  330. % (file_size_list[0], file_size_list[1] / 1024 / 1024)
  331. )
  332. # elif file_size > file_size_list[1]:
  333. # raise ValidationError(
  334. # _("Media file size cannot be larger than %sMB")
  335. # % (file_size_list[1] / 1024 / 1024)
  336. # )
  337. def get_amr_duration(self, filepath):
  338. """
  339. 获取.amr语音文件的时长
  340. """
  341. path = os.path.split(filepath)[0]
  342. filename = os.path.split(filepath)[1].split(".")[0]
  343. mp3_audio_filepath = os.path.join(path, filename + ".mp3")
  344. mp3_transformat_path = self.amr_transformat_mp3(
  345. os.path.abspath(filepath), mp3_audio_filepath
  346. )
  347. if os.path.exists(mp3_transformat_path):
  348. mp3_audio = AudioSegment.from_file(
  349. os.path.abspath(mp3_transformat_path), format="mp3"
  350. )
  351. return mp3_audio.duration_seconds, mp3_transformat_path
  352. @classmethod
  353. def amr_transformat_mp3(self, amr_path, mp3_path=None):
  354. path, name = os.path.split(amr_path)
  355. if name.split(".")[-1] != "amr":
  356. print("not a amr file")
  357. return 0
  358. if mp3_path is None or mp3_path.split(".")[-1] != "mp3":
  359. mp3_path = os.path.join(path, name + ".mp3")
  360. error = subprocess.call(["ffmpeg", "-i", amr_path, mp3_path])
  361. if error:
  362. _logger.info("[Convert Error]:Convert file-%s to mp3 failed" % amr_path)
  363. return 0
  364. return mp3_path
上海开阖软件有限公司 沪ICP备12045867号-1