{"action":"created","comment":{"url":"https://api.github.com/repos/serokell/github-api/pulls/comments/207656756","pull_request_review_id":143308915,"id":207656756,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDIwNzY1Njc1Ng==","diff_hunk":"@@ -0,0 +1,152 @@\n+{-# LANGUAGE TemplateHaskell #-}\n+\n+module GitHub.App\n+  (\n+   , InstallationAuth \n+   , authenticateInstallation\n+   , createInstToken\n+  ) where\n+\n+import Universum hiding          (exp, Option)\n+import Crypto.Types.PubKey.RSA   (PrivateKey(..))\n+import Data.Aeson                (FromJSON (..), withObject, (.:))\n+import Data.Default.Class        (def)\n+import GitHub.Auth               (Auth (..))\n+import Lens.Micro.Platform       (makeLenses, (^.))\n+import Network.HTTP.Req          (POST (..), NoReqBody (..), Option, Url, header, https,\n+                                   jsonResponse, req, responseBody, runReq, (/:))\n+import Data.Time.Clock           (UTCTime (..))\n+import Data.Time.Clock.POSIX     (POSIXTime,getPOSIXTime, utcTimeToPOSIXSeconds)\n+import Web.JWT                   (Signer(..), JSON, JWTClaimsSet(..), encodeSigned, numericDate, stringOrURI)\n+import qualified Prelude as Show (Show, show)\n+import qualified  Data.Text as T (takeWhileEnd, takeWhile)\n+\n+-- | Json Web Token expiration time. Maximun accepted by github is 10 minutes. \n+jwtExpTime :: POSIXTime\n+jwtExpTime = 600.0 \n+\n+-- | Installation key expiration time. It is fixed by github and is equal to 1 hour. \n+instKeyExpTime :: POSIXTime\n+instKeyExpTime = 3600.0\n+\n+-- | Time preserved to be on the safe side. \n+-- if expiration time of installation auth token <= current time + bufferTime \n+-- then installation Auth token shoul be updated. \n+bufferTime :: POSIXTime \n+bufferTime = instKeyExpTime * 0.25\n+\n+-- | base URL of api\n+baseURL :: Text\n+baseURL = \"api.github.com\"\n+\n+type BaseURL = Text \n+type InstallationId = Text \n+\n+\n+-- | Github installation Auth token \n+data GTAToken = GTAToken \n+    {\n+        instToken  :: !Text,      -- ^ Installation token \n+        expireAt   :: !POSIXTime  -- ^ expiration time \n+    }\n+\n+-- | Main installation auth info for application. \n+-- it containes immutable app information and app installation token\n+-- which shoud be updated at least once per hour  \n+data InstallationAuth = InstallationAuth \n+    {\n+        _appId          :: !Int,           -- ^ application id \n+        _appPrivateKey  :: !PrivateKey,    -- ^ Private key to sign token requests  \n+        _installationId :: !Text,          -- ^ Installation id \n+        _token          :: IORef GTAToken  -- ^ Installation Auth token. \n+    } \n+\n+makeLenses '' InstallationAuth\n+\n+instance FromJSON GTAToken where\n+    parseJSON = withObject \"Github installation token\" $ \\o -> do \n+        instToken   <- o .: \"token\"  \n+        expireAtM   <- expPOSIXtime <$> o .: \"expires_at\"\n+        case expireAtM of \n+            Nothing       -> fail \"Failed to parse expiraion time.\"\n+            Just expireAt -> return GTAToken{..}\n+\n+-- | newtype for failed JSON Web token generation.\n+newtype FailedGenerateJWT = FailedGenerateJWT Text\n+\n+instance Show.Show FailedGenerateJWT where\n+    show (FailedGenerateJWT err) = toString err\n+\n+instance Exception FailedGenerateJWT\n+\n+-- | Checks if current app installation token is not expired. If it is not, than it just returns token \n+-- with OAuth wrapping. \n+-- Otherwise it call function that gets new token from github and writes it to IORef, and also returns that new token. \n+authenticateInstallation :: InstallationAuth -> IO Auth\n+authenticateInstallation instAuth = do \n+    gtaToken    <- readIORef (instAuth ^. token)\n+    currentTime <- getPOSIXTime\n+    if (expireAt gtaToken) - currentTime >= bufferTime \n+    then return $ OAuth $ encodeUtf8 $ instToken gtaToken\n+    else do \n+        let applicationId = instAuth ^. appId\n+        OAuth <$> encodeUtf8 <$> instToken <$> (join $ readIORef <$> renewInstAuthToken instAuth) \n+\n+\n+-- | Gets new token from github and writes it to IORef in given InstallationAuth. Also returns that token. \n+renewInstAuthToken :: InstallationAuth -> IO (IORef GTAToken) \n+renewInstAuthToken instAuth = do \n+    time    <- getPOSIXTime\n+    let tkn = instAuth ^. token\n+    case (makeJWT time (instAuth ^. appId) (instAuth ^. appPrivateKey)) of \n+        Left  err  -> throwM (FailedGenerateJWT err) \n+        Right jwt  -> do \n+            t <- request (https baseURL /: \"installations\" /: (instAuth ^. installationId) /: \"access_tokens\") mempty jwt    \n+            writeIORef tkn t \n+            return tkn\n+\n+-- | Creates new InstallationAuth value. Useing this function is the only way to create InstallationAuth value, \n+-- because we don't export constructors. \n+createInstAuth :: Int -> PrivateKey -> InstallationId -> IO InstallationAuth ","path":"src/GitHub/App.hs","position":110,"original_position":110,"commit_id":"cf31aa5e02d1ce593cdb0fb3712bf0a4234d5fcc","original_commit_id":"cf31aa5e02d1ce593cdb0fb3712bf0a4234d5fcc","user":{"login":"kirelagin","id":451835,"node_id":"MDQ6VXNlcjQ1MTgzNQ==","avatar_url":"https://avatars1.githubusercontent.com/u/451835?v=4","gravatar_id":"","url":"https://api.github.com/users/kirelagin","html_url":"https://github.com/kirelagin","followers_url":"https://api.github.com/users/kirelagin/followers","following_url":"https://api.github.com/users/kirelagin/following{/other_user}","gists_url":"https://api.github.com/users/kirelagin/gists{/gist_id}","starred_url":"https://api.github.com/users/kirelagin/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kirelagin/subscriptions","organizations_url":"https://api.github.com/users/kirelagin/orgs","repos_url":"https://api.github.com/users/kirelagin/repos","events_url":"https://api.github.com/users/kirelagin/events{/privacy}","received_events_url":"https://api.github.com/users/kirelagin/received_events","type":"User","site_admin":false},"body":"Isn’t it the same function as above?","created_at":"2018-08-03T20:19:58Z","updated_at":"2018-08-03T20:26:28Z","html_url":"https://github.com/serokell/github-api/pull/2#discussion_r207656756","pull_request_url":"https://api.github.com/repos/serokell/github-api/pulls/2","author_association":"MEMBER","_links":{"self":{"href":"https://api.github.com/repos/serokell/github-api/pulls/comments/207656756"},"html":{"href":"https://github.com/serokell/github-api/pull/2#discussion_r207656756"},"pull_request":{"href":"https://api.github.com/repos/serokell/github-api/pulls/2"}}},"pull_request":{"url":"https://api.github.com/repos/serokell/github-api/pulls/2","id":205185825,"node_id":"MDExOlB1bGxSZXF1ZXN0MjA1MTg1ODI1","html_url":"https://github.com/serokell/github-api/pull/2","diff_url":"https://github.com/serokell/github-api/pull/2.diff","patch_url":"https://github.com/serokell/github-api/pull/2.patch","issue_url":"https://api.github.com/repos/serokell/github-api/issues/2","number":2,"state":"open","locked":false,"title":"The first commit. Added functions, which get (and update when it is n…","user":{"login":"Haskell-mouse","id":29661856,"node_id":"MDQ6VXNlcjI5NjYxODU2","avatar_url":"https://avatars1.githubusercontent.com/u/29661856?v=4","gravatar_id":"","url":"https://api.github.com/users/Haskell-mouse","html_url":"https://github.com/Haskell-mouse","followers_url":"https://api.github.com/users/Haskell-mouse/followers","following_url":"https://api.github.com/users/Haskell-mouse/following{/other_user}","gists_url":"https://api.github.com/users/Haskell-mouse/gists{/gist_id}","starred_url":"https://api.github.com/users/Haskell-mouse/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Haskell-mouse/subscriptions","organizations_url":"https://api.github.com/users/Haskell-mouse/orgs","repos_url":"https://api.github.com/users/Haskell-mouse/repos","events_url":"https://api.github.com/users/Haskell-mouse/events{/privacy}","received_events_url":"https://api.github.com/users/Haskell-mouse/received_events","type":"User","site_admin":false},"body":"Added functions, which get (and update when it is needed) app installation tokens from github.","created_at":"2018-07-31T17:24:46Z","updated_at":"2018-08-03T20:26:28Z","closed_at":null,"merged_at":null,"merge_commit_sha":"adb3210a6773ce400c5fcf9a338e94b08d4edbee","assignee":null,"assignees":[],"requested_reviewers":[{"login":"artemohanjanyan","id":7469923,"node_id":"MDQ6VXNlcjc0Njk5MjM=","avatar_url":"https://avatars3.githubusercontent.com/u/7469923?v=4","gravatar_id":"","url":"https://api.github.com/users/artemohanjanyan","html_url":"https://github.com/artemohanjanyan","followers_url":"https://api.github.com/users/artemohanjanyan/followers","following_url":"https://api.github.com/users/artemohanjanyan/following{/other_user}","gists_url":"https://api.github.com/users/artemohanjanyan/gists{/gist_id}","starred_url":"https://api.github.com/users/artemohanjanyan/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/artemohanjanyan/subscriptions","organizations_url":"https://api.github.com/users/artemohanjanyan/orgs","repos_url":"https://api.github.com/users/artemohanjanyan/repos","events_url":"https://api.github.com/users/artemohanjanyan/events{/privacy}","received_events_url":"https://api.github.com/users/artemohanjanyan/received_events","type":"User","site_admin":false}],"requested_teams":[],"labels":[],"milestone":null,"commits_url":"https://api.github.com/repos/serokell/github-api/pulls/2/commits","review_comments_url":"https://api.github.com/repos/serokell/github-api/pulls/2/comments","review_comment_url":"https://api.github.com/repos/serokell/github-api/pulls/comments{/number}","comments_url":"https://api.github.com/repos/serokell/github-api/issues/2/comments","statuses_url":"https://api.github.com/repos/serokell/github-api/statuses/cf31aa5e02d1ce593cdb0fb3712bf0a4234d5fcc","head":{"label":"serokell:rinat/AppInstallationAuth","ref":"rinat/AppInstallationAuth","sha":"cf31aa5e02d1ce593cdb0fb3712bf0a4234d5fcc","user":{"login":"serokell","id":13840520,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzODQwNTIw","avatar_url":"https://avatars2.githubusercontent.com/u/13840520?v=4","gravatar_id":"","url":"https://api.github.com/users/serokell","html_url":"https://github.com/serokell","followers_url":"https://api.github.com/users/serokell/followers","following_url":"https://api.github.com/users/serokell/following{/other_user}","gists_url":"https://api.github.com/users/serokell/gists{/gist_id}","starred_url":"https://api.github.com/users/serokell/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serokell/subscriptions","organizations_url":"https://api.github.com/users/serokell/orgs","repos_url":"https://api.github.com/users/serokell/repos","events_url":"https://api.github.com/users/serokell/events{/privacy}","received_events_url":"https://api.github.com/users/serokell/received_events","type":"Organization","site_admin":false},"repo":{"id":142762850,"node_id":"MDEwOlJlcG9zaXRvcnkxNDI3NjI4NTA=","name":"github-api","full_name":"serokell/github-api","owner":{"login":"serokell","id":13840520,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzODQwNTIw","avatar_url":"https://avatars2.githubusercontent.com/u/13840520?v=4","gravatar_id":"","url":"https://api.github.com/users/serokell","html_url":"https://github.com/serokell","followers_url":"https://api.github.com/users/serokell/followers","following_url":"https://api.github.com/users/serokell/following{/other_user}","gists_url":"https://api.github.com/users/serokell/gists{/gist_id}","starred_url":"https://api.github.com/users/serokell/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serokell/subscriptions","organizations_url":"https://api.github.com/users/serokell/orgs","repos_url":"https://api.github.com/users/serokell/repos","events_url":"https://api.github.com/users/serokell/events{/privacy}","received_events_url":"https://api.github.com/users/serokell/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/serokell/github-api","description":"Haskell API for authenticating as a GitHub App","fork":false,"url":"https://api.github.com/repos/serokell/github-api","forks_url":"https://api.github.com/repos/serokell/github-api/forks","keys_url":"https://api.github.com/repos/serokell/github-api/keys{/key_id}","collaborators_url":"https://api.github.com/repos/serokell/github-api/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/serokell/github-api/teams","hooks_url":"https://api.github.com/repos/serokell/github-api/hooks","issue_events_url":"https://api.github.com/repos/serokell/github-api/issues/events{/number}","events_url":"https://api.github.com/repos/serokell/github-api/events","assignees_url":"https://api.github.com/repos/serokell/github-api/assignees{/user}","branches_url":"https://api.github.com/repos/serokell/github-api/branches{/branch}","tags_url":"https://api.github.com/repos/serokell/github-api/tags","blobs_url":"https://api.github.com/repos/serokell/github-api/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/serokell/github-api/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/serokell/github-api/git/refs{/sha}","trees_url":"https://api.github.com/repos/serokell/github-api/git/trees{/sha}","statuses_url":"https://api.github.com/repos/serokell/github-api/statuses/{sha}","languages_url":"https://api.github.com/repos/serokell/github-api/languages","stargazers_url":"https://api.github.com/repos/serokell/github-api/stargazers","contributors_url":"https://api.github.com/repos/serokell/github-api/contributors","subscribers_url":"https://api.github.com/repos/serokell/github-api/subscribers","subscription_url":"https://api.github.com/repos/serokell/github-api/subscription","commits_url":"https://api.github.com/repos/serokell/github-api/commits{/sha}","git_commits_url":"https://api.github.com/repos/serokell/github-api/git/commits{/sha}","comments_url":"https://api.github.com/repos/serokell/github-api/comments{/number}","issue_comment_url":"https://api.github.com/repos/serokell/github-api/issues/comments{/number}","contents_url":"https://api.github.com/repos/serokell/github-api/contents/{+path}","compare_url":"https://api.github.com/repos/serokell/github-api/compare/{base}...{head}","merges_url":"https://api.github.com/repos/serokell/github-api/merges","archive_url":"https://api.github.com/repos/serokell/github-api/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/serokell/github-api/downloads","issues_url":"https://api.github.com/repos/serokell/github-api/issues{/number}","pulls_url":"https://api.github.com/repos/serokell/github-api/pulls{/number}","milestones_url":"https://api.github.com/repos/serokell/github-api/milestones{/number}","notifications_url":"https://api.github.com/repos/serokell/github-api/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/serokell/github-api/labels{/name}","releases_url":"https://api.github.com/repos/serokell/github-api/releases{/id}","deployments_url":"https://api.github.com/repos/serokell/github-api/deployments","created_at":"2018-07-29T13:23:01Z","updated_at":"2018-07-30T16:34:58Z","pushed_at":"2018-07-31T17:28:24Z","git_url":"git://github.com/serokell/github-api.git","ssh_url":"git@github.com:serokell/github-api.git","clone_url":"https://github.com/serokell/github-api.git","svn_url":"https://github.com/serokell/github-api","homepage":"","size":8,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"master"}},"base":{"label":"serokell:master","ref":"master","sha":"b5739dfa639664d5d1e3f5bebf86101cfb189602","user":{"login":"serokell","id":13840520,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzODQwNTIw","avatar_url":"https://avatars2.githubusercontent.com/u/13840520?v=4","gravatar_id":"","url":"https://api.github.com/users/serokell","html_url":"https://github.com/serokell","followers_url":"https://api.github.com/users/serokell/followers","following_url":"https://api.github.com/users/serokell/following{/other_user}","gists_url":"https://api.github.com/users/serokell/gists{/gist_id}","starred_url":"https://api.github.com/users/serokell/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serokell/subscriptions","organizations_url":"https://api.github.com/users/serokell/orgs","repos_url":"https://api.github.com/users/serokell/repos","events_url":"https://api.github.com/users/serokell/events{/privacy}","received_events_url":"https://api.github.com/users/serokell/received_events","type":"Organization","site_admin":false},"repo":{"id":142762850,"node_id":"MDEwOlJlcG9zaXRvcnkxNDI3NjI4NTA=","name":"github-api","full_name":"serokell/github-api","owner":{"login":"serokell","id":13840520,"node_id":"MDEyOk9yZ2FuaXphdGlvbjEzODQwNTIw","avatar_url":"https://avatars2.githubusercontent.com/u/13840520?v=4","gravatar_id":"","url":"https://api.github.com/users/serokell","html_url":"https://github.com/serokell","followers_url":"https://api.github.com/users/serokell/followers","following_url":"https://api.github.com/users/serokell/following{/other_user}","gists_url":"https://api.github.com/users/serokell/gists{/gist_id}","starred_url":"https://api.github.com/users/serokell/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/serokell/subscriptions","organizations_url":"https://api.github.com/users/serokell/orgs","repos_url":"https://api.github.com/users/serokell/repos","events_url":"https://api.github.com/users/serokell/events{/privacy}","received_events_url":"https://api.github.com/users/serokell/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/serokell/github-api","description":"Haskell API for authenticating as a GitHub App","fork":false,"url":"https://api.github.com/repos/serokell/github-api","forks_url":"https://api.github.com/repos/serokell/github-api/forks","keys_url":"https://api.github.com/repos/serokell/github-api/keys{/key_id}","collaborators_url":"https://api.github.com/repos/serokell/github-api/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/serokell/github-api/teams","hooks_url":"https://api.github.com/repos/serokell/github-api/hooks","issue_events_url":"https://api.github.com/repos/serokell/github-api/issues/events{/number}","events_url":"https://api.github.com/repos/serokell/github-api/events","assignees_url":"https://api.github.com/repos/serokell/github-api/assignees{/user}","branches_url":"https://api.github.com/repos/serokell/github-api/branches{/branch}","tags_url":"https://api.github.com/repos/serokell/github-api/tags","blobs_url":"https://api.github.com/repos/serokell/github-api/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/serokell/github-api/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/serokell/github-api/git/refs{/sha}","trees_url":"https://api.github.com/repos/serokell/github-api/git/trees{/sha}","statuses_url":"https://api.github.com/repos/serokell/github-api/statuses/{sha}","languages_url":"https://api.github.com/repos/serokell/github-api/languages","stargazers_url":"https://api.github.com/repos/serokell/github-api/stargazers","contributors_url":"https://api.github.com/repos/serokell/github-api/contributors","subscribers_url":"https://api.github.com/repos/serokell/github-api/subscribers","subscription_url":"https://api.github.com/repos/serokell/github-api/subscription","commits_url":"https://api.github.com/repos/serokell/github-api/commits{/sha}","git_commits_url":"https://api.github.com/repos/serokell/github-api/git/commits{/sha}","comments_url":"https://api.github.com/repos/serokell/github-api/comments{/number}","issue_comment_url":"https://api.github.com/repos/serokell/github-api/issues/comments{/number}","contents_url":"https://api.github.com/repos/serokell/github-api/contents/{+path}","compare_url":"https://api.github.com/repos/serokell/github-api/compare/{base}...{head}","merges_url":"https://api.github.com/repos/serokell/github-api/merges","archive_url":"https://api.github.com/repos/serokell/github-api/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/serokell/github-api/downloads","issues_url":"https://api.github.com/repos/serokell/github-api/issues{/number}","pulls_url":"https://api.github.com/repos/serokell/github-api/pulls{/number}","milestones_url":"https://api.github.com/repos/serokell/github-api/milestones{/number}","notifications_url":"https://api.github.com/repos/serokell/github-api/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/serokell/github-api/labels{/name}","releases_url":"https://api.github.com/repos/serokell/github-api/releases{/id}","deployments_url":"https://api.github.com/repos/serokell/github-api/deployments","created_at":"2018-07-29T13:23:01Z","updated_at":"2018-07-30T16:34:58Z","pushed_at":"2018-07-31T17:28:24Z","git_url":"git://github.com/serokell/github-api.git","ssh_url":"git@github.com:serokell/github-api.git","clone_url":"https://github.com/serokell/github-api.git","svn_url":"https://github.com/serokell/github-api","homepage":"","size":8,"stargazers_count":0,"watchers_count":0,"language":null,"has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":2,"license":null,"forks":0,"open_issues":2,"watchers":0,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/serokell/github-api/pulls/2"},"html":{"href":"https://github.com/serokell/github-api/pull/2"},"issue":{"href":"https://api.github.com/repos/serokell/github-api/issues/2"},"comments":{"href":"https://api.github.com/repos/serokell/github-api/issues/2/comments"},"review_comments":{"href":"https://api.github.com/repos/serokell/github-api/pulls/2/comments"},"review_comment":{"href":"https://api.github.com/repos/serokell/github-api/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/serokell/github-api/pulls/2/commits"},"statuses":{"href":"https://api.github.com/repos/serokell/github-api/statuses/cf31aa5e02d1ce593cdb0fb3712bf0a4234d5fcc"}},"author_association":"COLLABORATOR"}}
{"actor":{"display_login":"kirelagin"}}