webhooks.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. from dataclasses import asdict, dataclass, field
  2. from typing import TYPE_CHECKING, Dict, List, Optional
  3. import requests
  4. from django.conf import settings
  5. from .constants import RecipientType
  6. if TYPE_CHECKING:
  7. from issues.models import Issue
  8. from .models import Notification
  9. @dataclass
  10. class WebhookAttachmentField:
  11. title: str
  12. value: str
  13. short: bool
  14. @dataclass
  15. class WebhookAttachment:
  16. title: str
  17. title_link: str
  18. text: str
  19. image_url: Optional[str] = None
  20. color: Optional[str] = None
  21. fields: Optional[List[WebhookAttachmentField]] = None
  22. mrkdown_in: Optional[List[str]] = None
  23. @dataclass
  24. class MSTeamsSection:
  25. """
  26. Similar to WebhookAttachment but for MS Teams
  27. https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL
  28. """
  29. activityTitle: str
  30. activitySubtitle: str
  31. @dataclass
  32. class WebhookPayload:
  33. alias: str
  34. text: str
  35. attachments: List[WebhookAttachment]
  36. sections: List[MSTeamsSection]
  37. def send_webhook(
  38. url: str,
  39. message: str,
  40. attachments: Optional[List[WebhookAttachment]] = None,
  41. sections: Optional[List[MSTeamsSection]] = None,
  42. ):
  43. if not attachments:
  44. attachments = []
  45. if not sections:
  46. sections = []
  47. data = WebhookPayload(
  48. alias="GlitchTip", text=message, attachments=attachments, sections=sections
  49. )
  50. return requests.post(url, json=asdict(data), timeout=10)
  51. def send_issue_as_webhook(url, issues: List["Issue"], issue_count: int = 1):
  52. """
  53. Notification about issues via webhook.
  54. url: Webhook URL
  55. issues: This should be only the issues to send as attachment
  56. issue_count - total issues, may be greater than len(issues)
  57. """
  58. attachments: List[WebhookAttachment] = []
  59. sections: List[MSTeamsSection] = []
  60. for issue in issues:
  61. fields = [
  62. WebhookAttachmentField(
  63. title="Project",
  64. value=issue.project.name,
  65. short=True,
  66. )
  67. ]
  68. environment = issue.tags.get("environment")
  69. if environment:
  70. fields.append(
  71. WebhookAttachmentField(
  72. title="Environment",
  73. value=environment[0],
  74. short=True,
  75. )
  76. )
  77. release = issue.tags.get("release")
  78. if release:
  79. fields.append(
  80. WebhookAttachmentField(
  81. title="Release",
  82. value=release[0],
  83. short=False,
  84. )
  85. )
  86. attachments.append(
  87. WebhookAttachment(
  88. mrkdown_in=["text"],
  89. title=str(issue),
  90. title_link=issue.get_detail_url(),
  91. text=issue.culprit,
  92. color=issue.get_hex_color(),
  93. fields=fields,
  94. )
  95. )
  96. sections.append(
  97. MSTeamsSection(
  98. activityTitle=str(issue),
  99. activitySubtitle=f"[View Issue {issue.short_id_display}]({issue.get_detail_url()})",
  100. )
  101. )
  102. message = "GlitchTip Alert"
  103. if issue_count > 1:
  104. message += f" ({issue_count} issues)"
  105. return send_webhook(url, message, attachments, sections)
  106. @dataclass
  107. class DiscordField:
  108. name: str
  109. value: str
  110. inline: bool = False
  111. @dataclass
  112. class DiscordEmbed:
  113. title: str
  114. description: str
  115. color: int
  116. url: str
  117. fields: List[DiscordField]
  118. @dataclass
  119. class DiscordWebhookPayload:
  120. content: str
  121. embeds: List[DiscordEmbed]
  122. def send_issue_as_discord_webhook(url, issues: List["Issue"], issue_count: int = 1):
  123. embeds: List[DiscordEmbed] = []
  124. for issue in issues:
  125. fields = [
  126. DiscordField(
  127. name="Project",
  128. value=issue.project.name,
  129. inline=True,
  130. )
  131. ]
  132. environment = issue.tags.get("environment")
  133. if environment:
  134. fields.append(
  135. DiscordField(
  136. name="Environment",
  137. value=environment[0],
  138. inline=True,
  139. )
  140. )
  141. release = issue.tags.get("release")
  142. if release:
  143. fields.append(
  144. DiscordField(
  145. name="Release",
  146. value=release[0],
  147. inline=False,
  148. )
  149. )
  150. embeds.append(
  151. DiscordEmbed(
  152. title=str(issue),
  153. description=issue.culprit,
  154. color=int(issue.get_hex_color()[1:], 16)
  155. if issue.get_hex_color() is not None
  156. else None,
  157. url=issue.get_detail_url(),
  158. fields=fields,
  159. )
  160. )
  161. message = "GlitchTip Alert"
  162. if issue_count > 1:
  163. message += f" ({issue_count} issues)"
  164. return send_discord_webhook(url, message, embeds)
  165. def send_discord_webhook(url: str, message: str, embeds: List[DiscordEmbed]):
  166. payload = DiscordWebhookPayload(content=message, embeds=embeds)
  167. return requests.post(url, json=asdict(payload), timeout=10)
  168. @dataclass
  169. class GoogleChatCard:
  170. header: Dict = None
  171. sections: List[Dict] = None
  172. def construct_uptime_card(self, title: str, subtitle: str, text: str, url: str):
  173. self.header = dict(
  174. title=title,
  175. subtitle=subtitle,
  176. )
  177. self.sections = [
  178. dict(
  179. widgets=[
  180. dict(
  181. decoratedText=dict(
  182. text=text,
  183. button=dict(
  184. text="View", onClick=dict(openLink=dict(url=url))
  185. ),
  186. )
  187. )
  188. ]
  189. )
  190. ]
  191. return self
  192. def construct_issue_card(self, title: str, issue: "Issue"):
  193. self.header = dict(title=title, subtitle=issue.project.name)
  194. section_header = "<font color='{}'>{}</font>".format(
  195. issue.get_hex_color(), str(issue)
  196. )
  197. widgets = []
  198. widgets.append(dict(decoratedText=dict(topLabel="Culprit", text=issue.culprit)))
  199. environment = issue.tags.get("environment")
  200. if environment:
  201. widgets.append(
  202. dict(decoratedText=dict(topLabel="Environment", text=environment[0]))
  203. )
  204. release = issue.tags.get("release")
  205. if release:
  206. widgets.append(
  207. dict(decoratedText=dict(topLabel="Release", text=release[0]))
  208. )
  209. widgets.append(
  210. dict(
  211. buttonList=dict(
  212. buttons=[
  213. dict(
  214. text="View Issue {}".format(issue.short_id_display),
  215. onClick=dict(openLink=dict(url=issue.get_detail_url())),
  216. )
  217. ]
  218. )
  219. )
  220. )
  221. self.sections = [dict(header=section_header, widgets=widgets)]
  222. return self
  223. @dataclass
  224. class GoogleChatWebhookPayload:
  225. cardsV2: List[Dict[str, GoogleChatCard]] = field(default_factory=list)
  226. def add_card(self, card):
  227. return self.cardsV2.append(dict(cardId="createCardMessage", card=card))
  228. def send_googlechat_webhook(url: str, cards: List[GoogleChatCard]):
  229. """
  230. Send Google Chat compatible message as documented in
  231. https://developers.google.com/chat/messages-overview
  232. """
  233. payload = GoogleChatWebhookPayload()
  234. [payload.add_card(card) for card in cards]
  235. return requests.post(url, json=asdict(payload), timeout=10)
  236. def send_issue_as_googlechat_webhook(url, issues: List["Issue"]):
  237. cards = []
  238. for issue in issues:
  239. card = GoogleChatCard().construct_issue_card(
  240. title="GlitchTip Alert", issue=issue
  241. )
  242. cards.append(card)
  243. return send_googlechat_webhook(url, cards)
  244. def send_webhook_notification(
  245. notification: "Notification", url: str, recipient_type: str
  246. ):
  247. issue_count = notification.issues.count()
  248. issues = notification.issues.all()[: settings.MAX_ISSUES_PER_ALERT]
  249. if recipient_type == RecipientType.DISCORD:
  250. send_issue_as_discord_webhook(url, issues, issue_count)
  251. elif recipient_type == RecipientType.GOOGLE_CHAT:
  252. send_issue_as_googlechat_webhook(url, issues)
  253. else:
  254. send_issue_as_webhook(url, issues, issue_count)