diff --git a/github-tools.cabal b/github-tools.cabal index 056b45f..6d6ee13 100644 --- a/github-tools.cabal +++ b/github-tools.cabal @@ -80,14 +80,16 @@ executable webservice , aeson , bytestring , case-insensitive + , cryptohash , github >= 0.15.0 - , github-types - , github-webhook-handler , http-types , servant , servant-server , text - , uuid-types + , time + , unordered-containers + , uuid + , vector , wai , wai-extra , warp diff --git a/web/GitHub/Types.hs b/web/GitHub/Types.hs new file mode 100644 index 0000000..2f8e5f4 --- /dev/null +++ b/web/GitHub/Types.hs @@ -0,0 +1,8 @@ +module GitHub.Types + ( module GitHub.Types.Base + , module GitHub.Types.Events + ) where + + +import GitHub.Types.Base +import GitHub.Types.Events diff --git a/web/GitHub/Types/Base.hs b/web/GitHub/Types/Base.hs new file mode 100644 index 0000000..c3ff338 --- /dev/null +++ b/web/GitHub/Types/Base.hs @@ -0,0 +1,1557 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module GitHub.Types.Base where + + +import Control.Applicative ((<$>), (<*>), (<|>)) + +import Data.Aeson (FromJSON (..), ToJSON (..), object) +import Data.Aeson.Types (Value (..), (.:), (.:?), (.=)) +import Data.Text (Text) + + + +------------------------------------------------------------------------------ +-- IssueComment + +data IssueComment = IssueComment + { issueCommentBody :: Text + , issueCommentUrl :: Text + , issueCommentUser :: User + , issueCommentUpdatedAt :: DateTime + , issueCommentCreatedAt :: DateTime + , issueCommentId :: Int + , issueCommentIssueUrl :: Text + , issueCommentHtmlUrl :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON IssueComment where + parseJSON (Object x) = IssueComment + <$> x .: "body" + <*> x .: "url" + <*> x .: "user" + <*> x .: "updated_at" + <*> x .: "created_at" + <*> x .: "id" + <*> x .: "issue_url" + <*> x .: "html_url" + + parseJSON _ = fail "IssueComment" + + +instance ToJSON IssueComment where + toJSON IssueComment{..} = object + [ "body" .= issueCommentBody + , "url" .= issueCommentUrl + , "user" .= issueCommentUser + , "updated_at" .= issueCommentUpdatedAt + , "created_at" .= issueCommentCreatedAt + , "id" .= issueCommentId + , "issue_url" .= issueCommentIssueUrl + , "html_url" .= issueCommentHtmlUrl + ] + + + +------------------------------------------------------------------------------ +-- Milestone + +data Milestone = Milestone + { milestoneCreator :: User + , milestoneClosedIssues :: Int + , milestoneState :: Text + , milestoneDueOn :: Text + , milestoneUrl :: Text + , milestoneUpdatedAt :: DateTime + , milestoneCreatedAt :: DateTime + , milestoneId :: Int + , milestoneTitle :: Text + , milestoneClosedAt :: Maybe DateTime + , milestoneNumber :: Int + , milestoneDescription :: Text + , milestoneLabelsUrl :: Text + , milestoneHtmlUrl :: Text + , milestoneOpenIssues :: Int + } deriving (Eq, Show, Read) + + +instance FromJSON Milestone where + parseJSON (Object x) = Milestone + <$> x .: "creator" + <*> x .: "closed_issues" + <*> x .: "state" + <*> x .: "due_on" + <*> x .: "url" + <*> x .: "updated_at" + <*> x .: "created_at" + <*> x .: "id" + <*> x .: "title" + <*> x .: "closed_at" + <*> x .: "number" + <*> x .: "description" + <*> x .: "labels_url" + <*> x .: "html_url" + <*> x .: "open_issues" + + parseJSON _ = fail "Milestone" + + +instance ToJSON Milestone where + toJSON Milestone{..} = object + [ "creator" .= milestoneCreator + , "closed_issues" .= milestoneClosedIssues + , "state" .= milestoneState + , "due_on" .= milestoneDueOn + , "url" .= milestoneUrl + , "updated_at" .= milestoneUpdatedAt + , "created_at" .= milestoneCreatedAt + , "id" .= milestoneId + , "title" .= milestoneTitle + , "closed_at" .= milestoneClosedAt + , "number" .= milestoneNumber + , "description" .= milestoneDescription + , "labels_url" .= milestoneLabelsUrl + , "html_url" .= milestoneHtmlUrl + , "open_issues" .= milestoneOpenIssues + ] + + + +------------------------------------------------------------------------------ +-- Label + +data Label = Label + { labelColor :: Text + , labelDefault :: Bool + , labelUrl :: Text + , labelName :: Text + , labelId :: Int + } deriving (Eq, Show, Read) + + +instance FromJSON Label where + parseJSON (Object x) = Label + <$> x .: "color" + <*> x .: "default" + <*> x .: "url" + <*> x .: "name" + <*> x .: "id" + + parseJSON _ = fail "Label" + + +instance ToJSON Label where + toJSON Label{..} = object + [ "color" .= labelColor + , "default" .= labelDefault + , "url" .= labelUrl + , "name" .= labelName + , "id" .= labelId + ] + + + +------------------------------------------------------------------------------ +-- PullRequestRef + +data PullRequestRef = PullRequestRef + { pullRequestRefUrl :: Text + , pullRequestRefDiffUrl :: Text + , pullRequestRefPatchUrl :: Text + , pullRequestRefHtmlUrl :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON PullRequestRef where + parseJSON (Object x) = PullRequestRef + <$> x .: "url" + <*> x .: "diff_url" + <*> x .: "patch_url" + <*> x .: "html_url" + + parseJSON _ = fail "PullRequestRef" + + +instance ToJSON PullRequestRef where + toJSON PullRequestRef{..} = object + [ "url" .= pullRequestRefUrl + , "diff_url" .= pullRequestRefDiffUrl + , "patch_url" .= pullRequestRefPatchUrl + , "html_url" .= pullRequestRefHtmlUrl + ] + + + +------------------------------------------------------------------------------ +-- Change + +data Change = Change + { changesFrom :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON Change where + parseJSON (Object x) = Change + <$> x .: "from" + + parseJSON _ = fail "Change" + + +instance ToJSON Change where + toJSON Change{..} = object + [ "from" .= changesFrom + ] + + + +------------------------------------------------------------------------------ +-- Changes + +data Changes = Changes + { changesTitle :: Maybe Change + , changesBody :: Maybe Change + } deriving (Eq, Show, Read) + + +instance FromJSON Changes where + parseJSON (Object x) = Changes + <$> x .:? "title" + <*> x .:? "body" + + parseJSON _ = fail "Changes" + + +instance ToJSON Changes where + toJSON Changes{..} = object + [ "title" .= changesTitle + , "body" .= changesBody + ] + + + +------------------------------------------------------------------------------ +-- DateTime + +data DateTime + = DateTimeStamp Int + | DateTimeText Text + deriving (Eq, Show, Read) + + +instance FromJSON DateTime where + parseJSON x = + DateTimeStamp <$> parseJSON x + <|> DateTimeText <$> parseJSON x + + +instance ToJSON DateTime where + toJSON (DateTimeStamp x) = toJSON x + toJSON (DateTimeText x) = toJSON x + + + +------------------------------------------------------------------------------ +-- RepoOwner + +data RepoOwner + = RepoOwnerUser User + | RepoOwnerUserRef UserRef + deriving (Eq, Show, Read) + + +instance FromJSON RepoOwner where + parseJSON x = + RepoOwnerUser <$> parseJSON x + <|> RepoOwnerUserRef <$> parseJSON x + + +instance ToJSON RepoOwner where + toJSON (RepoOwnerUser x) = toJSON x + toJSON (RepoOwnerUserRef x) = toJSON x + + + +------------------------------------------------------------------------------ +-- Review + +data Review = Review + { reviewId :: Int + , reviewUser :: User + , reviewBody :: Text + , reviewSubmittedAt :: DateTime + , reviewState :: Text + , reviewHtmlUrl :: Text + , reviewPullRequestUrl :: Text + , reviewLinks :: ReviewLinks + } deriving (Eq, Show, Read) + + +instance FromJSON Review where + parseJSON (Object x) = Review + <$> x .: "id" + <*> x .: "user" + <*> x .: "body" + <*> x .: "submitted_at" + <*> x .: "state" + <*> x .: "html_url" + <*> x .: "pull_request_url" + <*> x .: "_links" + + parseJSON _ = fail "Review" + + +instance ToJSON Review where + toJSON Review{..} = object + [ "id" .= reviewId + , "user" .= reviewUser + , "body" .= reviewBody + , "submitted_at" .= reviewSubmittedAt + , "state" .= reviewState + , "html_url" .= reviewHtmlUrl + , "pull_request_url" .= reviewPullRequestUrl + , "_links" .= reviewLinks + ] + + + +------------------------------------------------------------------------------ +-- Link + +data Link = Link + { linkHref :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON Link where + parseJSON (Object x) = Link + <$> x .: "href" + + parseJSON _ = fail "Link" + + +instance ToJSON Link where + toJSON Link{..} = object + [ "href" .= linkHref + ] + + + +------------------------------------------------------------------------------ +-- ReviewLinks + +data ReviewLinks = ReviewLinks + { reviewLinksHtml :: Link + , reviewLinksPullRequest :: Link + } deriving (Eq, Show, Read) + + +instance FromJSON ReviewLinks where + parseJSON (Object x) = ReviewLinks + <$> x .: "html" + <*> x .: "pull_request" + + parseJSON _ = fail "ReviewLinks" + + +instance ToJSON ReviewLinks where + toJSON ReviewLinks{..} = object + [ "html" .= reviewLinksHtml + , "pull_request" .= reviewLinksPullRequest + ] + + + +------------------------------------------------------------------------------ +-- PullRequestLinks + +data PullRequestLinks = PullRequestLinks + { pullRequestLinksSelf :: Link + , pullRequestLinksCommits :: Link + , pullRequestLinksStatuses :: Link + , pullRequestLinksReviewComments :: Link + , pullRequestLinksHtml :: Link + , pullRequestLinksComments :: Link + , pullRequestLinksReviewComment :: Link + , pullRequestLinksIssue :: Link + } deriving (Eq, Show, Read) + + +instance FromJSON PullRequestLinks where + parseJSON (Object x) = PullRequestLinks + <$> x .: "self" + <*> x .: "commits" + <*> x .: "statuses" + <*> x .: "review_comments" + <*> x .: "html" + <*> x .: "comments" + <*> x .: "review_comment" + <*> x .: "issue" + + parseJSON _ = fail "PullRequestLinks" + + +instance ToJSON PullRequestLinks where + toJSON PullRequestLinks{..} = object + [ "self" .= pullRequestLinksSelf + , "commits" .= pullRequestLinksCommits + , "statuses" .= pullRequestLinksStatuses + , "review_comments" .= pullRequestLinksReviewComments + , "html" .= pullRequestLinksHtml + , "comments" .= pullRequestLinksComments + , "review_comment" .= pullRequestLinksReviewComment + , "issue" .= pullRequestLinksIssue + ] + + + +------------------------------------------------------------------------------ +-- Issue + +data Issue = Issue + { issueState :: Text + , issueAssignees :: [User] + , issueLocked :: Bool + , issueBody :: Text + , issueUrl :: Text + , issuePullRequest :: Maybe PullRequestRef + , issueMilestone :: Maybe Milestone + , issueAssignee :: Maybe User + , issueUser :: User + , issueCommentsUrl :: Text + , issueUpdatedAt :: DateTime + , issueCreatedAt :: DateTime + , issueId :: Int + , issueLabels :: [Label] + , issueComments :: Int + , issueTitle :: Text + , issueClosedAt :: Maybe DateTime + , issueNumber :: Int + , issueEventsUrl :: Text + , issueRepositoryUrl :: Text + , issueLabelsUrl :: Text + , issueHtmlUrl :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON Issue where + parseJSON (Object x) = Issue + <$> x .: "state" + <*> x .: "assignees" + <*> x .: "locked" + <*> x .: "body" + <*> x .: "url" + <*> x .:? "pull_request" + <*> x .: "milestone" + <*> x .: "assignee" + <*> x .: "user" + <*> x .: "comments_url" + <*> x .: "updated_at" + <*> x .: "created_at" + <*> x .: "id" + <*> x .: "labels" + <*> x .: "comments" + <*> x .: "title" + <*> x .: "closed_at" + <*> x .: "number" + <*> x .: "events_url" + <*> x .: "repository_url" + <*> x .: "labels_url" + <*> x .: "html_url" + + parseJSON _ = fail "Issue" + + +instance ToJSON Issue where + toJSON Issue{..} = object + [ "state" .= issueState + , "assignees" .= issueAssignees + , "locked" .= issueLocked + , "body" .= issueBody + , "url" .= issueUrl + , "pull_request" .= issuePullRequest + , "milestone" .= issueMilestone + , "assignee" .= issueAssignee + , "user" .= issueUser + , "comments_url" .= issueCommentsUrl + , "updated_at" .= issueUpdatedAt + , "created_at" .= issueCreatedAt + , "id" .= issueId + , "labels" .= issueLabels + , "comments" .= issueComments + , "title" .= issueTitle + , "closed_at" .= issueClosedAt + , "number" .= issueNumber + , "events_url" .= issueEventsUrl + , "repository_url" .= issueRepositoryUrl + , "labels_url" .= issueLabelsUrl + , "html_url" .= issueHtmlUrl + ] + + + +------------------------------------------------------------------------------ +-- SimplePullRequest + +data SimplePullRequest = SimplePullRequest + { simplePullRequestState :: Text + , simplePullRequestReviewCommentUrl :: Text + , simplePullRequestAssignees :: [User] + , simplePullRequestLocked :: Bool + , simplePullRequestBase :: Commit + , simplePullRequestBody :: Text + , simplePullRequestHead :: Commit + , simplePullRequestUrl :: Text + , simplePullRequestMilestone :: Maybe Milestone + , simplePullRequestStatusesUrl :: Text + , simplePullRequestMergedAt :: Maybe DateTime + , simplePullRequestCommitsUrl :: Text + , simplePullRequestAssignee :: Maybe User + , simplePullRequestDiffUrl :: Text + , simplePullRequestUser :: User + , simplePullRequestCommentsUrl :: Text + , simplePullRequestLinks :: PullRequestLinks + , simplePullRequestUpdatedAt :: DateTime + , simplePullRequestPatchUrl :: Text + , simplePullRequestCreatedAt :: DateTime + , simplePullRequestId :: Int + , simplePullRequestIssueUrl :: Text + , simplePullRequestTitle :: Text + , simplePullRequestClosedAt :: Maybe DateTime + , simplePullRequestNumber :: Int + , simplePullRequestMergeCommitSha :: Maybe Text + , simplePullRequestReviewCommentsUrl :: Text + , simplePullRequestHtmlUrl :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON SimplePullRequest where + parseJSON (Object x) = SimplePullRequest + <$> x .: "state" + <*> x .: "review_comment_url" + <*> x .: "assignees" + <*> x .: "locked" + <*> x .: "base" + <*> x .: "body" + <*> x .: "head" + <*> x .: "url" + <*> x .: "milestone" + <*> x .: "statuses_url" + <*> x .: "merged_at" + <*> x .: "commits_url" + <*> x .: "assignee" + <*> x .: "diff_url" + <*> x .: "user" + <*> x .: "comments_url" + <*> x .: "_links" + <*> x .: "updated_at" + <*> x .: "patch_url" + <*> x .: "created_at" + <*> x .: "id" + <*> x .: "issue_url" + <*> x .: "title" + <*> x .: "closed_at" + <*> x .: "number" + <*> x .: "merge_commit_sha" + <*> x .: "review_comments_url" + <*> x .: "html_url" + + parseJSON _ = fail "SimplePullRequest" + + +instance ToJSON SimplePullRequest where + toJSON SimplePullRequest{..} = object + [ "state" .= simplePullRequestState + , "review_comment_url" .= simplePullRequestReviewCommentUrl + , "assignees" .= simplePullRequestAssignees + , "locked" .= simplePullRequestLocked + , "base" .= simplePullRequestBase + , "body" .= simplePullRequestBody + , "head" .= simplePullRequestHead + , "url" .= simplePullRequestUrl + , "milestone" .= simplePullRequestMilestone + , "statuses_url" .= simplePullRequestStatusesUrl + , "merged_at" .= simplePullRequestMergedAt + , "commits_url" .= simplePullRequestCommitsUrl + , "assignee" .= simplePullRequestAssignee + , "diff_url" .= simplePullRequestDiffUrl + , "user" .= simplePullRequestUser + , "comments_url" .= simplePullRequestCommentsUrl + , "_links" .= simplePullRequestLinks + , "updated_at" .= simplePullRequestUpdatedAt + , "patch_url" .= simplePullRequestPatchUrl + , "created_at" .= simplePullRequestCreatedAt + , "id" .= simplePullRequestId + , "issue_url" .= simplePullRequestIssueUrl + , "title" .= simplePullRequestTitle + , "closed_at" .= simplePullRequestClosedAt + , "number" .= simplePullRequestNumber + , "merge_commit_sha" .= simplePullRequestMergeCommitSha + , "review_comments_url" .= simplePullRequestReviewCommentsUrl + , "html_url" .= simplePullRequestHtmlUrl + ] + + + +------------------------------------------------------------------------------ +-- PullRequest + +data PullRequest = PullRequest + { pullRequestMerged :: Bool + , pullRequestAdditions :: Int + , pullRequestState :: Text + , pullRequestMergeableState :: Text + , pullRequestReviewCommentUrl :: Text + , pullRequestMergeable :: Maybe Bool + , pullRequestAssignees :: [User] + , pullRequestLocked :: Bool + , pullRequestBase :: Commit + , pullRequestBody :: Text + , pullRequestHead :: Commit + , pullRequestUrl :: Text + , pullRequestMilestone :: Maybe Milestone + , pullRequestStatusesUrl :: Text + , pullRequestMergedAt :: Maybe DateTime + , pullRequestCommitsUrl :: Text + , pullRequestAssignee :: Maybe User + , pullRequestDiffUrl :: Text + , pullRequestUser :: User + , pullRequestCommentsUrl :: Text + , pullRequestLinks :: PullRequestLinks + , pullRequestUpdatedAt :: DateTime + , pullRequestDeletions :: Int + , pullRequestCommits :: Int + , pullRequestPatchUrl :: Text + , pullRequestCreatedAt :: DateTime + , pullRequestReviewComments :: Int + , pullRequestId :: Int + , pullRequestIssueUrl :: Text + , pullRequestComments :: Int + , pullRequestMergedBy :: Maybe User + , pullRequestTitle :: Text + , pullRequestClosedAt :: Maybe DateTime + , pullRequestChangedFiles :: Int + , pullRequestNumber :: Int + , pullRequestMergeCommitSha :: Maybe Text + , pullRequestReviewCommentsUrl :: Text + , pullRequestHtmlUrl :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON PullRequest where + parseJSON (Object x) = PullRequest + <$> x .: "merged" + <*> x .: "additions" + <*> x .: "state" + <*> x .: "mergeable_state" + <*> x .: "review_comment_url" + <*> x .: "mergeable" + <*> x .: "assignees" + <*> x .: "locked" + <*> x .: "base" + <*> x .: "body" + <*> x .: "head" + <*> x .: "url" + <*> x .: "milestone" + <*> x .: "statuses_url" + <*> x .: "merged_at" + <*> x .: "commits_url" + <*> x .: "assignee" + <*> x .: "diff_url" + <*> x .: "user" + <*> x .: "comments_url" + <*> x .: "_links" + <*> x .: "updated_at" + <*> x .: "deletions" + <*> x .: "commits" + <*> x .: "patch_url" + <*> x .: "created_at" + <*> x .: "review_comments" + <*> x .: "id" + <*> x .: "issue_url" + <*> x .: "comments" + <*> x .: "merged_by" + <*> x .: "title" + <*> x .: "closed_at" + <*> x .: "changed_files" + <*> x .: "number" + <*> x .: "merge_commit_sha" + <*> x .: "review_comments_url" + <*> x .: "html_url" + + parseJSON _ = fail "PullRequest" + + +instance ToJSON PullRequest where + toJSON PullRequest{..} = object + [ "merged" .= pullRequestMerged + , "additions" .= pullRequestAdditions + , "state" .= pullRequestState + , "mergeable_state" .= pullRequestMergeableState + , "review_comment_url" .= pullRequestReviewCommentUrl + , "mergeable" .= pullRequestMergeable + , "assignees" .= pullRequestAssignees + , "locked" .= pullRequestLocked + , "base" .= pullRequestBase + , "body" .= pullRequestBody + , "head" .= pullRequestHead + , "url" .= pullRequestUrl + , "milestone" .= pullRequestMilestone + , "statuses_url" .= pullRequestStatusesUrl + , "merged_at" .= pullRequestMergedAt + , "commits_url" .= pullRequestCommitsUrl + , "assignee" .= pullRequestAssignee + , "diff_url" .= pullRequestDiffUrl + , "user" .= pullRequestUser + , "comments_url" .= pullRequestCommentsUrl + , "_links" .= pullRequestLinks + , "updated_at" .= pullRequestUpdatedAt + , "deletions" .= pullRequestDeletions + , "commits" .= pullRequestCommits + , "patch_url" .= pullRequestPatchUrl + , "created_at" .= pullRequestCreatedAt + , "review_comments" .= pullRequestReviewComments + , "id" .= pullRequestId + , "issue_url" .= pullRequestIssueUrl + , "comments" .= pullRequestComments + , "merged_by" .= pullRequestMergedBy + , "title" .= pullRequestTitle + , "closed_at" .= pullRequestClosedAt + , "changed_files" .= pullRequestChangedFiles + , "number" .= pullRequestNumber + , "merge_commit_sha" .= pullRequestMergeCommitSha + , "review_comments_url" .= pullRequestReviewCommentsUrl + , "html_url" .= pullRequestHtmlUrl + ] + + + +------------------------------------------------------------------------------ +-- StatusCommit + +data StatusCommit = StatusCommit + { statusCommitSha :: Text + , statusCommitCommit :: CommitDetails + , statusCommitUrl :: Text + , statusCommitCommentsUrl :: Text + , statusCommitHtmlUrl :: Text + , statusCommitAuthor :: User + , statusCommitCommitter :: User + , statusCommitParents :: [CommitRefHtml] + } deriving (Eq, Show, Read) + + +instance FromJSON StatusCommit where + parseJSON (Object x) = StatusCommit + <$> x .: "sha" + <*> x .: "commit" + <*> x .: "url" + <*> x .: "comments_url" + <*> x .: "html_url" + <*> x .: "author" + <*> x .: "committer" + <*> x .: "parents" + + parseJSON _ = fail "StatusCommit" + + +instance ToJSON StatusCommit where + toJSON StatusCommit{..} = object + [ "sha" .= statusCommitSha + , "commit" .= statusCommitCommit + , "url" .= statusCommitUrl + , "comments_url" .= statusCommitCommentsUrl + , "html_url" .= statusCommitHtmlUrl + , "author" .= statusCommitAuthor + , "committer" .= statusCommitCommitter + , "parents" .= statusCommitParents + ] + + + +------------------------------------------------------------------------------ +-- PushCommit + +data PushCommit = PushCommit + { pushCommitId :: Text + , pushCommitTreeId :: Text + , pushCommitDistinct :: Bool + , pushCommitMessage :: Text + , pushCommitTimestamp :: Text + , pushCommitUrl :: Text + , pushCommitAuthor :: Author + , pushCommitCommitter :: Author + , pushCommitAdded :: [Text] + , pushCommitRemoved :: [Text] + , pushCommitModified :: [Text] + } deriving (Eq, Show, Read) + + +instance FromJSON PushCommit where + parseJSON (Object x) = PushCommit + <$> x .: "id" + <*> x .: "tree_id" + <*> x .: "distinct" + <*> x .: "message" + <*> x .: "timestamp" + <*> x .: "url" + <*> x .: "author" + <*> x .: "committer" + <*> x .: "added" + <*> x .: "removed" + <*> x .: "modified" + + parseJSON _ = fail "PushCommit" + + +instance ToJSON PushCommit where + toJSON PushCommit{..} = object + [ "id" .= pushCommitId + , "tree_id" .= pushCommitTreeId + , "distinct" .= pushCommitDistinct + , "message" .= pushCommitMessage + , "timestamp" .= pushCommitTimestamp + , "url" .= pushCommitUrl + , "author" .= pushCommitAuthor + , "committer" .= pushCommitCommitter + , "added" .= pushCommitAdded + , "removed" .= pushCommitRemoved + , "modified" .= pushCommitModified + ] + + + +------------------------------------------------------------------------------ +-- Organization + +data Organization = Organization + { organizationLogin :: Text + , organizationId :: Int + , organizationUrl :: Text + , organizationReposUrl :: Text + , organizationEventsUrl :: Text + , organizationHooksUrl :: Text + , organizationIssuesUrl :: Text + , organizationMembersUrl :: Text + , organizationPublicMembersUrl :: Text + , organizationAvatarUrl :: Text + , organizationDescription :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON Organization where + parseJSON (Object x) = Organization + <$> x .: "login" + <*> x .: "id" + <*> x .: "url" + <*> x .: "repos_url" + <*> x .: "events_url" + <*> x .: "hooks_url" + <*> x .: "issues_url" + <*> x .: "members_url" + <*> x .: "public_members_url" + <*> x .: "avatar_url" + <*> x .: "description" + + parseJSON _ = fail "Organization" + + +instance ToJSON Organization where + toJSON Organization{..} = object + [ "login" .= organizationLogin + , "id" .= organizationId + , "url" .= organizationUrl + , "repos_url" .= organizationReposUrl + , "events_url" .= organizationEventsUrl + , "hooks_url" .= organizationHooksUrl + , "issues_url" .= organizationIssuesUrl + , "members_url" .= organizationMembersUrl + , "public_members_url" .= organizationPublicMembersUrl + , "avatar_url" .= organizationAvatarUrl + , "description" .= organizationDescription + ] + + + +------------------------------------------------------------------------------ +-- User + +data User = User + { userLogin :: Text + , userId :: Int + , userAvatarUrl :: Text + , userGravatarId :: Text + , userUrl :: Text + , userHtmlUrl :: Text + , userFollowersUrl :: Text + , userFollowingUrl :: Text + , userGistsUrl :: Text + , userStarredUrl :: Text + , userSubscriptionsUrl :: Text + , userOrganizationsUrl :: Text + , userReposUrl :: Text + , userEventsUrl :: Text + , userReceivedEventsUrl :: Text + , userType :: Text + , userSiteAdmin :: Bool + } deriving (Eq, Show, Read) + + +instance FromJSON User where + parseJSON (Object x) = User + <$> x .: "login" + <*> x .: "id" + <*> x .: "avatar_url" + <*> x .: "gravatar_id" + <*> x .: "url" + <*> x .: "html_url" + <*> x .: "followers_url" + <*> x .: "following_url" + <*> x .: "gists_url" + <*> x .: "starred_url" + <*> x .: "subscriptions_url" + <*> x .: "organizations_url" + <*> x .: "repos_url" + <*> x .: "events_url" + <*> x .: "received_events_url" + <*> x .: "type" + <*> x .: "site_admin" + + parseJSON _ = fail "User" + + +instance ToJSON User where + toJSON User{..} = object + [ "login" .= userLogin + , "id" .= userId + , "avatar_url" .= userAvatarUrl + , "gravatar_id" .= userGravatarId + , "url" .= userUrl + , "html_url" .= userHtmlUrl + , "followers_url" .= userFollowersUrl + , "following_url" .= userFollowingUrl + , "gists_url" .= userGistsUrl + , "starred_url" .= userStarredUrl + , "subscriptions_url" .= userSubscriptionsUrl + , "organizations_url" .= userOrganizationsUrl + , "repos_url" .= userReposUrl + , "events_url" .= userEventsUrl + , "received_events_url" .= userReceivedEventsUrl + , "type" .= userType + , "site_admin" .= userSiteAdmin + ] + + + +------------------------------------------------------------------------------ +-- Commit + +data Commit = Commit + { commitSha :: Text + , commitUser :: User + , commitRepo :: Repository + , commitLabel :: Text + , commitRef :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON Commit where + parseJSON (Object x) = Commit + <$> x .: "sha" + <*> x .: "user" + <*> x .: "repo" + <*> x .: "label" + <*> x .: "ref" + + parseJSON _ = fail "Commit" + + +instance ToJSON Commit where + toJSON Commit{..} = object + [ "sha" .= commitSha + , "user" .= commitUser + , "repo" .= commitRepo + , "label" .= commitLabel + , "ref" .= commitRef + ] + + + +------------------------------------------------------------------------------ +-- CommitDetails + +data CommitDetails = CommitDetails + { commitDetailsAuthor :: UserStamp + , commitDetailsCommitter :: UserStamp + , commitDetailsMessage :: Text + , commitDetailsTree :: CommitRef + , commitDetailsUrl :: Text + , commitDetailsCommentCount :: Int + } deriving (Eq, Show, Read) + + +instance FromJSON CommitDetails where + parseJSON (Object x) = CommitDetails + <$> x .: "author" + <*> x .: "committer" + <*> x .: "message" + <*> x .: "tree" + <*> x .: "url" + <*> x .: "comment_count" + + parseJSON _ = fail "CommitDetails" + + +instance ToJSON CommitDetails where + toJSON CommitDetails{..} = object + [ "author" .= commitDetailsAuthor + , "committer" .= commitDetailsCommitter + , "message" .= commitDetailsMessage + , "tree" .= commitDetailsTree + , "url" .= commitDetailsUrl + , "comment_count" .= commitDetailsCommentCount + ] + + + +------------------------------------------------------------------------------ +-- CommitRefHtml + +data CommitRefHtml = CommitRefHtml + { commitRefHtmlSha :: Text + , commitRefHtmlUrl :: Text + , commitRefHtmlHtmlUrl :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON CommitRefHtml where + parseJSON (Object x) = CommitRefHtml + <$> x .: "sha" + <*> x .: "url" + <*> x .: "html_url" + + parseJSON _ = fail "CommitRefHtml" + + +instance ToJSON CommitRefHtml where + toJSON CommitRefHtml{..} = object + [ "sha" .= commitRefHtmlSha + , "url" .= commitRefHtmlUrl + , "html_url" .= commitRefHtmlHtmlUrl + ] + + + +------------------------------------------------------------------------------ +-- CommitRef + +data CommitRef = CommitRef + { commitRefSha :: Text + , commitRefUrl :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON CommitRef where + parseJSON (Object x) = CommitRef + <$> x .: "sha" + <*> x .: "url" + + parseJSON _ = fail "CommitRef" + + +instance ToJSON CommitRef where + toJSON CommitRef{..} = object + [ "sha" .= commitRefSha + , "url" .= commitRefUrl + ] + + + +------------------------------------------------------------------------------ +-- Branch + +data Branch = Branch + { branchName :: Text + , branchCommit :: CommitRef + } deriving (Eq, Show, Read) + + +instance FromJSON Branch where + parseJSON (Object x) = Branch + <$> x .: "name" + <*> x .: "commit" + + parseJSON _ = fail "Branch" + + +instance ToJSON Branch where + toJSON Branch{..} = object + [ "name" .= branchName + , "commit" .= branchCommit + ] + + + +------------------------------------------------------------------------------ +-- Pusher + +data Pusher = Pusher + { pusherName :: Text + , pusherEmail :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON Pusher where + parseJSON (Object x) = Pusher + <$> x .: "name" + <*> x .: "email" + + parseJSON _ = fail "Pusher" + + +instance ToJSON Pusher where + toJSON Pusher{..} = object + [ "name" .= pusherName + , "email" .= pusherEmail + ] + + + +------------------------------------------------------------------------------ +-- UserRef + +data UserRef = UserRef + { userRefName :: Text + , userRefEmail :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON UserRef where + parseJSON (Object x) = UserRef + <$> x .: "name" + <*> x .: "email" + + parseJSON _ = fail "UserRef" + + +instance ToJSON UserRef where + toJSON UserRef{..} = object + [ "name" .= userRefName + , "email" .= userRefEmail + ] + + + +------------------------------------------------------------------------------ +-- UserStamp + +data UserStamp = UserStamp + { userStampName :: Text + , userStampEmail :: Text + , userStampDate :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON UserStamp where + parseJSON (Object x) = UserStamp + <$> x .: "name" + <*> x .: "email" + <*> x .: "date" + + parseJSON _ = fail "UserStamp" + + +instance ToJSON UserStamp where + toJSON UserStamp{..} = object + [ "name" .= userStampName + , "email" .= userStampEmail + , "date" .= userStampDate + ] + + + +------------------------------------------------------------------------------ +-- Author + +data Author = Author + { authorName :: Text + , authorEmail :: Text + , authorUsername :: Text + } deriving (Eq, Show, Read) + + +instance FromJSON Author where + parseJSON (Object x) = Author + <$> x .: "name" + <*> x .: "email" + <*> x .: "username" + + parseJSON _ = fail "Author" + + +instance ToJSON Author where + toJSON Author{..} = object + [ "name" .= authorName + , "email" .= authorEmail + , "username" .= authorUsername + ] + + + +------------------------------------------------------------------------------ +-- Repository + +data Repository = Repository + { repositoryHomepage :: Maybe Text + , repositoryHooksUrl :: Text + , repositoryBlobsUrl :: Text + , repositorySshUrl :: Text + , repositorySvnUrl :: Text + , repositoryCloneUrl :: Text + , repositoryMergesUrl :: Text + , repositoryNotificationsUrl :: Text + , repositoryCollaboratorsUrl :: Text + , repositoryLanguagesUrl :: Text + , repositorySize :: Int + , repositoryIssueEventsUrl :: Text + , repositoryPrivate :: Bool + , repositoryFork :: Bool + , repositoryGitCommitsUrl :: Text + , repositoryDownloadsUrl :: Text + , repositoryFullName :: Text + , repositoryUrl :: Text + , repositoryArchiveUrl :: Text + , repositoryGitUrl :: Text + , repositoryStatusesUrl :: Text + , repositoryIssuesUrl :: Text + , repositoryDeploymentsUrl :: Text + , repositoryCommitsUrl :: Text + , repositoryTreesUrl :: Text + , repositoryOwner :: RepoOwner + , repositoryMilestonesUrl :: Text + , repositoryHasWiki :: Bool + , repositoryIssueCommentUrl :: Text + , repositoryCommentsUrl :: Text + , repositoryContributorsUrl :: Text + , repositoryName :: Text + , repositoryHasIssues :: Bool + , repositoryUpdatedAt :: DateTime + , repositoryMasterBranch :: Maybe Text + , repositoryForksCount :: Int + , repositoryForksUrl :: Text + , repositorySubscriptionUrl :: Text + , repositoryHasDownloads :: Bool + , repositoryTeamsUrl :: Text + , repositoryPullsUrl :: Text + , repositoryLanguage :: Maybe Text + , repositoryCreatedAt :: DateTime + , repositoryHasPages :: Bool + , repositoryPushedAt :: DateTime + , repositoryId :: Int + , repositorySubscribersUrl :: Text + , repositoryTagsUrl :: Text + , repositoryOpenIssuesCount :: Int + , repositoryMirrorUrl :: Maybe Text + , repositoryWatchers :: Int + , repositoryStargazers :: Maybe Int + , repositoryStargazersCount :: Int + , repositoryStargazersUrl :: Text + , repositoryKeysUrl :: Text + , repositoryGitTagsUrl :: Text + , repositoryDefaultBranch :: Text + , repositoryEventsUrl :: Text + , repositoryCompareUrl :: Text + , repositoryGitRefsUrl :: Text + , repositoryOrganization :: Maybe Text + , repositoryForks :: Int + , repositoryContentsUrl :: Text + , repositoryBranchesUrl :: Text + , repositoryReleasesUrl :: Text + , repositoryAssigneesUrl :: Text + , repositoryDescription :: Text + , repositoryWatchersCount :: Int + , repositoryLabelsUrl :: Text + , repositoryHtmlUrl :: Text + , repositoryOpenIssues :: Int + , repositoryPublic :: Maybe Bool + } deriving (Eq, Show, Read) + + +instance FromJSON Repository where + parseJSON (Object x) = Repository + <$> x .: "homepage" + <*> x .: "hooks_url" + <*> x .: "blobs_url" + <*> x .: "ssh_url" + <*> x .: "svn_url" + <*> x .: "clone_url" + <*> x .: "merges_url" + <*> x .: "notifications_url" + <*> x .: "collaborators_url" + <*> x .: "languages_url" + <*> x .: "size" + <*> x .: "issue_events_url" + <*> x .: "private" + <*> x .: "fork" + <*> x .: "git_commits_url" + <*> x .: "downloads_url" + <*> x .: "full_name" + <*> x .: "url" + <*> x .: "archive_url" + <*> x .: "git_url" + <*> x .: "statuses_url" + <*> x .: "issues_url" + <*> x .: "deployments_url" + <*> x .: "commits_url" + <*> x .: "trees_url" + <*> x .: "owner" + <*> x .: "milestones_url" + <*> x .: "has_wiki" + <*> x .: "issue_comment_url" + <*> x .: "comments_url" + <*> x .: "contributors_url" + <*> x .: "name" + <*> x .: "has_issues" + <*> x .: "updated_at" + <*> x .:? "master_branch" + <*> x .: "forks_count" + <*> x .: "forks_url" + <*> x .: "subscription_url" + <*> x .: "has_downloads" + <*> x .: "teams_url" + <*> x .: "pulls_url" + <*> x .:? "language" + <*> x .: "created_at" + <*> x .: "has_pages" + <*> x .: "pushed_at" + <*> x .: "id" + <*> x .: "subscribers_url" + <*> x .: "tags_url" + <*> x .: "open_issues_count" + <*> x .: "mirror_url" + <*> x .: "watchers" + <*> x .:? "stargazers" + <*> x .: "stargazers_count" + <*> x .: "stargazers_url" + <*> x .: "keys_url" + <*> x .: "git_tags_url" + <*> x .: "default_branch" + <*> x .: "events_url" + <*> x .: "compare_url" + <*> x .: "git_refs_url" + <*> x .:? "organization" + <*> x .: "forks" + <*> x .: "contents_url" + <*> x .: "branches_url" + <*> x .: "releases_url" + <*> x .: "assignees_url" + <*> x .: "description" + <*> x .: "watchers_count" + <*> x .: "labels_url" + <*> x .: "html_url" + <*> x .: "open_issues" + <*> x .:? "public" + + parseJSON _ = fail "Repository" + + +instance ToJSON Repository where + toJSON Repository{..} = object + [ "homepage" .= repositoryHomepage + , "hooks_url" .= repositoryHooksUrl + , "blobs_url" .= repositoryBlobsUrl + , "ssh_url" .= repositorySshUrl + , "svn_url" .= repositorySvnUrl + , "clone_url" .= repositoryCloneUrl + , "merges_url" .= repositoryMergesUrl + , "notifications_url" .= repositoryNotificationsUrl + , "collaborators_url" .= repositoryCollaboratorsUrl + , "languages_url" .= repositoryLanguagesUrl + , "size" .= repositorySize + , "issue_events_url" .= repositoryIssueEventsUrl + , "private" .= repositoryPrivate + , "fork" .= repositoryFork + , "git_commits_url" .= repositoryGitCommitsUrl + , "downloads_url" .= repositoryDownloadsUrl + , "full_name" .= repositoryFullName + , "url" .= repositoryUrl + , "archive_url" .= repositoryArchiveUrl + , "git_url" .= repositoryGitUrl + , "statuses_url" .= repositoryStatusesUrl + , "issues_url" .= repositoryIssuesUrl + , "deployments_url" .= repositoryDeploymentsUrl + , "commits_url" .= repositoryCommitsUrl + , "trees_url" .= repositoryTreesUrl + , "owner" .= repositoryOwner + , "milestones_url" .= repositoryMilestonesUrl + , "has_wiki" .= repositoryHasWiki + , "issue_comment_url" .= repositoryIssueCommentUrl + , "comments_url" .= repositoryCommentsUrl + , "contributors_url" .= repositoryContributorsUrl + , "name" .= repositoryName + , "has_issues" .= repositoryHasIssues + , "updated_at" .= repositoryUpdatedAt + , "master_branch" .= repositoryMasterBranch + , "forks_count" .= repositoryForksCount + , "forks_url" .= repositoryForksUrl + , "subscription_url" .= repositorySubscriptionUrl + , "has_downloads" .= repositoryHasDownloads + , "teams_url" .= repositoryTeamsUrl + , "pulls_url" .= repositoryPullsUrl + , "language" .= repositoryLanguage + , "created_at" .= repositoryCreatedAt + , "has_pages" .= repositoryHasPages + , "pushed_at" .= repositoryPushedAt + , "id" .= repositoryId + , "subscribers_url" .= repositorySubscribersUrl + , "tags_url" .= repositoryTagsUrl + , "open_issues_count" .= repositoryOpenIssuesCount + , "mirror_url" .= repositoryMirrorUrl + , "watchers" .= repositoryWatchers + , "stargazers" .= repositoryStargazers + , "stargazers_count" .= repositoryStargazersCount + , "stargazers_url" .= repositoryStargazersUrl + , "keys_url" .= repositoryKeysUrl + , "git_tags_url" .= repositoryGitTagsUrl + , "default_branch" .= repositoryDefaultBranch + , "events_url" .= repositoryEventsUrl + , "compare_url" .= repositoryCompareUrl + , "git_refs_url" .= repositoryGitRefsUrl + , "forks" .= repositoryForks + , "organization" .= repositoryOrganization + , "contents_url" .= repositoryContentsUrl + , "branches_url" .= repositoryBranchesUrl + , "releases_url" .= repositoryReleasesUrl + , "assignees_url" .= repositoryAssigneesUrl + , "description" .= repositoryDescription + , "watchers_count" .= repositoryWatchersCount + , "labels_url" .= repositoryLabelsUrl + , "html_url" .= repositoryHtmlUrl + , "open_issues" .= repositoryOpenIssues + , "public" .= repositoryPublic + ] + + +------------------------------------------------------------------------------ +-- State + +data State = Pending | Success | Failure | Error + deriving (Eq, Show, Read) + + +instance FromJSON State where + parseJSON (String "pending") = return Pending + parseJSON (String "success") = return Success + parseJSON (String "failure") = return Failure + parseJSON (String "error") = return Error + parseJSON _ = fail "State" + +instance ToJSON State where + toJSON Pending = String "pending" + toJSON Success = String "success" + toJSON Failure = String "failure" + toJSON Error = String "error" + + + +------------------------------------------------------------------------------ +-- Deployment + +data Deployment = Deployment + { deploymentUrl :: Text + , deploymentId :: Int + , deploymentSha :: Text + , deploymentRef :: Text + , deploymentTask :: Text + , deploymentPayload :: Value + , deploymentEnvironment :: Text + , deploymentDescription :: Maybe Text + , deploymentCreator :: User + , deploymentCreatedAt :: DateTime + , deploymentUpdatedAt :: DateTime + , deploymentStatusesUrl :: Text + , deploymentRepositoryUrl :: Text + } deriving (Eq, Show, Read) + +instance FromJSON Deployment where + parseJSON (Object x) = Deployment + <$> x .: "url" + <*> x .: "id" + <*> x .: "sha" + <*> x .: "ref" + <*> x .: "task" + <*> x .: "payload" + <*> x .: "environment" + <*> x .: "description" + <*> x .: "creator" + <*> x .: "created_at" + <*> x .: "updated_at" + <*> x .: "statuses_url" + <*> x .: "repository_url" + + parseJSON _ = fail "Deployment" + +instance ToJSON Deployment where + toJSON Deployment{..} = object + [ "url" .= deploymentUrl + , "id" .= deploymentId + , "sha" .= deploymentSha + , "ref" .= deploymentRef + , "task" .= deploymentTask + , "payload" .= deploymentPayload + , "environment" .= deploymentEnvironment + , "description" .= deploymentDescription + , "creator" .= deploymentCreator + , "created_at" .= deploymentCreatedAt + , "updated_at" .= deploymentUpdatedAt + , "statuses_url" .= deploymentStatusesUrl + , "repository_url" .= deploymentRepositoryUrl + ] + + + +------------------------------------------------------------------------------ +-- DeploymentStatus + +data DeploymentStatus = DeploymentStatus + { deploymentStatusUrl :: Text + , deploymentStatusId :: Int + , deploymentStatusState :: Text + , deploymentStatusCreator :: User + , deploymentStatusDescription :: Text + , deploymentStatusTargetUrl :: Text + , deploymentStatusCreatedAt :: DateTime + , deploymentStatusUpdatedAt :: DateTime + , deploymentStatusDeploymentUrl :: Text + , deploymentStatusRepositoryUrl :: Text + } deriving (Eq, Show, Read) + +instance FromJSON DeploymentStatus where + parseJSON (Object x) = DeploymentStatus + <$> x .: "url" + <*> x .: "id" + <*> x .: "state" + <*> x .: "creator" + <*> x .: "description" + <*> x .: "target_url" + <*> x .: "created_at" + <*> x .: "updated_at" + <*> x .: "deployment_url" + <*> x .: "repository_url" + + parseJSON _ = fail "DeploymentStatus" + +instance ToJSON DeploymentStatus where + toJSON DeploymentStatus{..} = object + [ "url" .= deploymentStatusUrl + , "id" .= deploymentStatusId + , "state" .= deploymentStatusState + , "creator" .= deploymentStatusCreator + , "description" .= deploymentStatusDescription + , "target_url" .= deploymentStatusTargetUrl + , "created_at" .= deploymentStatusCreatedAt + , "updated_at" .= deploymentStatusUpdatedAt + , "deployment_url" .= deploymentStatusDeploymentUrl + , "repository_url" .= deploymentStatusRepositoryUrl + ] diff --git a/web/GitHub/Types/Events.hs b/web/GitHub/Types/Events.hs new file mode 100644 index 0000000..300b6bc --- /dev/null +++ b/web/GitHub/Types/Events.hs @@ -0,0 +1,706 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} + +module GitHub.Types.Events where + + +import Control.Applicative ((<$>), (<*>)) + +import Data.Aeson (FromJSON (..), ToJSON (..), object) +import Data.Aeson.Types (Parser, Value (..), (.:), (.:?), (.=)) +import qualified Data.List as List +import Data.Monoid ((<>)) +import Data.Text (Text) +import qualified Data.Text as Text +import Data.Time (UTCTime) + +import GitHub.Types.Base + + + +-- | All events which can be produced by GitHub. +-- +-- See https://developer.github.com/v3/activity/events/ +data Event = Event + { eventId :: !Text + , eventActor :: !Actor + , eventRepo :: !Repo + , eventCreatedAt :: !UTCTime + , eventPublic :: !Bool + , eventPayload :: !Payload + } deriving (Eq, Show, Read) + +instance FromJSON Event where + parseJSON (Object o) = do + eventType <- o .: "type" + + Event + <$> o .: "id" + <*> o .: "actor" + <*> o .: "repo" + <*> o .: "created_at" + <*> o .: "public" + <*> (eventPayloadParser eventType =<< o .: "payload") + + parseJSON _ = fail "Event" + + +data Actor = Actor + { actorId :: !Integer + , actorLogin :: !Text + } deriving (Eq, Show, Read) + +instance FromJSON Actor where + parseJSON (Object o) = Actor + <$> o .: "id" + <*> o .: "login" + + parseJSON _ = fail "Actor" + + +data Repo = Repo + { repoId :: !Integer + , repoName :: !Text + } deriving (Eq, Show, Read) + +instance FromJSON Repo where + parseJSON (Object o) = Repo + <$> o .: "id" + <*> o .: "name" + + parseJSON _ = fail "Repo" + + +data Payload + = CommitCommentEventPayload CommitCommentEvent + | DeploymentEventPayload DeploymentEvent + | DeploymentStatusEventPayload DeploymentStatusEvent + | PushEventPayload PushEvent + | IssuesEventPayload IssuesEvent + | IssueCommentEventPayload IssueCommentEvent + | CreateEventPayload CreateEvent + | PullRequestEventPayload PullRequestEvent + | PullRequestReviewEventPayload PullRequestReviewEvent + | PullRequestReviewCommentEventPayload PullRequestReviewCommentEvent + | WatchEventPayload WatchEvent + | DeleteEventPayload DeleteEvent + | ForkEventPayload ForkEvent + | ReleaseEventPayload ReleaseEvent + | GollumEventPayload GollumEvent + | MemberEventPayload MemberEvent + | PublicEventPayload Value + | StatusEventPayload StatusEvent + deriving (Eq, Show, Read) + + +instance ToJSON Payload where + toJSON (CommitCommentEventPayload x) = toJSON x + toJSON (DeploymentEventPayload x) = toJSON x + toJSON (DeploymentStatusEventPayload x) = toJSON x + toJSON (PushEventPayload x) = toJSON x + toJSON (IssuesEventPayload x) = toJSON x + toJSON (IssueCommentEventPayload x) = toJSON x + toJSON (CreateEventPayload x) = toJSON x + toJSON (PullRequestEventPayload x) = toJSON x + toJSON (PullRequestReviewEventPayload x) = toJSON x + toJSON (PullRequestReviewCommentEventPayload x) = toJSON x + toJSON (WatchEventPayload x) = toJSON x + toJSON (DeleteEventPayload x) = toJSON x + toJSON (ForkEventPayload x) = toJSON x + toJSON (ReleaseEventPayload x) = toJSON x + toJSON (GollumEventPayload x) = toJSON x + toJSON (MemberEventPayload x) = toJSON x + toJSON (PublicEventPayload x) = toJSON x + toJSON (StatusEventPayload x) = toJSON x + + +eventPayloadParsers :: [(Text, Text, Value -> Parser Payload)] +eventPayloadParsers = + [ ( "CommitCommentEvent", "commit_comment" + , fmap CommitCommentEventPayload . parseJSON) + + , ( "DeploymentEvent", "deployment" + , fmap DeploymentEventPayload . parseJSON) + + , ( "DeploymentStatusEvent", "deployment_status" + , fmap DeploymentStatusEventPayload . parseJSON) + + , ( "PushEvent", "push" + , fmap PushEventPayload . parseJSON) + + , ( "IssuesEvent", "issues" + , fmap IssuesEventPayload . parseJSON) + + , ( "IssueCommentEvent", "issue_comment" + , fmap IssueCommentEventPayload . parseJSON) + + , ( "CreateEvent", "create" + , fmap CreateEventPayload . parseJSON) + + , ( "PullRequestEvent", "pull_request" + , fmap PullRequestEventPayload . parseJSON) + + , ( "PullRequestReviewEvent", "pull_request_review" + , fmap PullRequestReviewEventPayload . parseJSON) + + , ( "PullRequestReviewCommentEvent", "pull_request_review_comment" + , fmap PullRequestReviewCommentEventPayload . parseJSON) + + , ( "WatchEvent", "watch" + , fmap WatchEventPayload . parseJSON) + + , ( "DeleteEvent", "delete" + , fmap DeleteEventPayload . parseJSON) + + , ( "ForkEvent", "fork" + , fmap ForkEventPayload . parseJSON) + + , ( "ReleaseEvent", "release" + , fmap ReleaseEventPayload . parseJSON) + + , ( "GollumEvent", "gollum" + , fmap GollumEventPayload . parseJSON) + + , ( "MemberEvent", "member" + , fmap MemberEventPayload . parseJSON) + + , ( "PublicEvent", "public" + , fmap PublicEventPayload . parseJSON) + + , ( "StatusEvent", "status" + , fmap StatusEventPayload . parseJSON) + ] + + + +eventPayloadParser :: Text -> Value -> Parser Payload +eventPayloadParser eventType x = + case List.find (\(t, _, _) -> t == eventType) eventPayloadParsers of + Nothing -> fail $ "eventPayloadParser: Unknown event type: " <> Text.unpack eventType + Just (_, _, p) -> p x + +-- | Since the event type is included through different means (X-GitHub-Event +-- header, or inline in the JSON object), it's not possible to make 'Event' +-- an instance of 'FromJSON'. But if you know the type, you can use this +-- parser. +webhookPayloadParser :: Text -> Value -> Parser Payload +webhookPayloadParser eventType x = + case List.find (\(_, t, _) -> t == eventType) eventPayloadParsers of + Nothing -> fail $ "webhookPayloadParser: Unknown event type: " <> Text.unpack eventType + Just (_, _, p) -> p x + + +------------------------------------------------------------------------------ +-- CommitCommentEvent + +data CommitCommentEvent = CommitCommentEvent + { commitCommentEventComment :: Value + } deriving (Eq, Show, Read) + +instance FromJSON CommitCommentEvent where + parseJSON (Object x) = CommitCommentEvent + <$> x .: "comment" + + parseJSON _ = fail "CommitCommentEvent" + +instance ToJSON CommitCommentEvent where + toJSON CommitCommentEvent{..} = object + [ "comment" .= commitCommentEventComment + ] + + +------------------------------------------------------------------------------ +-- DeploymentEvent + +data DeploymentEvent = DeploymentEvent + { deploymentEventDeployment :: Deployment + , deploymentEventRepository :: Repository + , deploymentEventSender :: User + , deploymentEventOrganization :: Organization + } deriving (Eq, Show, Read) + +instance FromJSON DeploymentEvent where + parseJSON (Object x) = DeploymentEvent + <$> x .: "deployment" + <*> x .: "repository" + <*> x .: "sender" + <*> x .: "organization" + + parseJSON _ = fail "DeploymentEvent" + +instance ToJSON DeploymentEvent where + toJSON DeploymentEvent{..} = object + [ "deployment" .= deploymentEventDeployment + , "repository" .= deploymentEventRepository + , "sender" .= deploymentEventSender + , "organization" .= deploymentEventOrganization + ] + + +------------------------------------------------------------------------------ +-- DeploymentStatusEvent + +data DeploymentStatusEvent = DeploymentStatusEvent + { deploymentStatusEventDeploymentStatus :: DeploymentStatus + , deploymentStatusEventDeployment :: Deployment + , deploymentStatusEventRepository :: Repository + , deploymentStatusEventOrganization :: Organization + , deploymentStatusEventSender :: User + } deriving (Eq, Show, Read) + +instance FromJSON DeploymentStatusEvent where + parseJSON (Object x) = DeploymentStatusEvent + <$> x .: "deployment_status" + <*> x .: "deployment" + <*> x .: "repository" + <*> x .: "organization" + <*> x .: "sender" + + parseJSON _ = fail "DeploymentStatusEvent" + +instance ToJSON DeploymentStatusEvent where + toJSON DeploymentStatusEvent{..} = object + [ "deployment_status" .= deploymentStatusEventDeploymentStatus + , "deployment" .= deploymentStatusEventDeployment + , "repository" .= deploymentStatusEventRepository + , "organization" .= deploymentStatusEventOrganization + , "sender" .= deploymentStatusEventSender + ] + + +------------------------------------------------------------------------------ +-- PushEvent + +data PushEvent = PushEvent + { pushEventRef :: Text + , pushEventBefore :: Text + , pushEventAfter :: Text + , pushEventCreated :: Bool + , pushEventDeleted :: Bool + , pushEventForced :: Bool + , pushEventRefName :: Maybe Text + , pushEventBaseRef :: Maybe Text + , pushEventCompare :: Text + , pushEventDistinctCommits :: Maybe [PushCommit] + , pushEventCommits :: [PushCommit] + , pushEventHeadCommit :: Maybe PushCommit + , pushEventRepository :: Repository + , pushEventPusher :: Pusher + , pushEventOrganization :: Organization + , pushEventSender :: User + } deriving (Eq, Show, Read) + +instance FromJSON PushEvent where + parseJSON (Object x) = PushEvent + <$> x .: "ref" + <*> x .: "before" + <*> x .: "after" + <*> x .: "created" + <*> x .: "deleted" + <*> x .: "forced" + <*> x .:? "ref_name" + <*> x .: "base_ref" + <*> x .: "compare" + <*> x .:? "distinct_commits" + <*> x .: "commits" + <*> x .: "head_commit" + <*> x .: "repository" + <*> x .: "pusher" + <*> x .: "organization" + <*> x .: "sender" + + parseJSON _ = fail "PushEvent" + +instance ToJSON PushEvent where + toJSON PushEvent{..} = object + [ "ref" .= pushEventRef + , "before" .= pushEventBefore + , "after" .= pushEventAfter + , "created" .= pushEventCreated + , "deleted" .= pushEventDeleted + , "forced" .= pushEventForced + , "ref_name" .= pushEventRefName + , "base_ref" .= pushEventBaseRef + , "compare" .= pushEventCompare + , "distinct_commits" .= pushEventDistinctCommits + , "commits" .= pushEventCommits + , "head_commit" .= pushEventHeadCommit + , "repository" .= pushEventRepository + , "pusher" .= pushEventPusher + , "organization" .= pushEventOrganization + , "sender" .= pushEventSender + ] + + +------------------------------------------------------------------------------ +-- IssuesEvent + +data IssuesEvent = IssuesEvent + { issuesEventRepository :: Repository + , issuesEventSender :: User + , issuesEventAction :: Text + , issuesEventOrganization :: Organization + , issuesEventIssue :: Issue + } deriving (Eq, Show, Read) + +instance FromJSON IssuesEvent where + parseJSON (Object x) = IssuesEvent + <$> x .: "repository" + <*> x .: "sender" + <*> x .: "action" + <*> x .: "organization" + <*> x .: "issue" + + parseJSON _ = fail "IssuesEvent" + +instance ToJSON IssuesEvent where + toJSON IssuesEvent{..} = object + [ "repository" .= issuesEventRepository + , "sender" .= issuesEventSender + , "action" .= issuesEventAction + , "organization" .= issuesEventOrganization + , "issue" .= issuesEventIssue + ] + + +------------------------------------------------------------------------------ +-- IssueCommentEvent + +data IssueCommentEvent = IssueCommentEvent + { issueCommentEventRepository :: Repository + , issueCommentEventSender :: User + , issueCommentEventAction :: Text + , issueCommentEventComment :: IssueComment + , issueCommentEventOrganization :: Organization + , issueCommentEventIssue :: Issue + } deriving (Eq, Show, Read) + +instance FromJSON IssueCommentEvent where + parseJSON (Object x) = IssueCommentEvent + <$> x .: "repository" + <*> x .: "sender" + <*> x .: "action" + <*> x .: "comment" + <*> x .: "organization" + <*> x .: "issue" + + parseJSON _ = fail "IssueCommentEvent" + +instance ToJSON IssueCommentEvent where + toJSON IssueCommentEvent{..} = object + [ "repository" .= issueCommentEventRepository + , "sender" .= issueCommentEventSender + , "action" .= issueCommentEventAction + , "comment" .= issueCommentEventComment + , "organization" .= issueCommentEventOrganization + , "issue" .= issueCommentEventIssue + ] + + +------------------------------------------------------------------------------ +-- CreateEvent + +data CreateEvent = CreateEvent + { createEventRef :: !(Maybe Text) + } deriving (Eq, Show, Read) + +instance FromJSON CreateEvent where + parseJSON (Object x) = CreateEvent + <$> x .: "ref" + + parseJSON _ = fail "CreateEvent" + +instance ToJSON CreateEvent where + toJSON CreateEvent{..} = object + [ "ref" .= createEventRef + ] + + +------------------------------------------------------------------------------ +-- PullRequestEvent + +data PullRequestEvent = PullRequestEvent + { pullRequestEventAfter :: Maybe Text + , pullRequestEventRepository :: Repository + , pullRequestEventChanges :: Maybe Changes + , pullRequestEventSender :: User + , pullRequestEventPullRequest :: PullRequest + , pullRequestEventAction :: Text + , pullRequestEventAssignee :: Maybe User + , pullRequestEventNumber :: Int + , pullRequestEventOrganization :: Organization + , pullRequestEventBefore :: Maybe Text + } deriving (Eq, Show, Read) + +instance FromJSON PullRequestEvent where + parseJSON (Object x) = PullRequestEvent + <$> x .:? "after" + <*> x .: "repository" + <*> x .:? "changes" + <*> x .: "sender" + <*> x .: "pull_request" + <*> x .: "action" + <*> x .:? "assignee" + <*> x .: "number" + <*> x .: "organization" + <*> x .:? "before" + + parseJSON _ = fail "PullRequestEvent" + +instance ToJSON PullRequestEvent where + toJSON PullRequestEvent{..} = object $ + [ "repository" .= pullRequestEventRepository + , "changes" .= pullRequestEventChanges + , "sender" .= pullRequestEventSender + , "pull_request" .= pullRequestEventPullRequest + , "action" .= pullRequestEventAction + , "assignee" .= pullRequestEventAssignee + , "number" .= pullRequestEventNumber + , "organization" .= pullRequestEventOrganization + , "before" .= pullRequestEventBefore + , "after" .= pullRequestEventAfter + ] + + +------------------------------------------------------------------------------ +-- PullRequestReviewEvent + +data PullRequestReviewEvent = PullRequestReviewEvent + { pullRequestReviewEventAction :: Text + , pullRequestReviewEventReview :: Review + , pullRequestReviewEventPullRequest :: SimplePullRequest + , pullRequestReviewEventRepository :: Repository + , pullRequestReviewEventOrganization :: Organization + , pullRequestReviewEventSender :: User + } deriving (Eq, Show, Read) + +instance FromJSON PullRequestReviewEvent where + parseJSON (Object x) = PullRequestReviewEvent + <$> x .: "action" + <*> x .: "review" + <*> x .: "pull_request" + <*> x .: "repository" + <*> x .: "organization" + <*> x .: "sender" + + parseJSON _ = fail "PullRequestReviewEvent" + +instance ToJSON PullRequestReviewEvent where + toJSON PullRequestReviewEvent{..} = object $ + [ "action" .= pullRequestReviewEventAction + , "review" .= pullRequestReviewEventReview + , "pull_request" .= pullRequestReviewEventPullRequest + , "repository" .= pullRequestReviewEventRepository + , "organization" .= pullRequestReviewEventOrganization + , "sender" .= pullRequestReviewEventSender + ] + + +------------------------------------------------------------------------------ +-- PullRequestReviewCommentEventPayload + +data PullRequestReviewCommentEvent = PullRequestReviewCommentEvent + { pullRequestReviewCommentEventPullRequest :: !Value + } deriving (Eq, Show, Read) + +instance FromJSON PullRequestReviewCommentEvent where + parseJSON (Object x) = PullRequestReviewCommentEvent + <$> x .: "pull_request" + + parseJSON _ = fail "PullRequestReviewCommentEvent" + +instance ToJSON PullRequestReviewCommentEvent where + toJSON PullRequestReviewCommentEvent{..} = object + [ "pull_request" .= pullRequestReviewCommentEventPullRequest + ] + + + +------------------------------------------------------------------------------ +-- WatchEvent + +data WatchEvent = WatchEvent + { watchEventRepository :: Repository + , watchEventSender :: User + , watchEventAction :: Text + , watchEventOrganization :: Organization + } deriving (Eq, Show, Read) + +instance FromJSON WatchEvent where + parseJSON (Object x) = WatchEvent + <$> x .: "repository" + <*> x .: "sender" + <*> x .: "action" + <*> x .: "organization" + + parseJSON _ = fail "WatchEvent" + +instance ToJSON WatchEvent where + toJSON WatchEvent{..} = object + [ "repository" .= watchEventRepository + , "sender" .= watchEventSender + , "action" .= watchEventAction + , "organization" .= watchEventOrganization + ] + + +------------------------------------------------------------------------------ +-- DeleteEvent + +data DeleteEvent = DeleteEvent + { deleteEventRef :: !Text + } deriving (Eq, Show, Read) + +instance FromJSON DeleteEvent where + parseJSON (Object x) = DeleteEvent + <$> x .: "ref" + + parseJSON _ = fail "DeleteEvent" + +instance ToJSON DeleteEvent where + toJSON DeleteEvent{..} = object + [ "ref" .= deleteEventRef + ] + + +------------------------------------------------------------------------------ +-- ForkEvent + +data ForkEvent = ForkEvent + { forkEventForkee :: Repository + , forkEventRepository :: Repository + , forkEventOrganization :: Organization + , forkEventSender :: User + } deriving (Eq, Show, Read) + +instance FromJSON ForkEvent where + parseJSON (Object x) = ForkEvent + <$> x .: "forkee" + <*> x .: "repository" + <*> x .: "organization" + <*> x .: "sender" + + parseJSON _ = fail "ForkEvent" + +instance ToJSON ForkEvent where + toJSON ForkEvent{..} = object + [ "forkee" .= forkEventForkee + , "repository" .= forkEventRepository + , "organization" .= forkEventOrganization + , "sender" .= forkEventSender + ] + + +------------------------------------------------------------------------------ +-- ReleaseEvent + +data ReleaseEvent = ReleaseEvent + { releaseEventAction :: !Text + } deriving (Eq, Show, Read) + +instance FromJSON ReleaseEvent where + parseJSON (Object x) = ReleaseEvent + <$> x .: "action" + + parseJSON _ = fail "ForkEvent" + +instance ToJSON ReleaseEvent where + toJSON ReleaseEvent{..} = object + [ "action" .= releaseEventAction + ] + + +------------------------------------------------------------------------------ +-- GollumEvent + +data GollumEvent = GollumEvent + { gollumEventPages :: !Value + } deriving (Eq, Show, Read) + +instance FromJSON GollumEvent where + parseJSON (Object x) = GollumEvent + <$> x .: "pages" + + parseJSON _ = fail "GollumEvent" + +instance ToJSON GollumEvent where + toJSON GollumEvent{..} = object + [ "pages" .= gollumEventPages + ] + + +------------------------------------------------------------------------------ +-- MemberEvent + +data MemberEvent = MemberEvent + { memberEventAction :: !Text + } deriving (Eq, Show, Read) + +instance FromJSON MemberEvent where + parseJSON (Object x) = MemberEvent + <$> x .: "action" + + parseJSON _ = fail "MemberEvent" + +instance ToJSON MemberEvent where + toJSON MemberEvent{..} = object + [ "action" .= memberEventAction + ] + + +------------------------------------------------------------------------------ +-- StatusEvent + +data StatusEvent = StatusEvent + { statusEventId :: Int + , statusEventSha :: Text + , statusEventName :: Text + , statusEventTargetUrl :: Text + , statusEventContext :: Text + , statusEventDescription :: Text + , statusEventState :: Text + , statusEventCommit :: StatusCommit + , statusEventBranches :: [Branch] + , statusEventCreatedAt :: DateTime + , statusEventUpdatedAt :: DateTime + , statusEventRepository :: Repository + , statusEventOrganization :: Organization + , statusEventSender :: User + } deriving (Eq, Show, Read) + +instance FromJSON StatusEvent where + parseJSON (Object x) = StatusEvent + <$> x .: "id" + <*> x .: "sha" + <*> x .: "name" + <*> x .: "target_url" + <*> x .: "context" + <*> x .: "description" + <*> x .: "state" + <*> x .: "commit" + <*> x .: "branches" + <*> x .: "created_at" + <*> x .: "updated_at" + <*> x .: "repository" + <*> x .: "organization" + <*> x .: "sender" + + parseJSON _ = fail "StatusEvent" + +instance ToJSON StatusEvent where + toJSON StatusEvent{..} = object + [ "id" .= statusEventId + , "sha" .= statusEventSha + , "name" .= statusEventName + , "target_url" .= statusEventTargetUrl + , "context" .= statusEventContext + , "description" .= statusEventDescription + , "state" .= statusEventState + , "commit" .= statusEventCommit + , "branches" .= statusEventBranches + , "created_at" .= statusEventCreatedAt + , "updated_at" .= statusEventUpdatedAt + , "repository" .= statusEventRepository + , "organization" .= statusEventOrganization + , "sender" .= statusEventSender + ] diff --git a/web/GitHub/WebHook/Handler.hs b/web/GitHub/WebHook/Handler.hs new file mode 100644 index 0000000..85dc9f2 --- /dev/null +++ b/web/GitHub/WebHook/Handler.hs @@ -0,0 +1,136 @@ +{-# LANGUAGE OverloadedStrings #-} +module GitHub.WebHook.Handler + ( Handler (..) + , Error (..) + , runHandler + , removeNulls + ) where + +import Control.Applicative (Applicative, pure) +import Crypto.Hash (HMAC, SHA1, digestToHexByteString, hmac, + hmacGetDigest) +import Data.Aeson (ToJSON (..), Value (..), + eitherDecodeStrict') +import Data.Aeson.Types (parseEither) +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as BC8 +import qualified Data.HashMap.Strict as HashMap +import Data.Monoid ((<>)) +import Data.Text (Text) +import qualified Data.Text as Text +import Data.Text.Encoding (decodeUtf8) +import Data.UUID (UUID, fromASCIIBytes) +import qualified Data.Vector as Vector + +import GitHub.Types + + + +data Handler m = Handler + { hSecretKeys :: [String] + -- ^ Secret keys which are used to authenticate the incoming request. + -- If the list is empty then no authentication is required. The handler + -- tries each key until it finds one which works. This makes it easier + -- to rotate keys because you can have multiple ones active at any given + -- point in time. + + , hBody :: m ByteString + -- ^ Action which is used to read the request body. + + , hHeader :: ByteString -> m (Maybe ByteString) + -- ^ Action which is used to retrieve a particular header from the + -- request. + } + + +data Error + = InvalidRequest + -- ^ The incoming request is not well-formed. If that happens it means + -- GitHub screwed something up, or changed the format of the request. + + | ParseError !Text + -- ^ The request looks valid but we failed to parse the payload. This + -- could be because our parsing code is wrong, or because GitHub added + -- a new event type which we don't handle yet. + + | IncompleteParse Value Payload + -- ^ The request looks valid but we failed to parse the payload. This + -- could be because our parsing code is wrong, or because GitHub added + -- a new event type which we don't handle yet. + + | UnsignedRequest + -- ^ We were expecting a signed request but no signature was included. + -- Such requests are rejected beause we don't want to accept requests from + -- untrusted sources. + + | InvalidSignature + -- ^ A signature was included in the request but it did not match the + -- secret key which was providid to the handler. Usually points to + -- a configuration error on either end. + + +toParseError :: String -> Either Error Payload +toParseError = Left . ParseError . Text.pack + + +removeNulls :: ToJSON a => a -> Value +removeNulls = go . toJSON + where + go (Array x) = Array . Vector.map go $ x + go (Object x) = Object . HashMap.map go . HashMap.filter (/= Null) $ x + go x = x + + +toSuccess :: Value -> Payload -> Either Error Payload +toSuccess value payload = + if removeNulls payload == removeNulls value + then Right payload + else Left $ IncompleteParse value payload + + +verifySecretKey :: ByteString -> ByteString -> String -> Bool +verifySecretKey rawBody sig key = sig == ("sha1=" <> digestToHexByteString + (hmacGetDigest (hmac (BC8.pack key) rawBody :: HMAC SHA1))) + + +runHandler :: (Applicative m, Monad m) => Handler m -> m (Either Error (UUID, Payload)) +runHandler h = do + mbDelivery <- pure . (fromASCIIBytes =<<) =<< hHeader h "X-GitHub-Delivery" + + res <- do + rawBody <- hBody h + mbSignature <- hHeader h "X-Hub-Signature" + + authenticatedBody <- pure $ case (hSecretKeys h, mbSignature) of + + -- No secret key and no signature. Pass along the body unverified. + ([], Nothing) -> Right rawBody + + -- Signature is available but no secret keys to verify it. This is + -- not a fatal error, we can still process the event. + ([], Just _) -> Right rawBody + + -- Secret keys are available but the request is not signed. Reject + -- the request. + (_, Nothing) -> Left UnsignedRequest + + -- Both the signature and secret keys are available. Verify the + -- signature with the first key which works, otherwise reject the + -- request. + (secretKeys, Just sig) -> do + if any (verifySecretKey rawBody sig) secretKeys + then Right rawBody + else Left InvalidSignature + + mbEventName <- hHeader h "X-GitHub-Event" + pure $ do + eventName <- maybe (Left InvalidRequest) Right mbEventName + body <- authenticatedBody + case eitherDecodeStrict' body of + Left e -> toParseError e + Right value -> either toParseError (toSuccess value) $ + parseEither (webhookPayloadParser $ decodeUtf8 eventName) value + + pure $ case mbDelivery of + Nothing -> Left InvalidRequest + Just uuid -> fmap ((,) uuid) res diff --git a/web/TokTok/Webhooks.hs b/web/TokTok/Webhooks.hs index 5abd491..ff2b818 100644 --- a/web/TokTok/Webhooks.hs +++ b/web/TokTok/Webhooks.hs @@ -2,6 +2,7 @@ module TokTok.Webhooks (app) where import Control.Applicative ((<$>)) +import qualified Data.Aeson as Aeson import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import qualified Data.CaseInsensitive as CI @@ -9,9 +10,10 @@ import qualified Data.Maybe as Maybe import Data.Monoid (mempty, (<>)) import qualified Data.Text as Text import Data.Text.Encoding (decodeUtf8, encodeUtf8) -import Data.UUID.Types (UUID) +import Data.UUID (UUID) import GitHub.Types.Events (Payload (..)) -import GitHub.WebHook.Handler (Error (..), Handler (..), runHandler) +import GitHub.WebHook.Handler (Error (..), Handler (..), removeNulls, + runHandler) import Network.HTTP.Types (HeaderName, status200, status501) import Network.Wai (Application, Request, Response, requestBody, requestHeaders, @@ -32,6 +34,10 @@ showError UnsignedRequest = "UnsignedRequest" showError InvalidSignature = "InvalidSignature" showError (ParseError err) = "ParseError " <> (LBS.fromStrict . encodeUtf8 . Text.pack . show $ err) +showError (IncompleteParse value payload) = + "IncompleteParse:" + <> "\n- Value: " <> Aeson.encode (removeNulls value ) + <> "\n- Payload: " <> Aeson.encode (removeNulls payload) handleError :: BS.ByteString -> [(HeaderName, BS.ByteString)] -> BS.ByteString -> Error -> IO Response @@ -44,24 +50,25 @@ handleError event headers body err = do return $ responseError res -handlePayload :: BS.ByteString -> UUID -> Payload -> IO Response -handlePayload event uuid payload = do - putStrLn $ "Success in event " ++ show event ++ ": UUID=" ++ show uuid - ++ ", Payload=" ++ show payload +handlePayload :: Bool -> BS.ByteString -> UUID -> Payload -> IO Response +handlePayload isSigned event uuid payload = do + putStrLn $ unsignedMsg ++ "Success in event " ++ show event ++ ": UUID=" ++ show uuid return responseOK + where + unsignedMsg = if isSigned then "" else "[UNSIGNED!] " -handleRequest :: BS.ByteString -> String -> Request -> IO Response -handleRequest event secretKey req = do +handleRequest :: BS.ByteString -> [String] -> Request -> IO Response +handleRequest event secretKeys req = do body <- fullRequestBody mempty parsed <- runHandler Handler - { hSecretKeys = [secretKey] + { hSecretKeys = secretKeys , hBody = return body , hHeader = return . flip lookup (requestHeaders req) . CI.mk } case parsed of Left err -> handleError event (requestHeaders req) body err - Right (uuid, payload) -> handlePayload event uuid payload + Right (uuid, payload) -> handlePayload (not $ null secretKeys) event uuid payload where fullRequestBody body = do @@ -73,28 +80,13 @@ handleRequest event secretKey req = do app :: Application app req respond = - if event `elem` unhandled - then do - -- Respond OK if we can't actually handle this type of event. - putStrLn $ "Ignoring unhandled GitHub event: " ++ show event + case lookup "X-GitHub-Event" . requestHeaders $ req of + Nothing -> do + -- Respond OK if we don't get an event. + putStrLn $ "Ignoring request without event type" respond responseOK - else do + Just event -> do putStrLn $ "Handling GitHub event: " ++ show event -- Get the github secret keys. secretKeys <- Maybe.maybeToList <$> lookupEnv "GITHUB_SECRET" - -- If we didn't configure any keys, we just say OK to everything but - -- don't actually handle any events. - case secretKeys of - [secretKey] -> respond =<< handleRequest event secretKey req - _ -> respond responseOK - - where - event = Maybe.fromMaybe "" . lookup "X-GitHub-Event" . requestHeaders $ req - - -- The list of unhandled event types. Each of them should have an - -- associated issue we can track, so we can eventually handle all events. - unhandled = - [ "" -- We ignore the empty string event (no event passed). - , "ping" -- https://github.com/wereHamster/github-types/issues/3 - , "status" -- https://github.com/wereHamster/github-types/issues/4 - ] + respond =<< handleRequest event secretKeys req