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.

295 lines
9.9 KiB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. // Package travisci implements a Service capable of processing webhooks from Travis-CI.
  2. package travisci
  3. import (
  4. "encoding/json"
  5. "fmt"
  6. "net/http"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "time"
  11. "github.com/matrix-org/go-neb/database"
  12. "github.com/matrix-org/go-neb/types"
  13. "github.com/matrix-org/gomatrix"
  14. log "github.com/sirupsen/logrus"
  15. )
  16. // ServiceType of the Travis-CI service.
  17. const ServiceType = "travis-ci"
  18. // DefaultTemplate contains the template that will be used if none is supplied.
  19. // This matches the default mentioned at: https://docs.travis-ci.com/user/notifications#Customizing-slack-notifications
  20. const DefaultTemplate = (`%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}
  21. Change view : %{compare_url}
  22. Build details : %{build_url}`)
  23. // Matches 'owner/repo'
  24. var ownerRepoRegex = regexp.MustCompile(`^([A-z0-9-_.]+)/([A-z0-9-_.]+)$`)
  25. var httpClient = &http.Client{}
  26. // Service contains the Config fields for the Travis-CI service.
  27. //
  28. // This service will send notifications into a Matrix room when Travis-CI sends
  29. // webhook events to it. It requires a public domain which Travis-CI can reach.
  30. // Notices will be sent as the service user ID.
  31. //
  32. // Example JSON request:
  33. // {
  34. // rooms: {
  35. // "!ewfug483gsfe:localhost": {
  36. // repos: {
  37. // "matrix-org/go-neb": {
  38. // template: "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}\nBuild details : %{build_url}"
  39. // }
  40. // }
  41. // }
  42. // }
  43. // }
  44. type Service struct {
  45. types.DefaultService
  46. webhookEndpointURL string
  47. // The URL which should be added to .travis.yml - Populated by Go-NEB after Service registration.
  48. WebhookURL string `json:"webhook_url"`
  49. // A map from Matrix room ID to Github-style owner/repo repositories.
  50. Rooms map[string]struct {
  51. // A map of "owner/repo" to configuration information
  52. Repos map[string]struct {
  53. // The template string to use when creating notifications.
  54. //
  55. // This is identical to the format of Slack Notifications for Travis-CI:
  56. // https://docs.travis-ci.com/user/notifications#Customizing-slack-notifications
  57. //
  58. // The following variables are available:
  59. // repository_slug: your GitHub repo identifier (like svenfuchs/minimal)
  60. // repository_name: the slug without the username
  61. // build_number: build number
  62. // build_id: build id
  63. // branch: branch build name
  64. // commit: shortened commit SHA
  65. // author: commit author name
  66. // commit_message: commit message of build
  67. // commit_subject: first line of the commit message
  68. // result: result of build
  69. // message: Travis CI message to the build
  70. // duration: total duration of all builds in the matrix
  71. // elapsed_time: time between build start and finish
  72. // compare_url: commit change view URL
  73. // build_url: URL of the build detail
  74. Template string `json:"template"`
  75. } `json:"repos"`
  76. } `json:"rooms"`
  77. }
  78. // The payload from Travis-CI
  79. type webhookNotification struct {
  80. ID int `json:"id"`
  81. Number string `json:"number"`
  82. Status *int `json:"status"` // 0 (success) or 1 (incomplete/fail).
  83. StartedAt *string `json:"started_at"`
  84. FinishedAt *string `json:"finished_at"`
  85. StatusMessage string `json:"status_message"`
  86. Commit string `json:"commit"`
  87. Branch string `json:"branch"`
  88. Message string `json:"message"`
  89. CompareURL string `json:"compare_url"`
  90. CommittedAt string `json:"committed_at"`
  91. CommitterName string `json:"committer_name"`
  92. CommitterEmail string `json:"committer_email"`
  93. AuthorName string `json:"author_name"`
  94. AuthorEmail string `json:"author_email"`
  95. Type string `json:"type"`
  96. BuildURL string `json:"build_url"`
  97. Repository struct {
  98. Name string `json:"name"`
  99. OwnerName string `json:"owner_name"`
  100. URL string `json:"url"`
  101. } `json:"repository"`
  102. }
  103. // Converts a webhook notification into a map of template var name to value
  104. func notifToTemplate(n webhookNotification) map[string]string {
  105. t := make(map[string]string)
  106. t["repository_slug"] = n.Repository.OwnerName + "/" + n.Repository.Name
  107. t["repository"] = t["repository_slug"] // Deprecated form but still used everywhere in people's templates
  108. t["repository_name"] = n.Repository.Name
  109. t["build_number"] = n.Number
  110. t["build_id"] = strconv.Itoa(n.ID)
  111. t["branch"] = n.Branch
  112. shaLength := len(n.Commit)
  113. if shaLength > 10 {
  114. shaLength = 10
  115. }
  116. t["commit"] = n.Commit[:shaLength] // shortened commit SHA
  117. t["author"] = n.CommitterName // author: commit author name
  118. // commit_message: commit message of build
  119. // commit_subject: first line of the commit message
  120. t["commit_message"] = n.Message
  121. subjAndMsg := strings.SplitN(n.Message, "\n", 2)
  122. t["commit_subject"] = subjAndMsg[0]
  123. if n.Status != nil {
  124. t["result"] = strconv.Itoa(*n.Status)
  125. }
  126. t["message"] = n.StatusMessage // message: Travis CI message to the build
  127. if n.StartedAt != nil && n.FinishedAt != nil {
  128. // duration: total duration of all builds in the matrix -- TODO
  129. // elapsed_time: time between build start and finish
  130. // Example from docs: "2011-11-11T11:11:11Z"
  131. start, err := time.Parse("2006-01-02T15:04:05Z", *n.StartedAt)
  132. finish, err2 := time.Parse("2006-01-02T15:04:05Z", *n.FinishedAt)
  133. if err != nil || err2 != nil {
  134. log.WithFields(log.Fields{
  135. "started_at": *n.StartedAt,
  136. "finished_at": *n.FinishedAt,
  137. }).Warn("Failed to parse Travis-CI start/finish times.")
  138. } else {
  139. t["duration"] = finish.Sub(start).String()
  140. t["elapsed_time"] = t["duration"]
  141. }
  142. }
  143. t["compare_url"] = n.CompareURL
  144. t["build_url"] = n.BuildURL
  145. return t
  146. }
  147. func outputForTemplate(travisTmpl string, tmpl map[string]string) (out string) {
  148. if travisTmpl == "" {
  149. travisTmpl = DefaultTemplate
  150. }
  151. out = travisTmpl
  152. for tmplVar, tmplValue := range tmpl {
  153. out = strings.Replace(out, "%{"+tmplVar+"}", tmplValue, -1)
  154. }
  155. return out
  156. }
  157. // OnReceiveWebhook receives requests from Travis-CI and possibly sends requests to Matrix as a result.
  158. //
  159. // If the repository matches a known Github repository, a notification will be formed from the
  160. // template for that repository and a notice will be sent to Matrix.
  161. //
  162. // Go-NEB cannot register with Travis-CI for webhooks automatically. The user must manually add the
  163. // webhook endpoint URL to their .travis.yml file:
  164. // notifications:
  165. // webhooks: http://go-neb-endpoint.com/notifications
  166. //
  167. // See https://docs.travis-ci.com/user/notifications#Webhook-notifications for more information.
  168. func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *gomatrix.Client) {
  169. if err := req.ParseForm(); err != nil {
  170. log.WithError(err).Error("Failed to read incoming Travis-CI webhook form")
  171. w.WriteHeader(400)
  172. return
  173. }
  174. payload := req.PostFormValue("payload")
  175. if payload == "" {
  176. log.Error("Travis-CI webhook is missing payload= form value")
  177. w.WriteHeader(400)
  178. return
  179. }
  180. if err := verifyOrigin([]byte(payload), req.Header.Get("Signature")); err != nil {
  181. log.WithFields(log.Fields{
  182. "Signature": req.Header.Get("Signature"),
  183. log.ErrorKey: err,
  184. }).Warn("Received unauthorised Travis-CI webhook request.")
  185. w.WriteHeader(403)
  186. return
  187. }
  188. var notif webhookNotification
  189. if err := json.Unmarshal([]byte(payload), &notif); err != nil {
  190. log.WithError(err).Error("Travis-CI webhook received an invalid JSON payload=")
  191. w.WriteHeader(400)
  192. return
  193. }
  194. if notif.Repository.OwnerName == "" || notif.Repository.Name == "" {
  195. log.WithField("repo", notif.Repository).Error("Travis-CI webhook missing repository fields")
  196. w.WriteHeader(400)
  197. return
  198. }
  199. whForRepo := notif.Repository.OwnerName + "/" + notif.Repository.Name
  200. tmplData := notifToTemplate(notif)
  201. logger := log.WithFields(log.Fields{
  202. "repo": whForRepo,
  203. })
  204. for roomID, roomData := range s.Rooms {
  205. for ownerRepo, repoData := range roomData.Repos {
  206. if ownerRepo != whForRepo {
  207. continue
  208. }
  209. msg := gomatrix.TextMessage{
  210. Body: outputForTemplate(repoData.Template, tmplData),
  211. MsgType: "m.notice",
  212. }
  213. logger.WithFields(log.Fields{
  214. "message": msg,
  215. "room_id": roomID,
  216. }).Print("Sending Travis-CI notification to room")
  217. if _, e := cli.SendMessageEvent(roomID, "m.room.message", msg); e != nil {
  218. logger.WithError(e).WithField("room_id", roomID).Print(
  219. "Failed to send Travis-CI notification to room.")
  220. }
  221. }
  222. }
  223. w.WriteHeader(200)
  224. }
  225. // Register makes sure the Config information supplied is valid.
  226. func (s *Service) Register(oldService types.Service, client *gomatrix.Client) error {
  227. s.WebhookURL = s.webhookEndpointURL
  228. for _, roomData := range s.Rooms {
  229. for repo := range roomData.Repos {
  230. match := ownerRepoRegex.FindStringSubmatch(repo)
  231. if len(match) == 0 {
  232. return fmt.Errorf("Repository '%s' is not a valid repository name", repo)
  233. }
  234. }
  235. }
  236. s.joinRooms(client)
  237. return nil
  238. }
  239. // PostRegister deletes this service if there are no registered repos.
  240. func (s *Service) PostRegister(oldService types.Service) {
  241. for _, roomData := range s.Rooms {
  242. for range roomData.Repos {
  243. return // at least 1 repo exists
  244. }
  245. }
  246. // Delete this service since no repos are configured
  247. logger := log.WithFields(log.Fields{
  248. "service_type": s.ServiceType(),
  249. "service_id": s.ServiceID(),
  250. })
  251. logger.Info("Removing service as no repositories are registered.")
  252. if err := database.GetServiceDB().DeleteService(s.ServiceID()); err != nil {
  253. logger.WithError(err).Error("Failed to delete service")
  254. }
  255. }
  256. func (s *Service) joinRooms(client *gomatrix.Client) {
  257. for roomID := range s.Rooms {
  258. if _, err := client.JoinRoom(roomID, "", nil); err != nil {
  259. log.WithFields(log.Fields{
  260. log.ErrorKey: err,
  261. "room_id": roomID,
  262. "user_id": client.UserID,
  263. }).Error("Failed to join room")
  264. }
  265. }
  266. }
  267. func init() {
  268. types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service {
  269. return &Service{
  270. DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType),
  271. webhookEndpointURL: webhookEndpointURL,
  272. }
  273. })
  274. }