{ "name": "GitHub User Profile to Slack", "nodes": [ { "id": "github-user-profile-to-slack-webhook", "name": "Receive a GitHub profile request", "type": "n8n-nodes-base.webhook", "typeVersion": 1, "position": [ 100, 240 ], "parameters": { "authentication": "none", "httpMethod": "POST", "path": "github-user-profile-to-slack", "responseMode": "onReceived", "responseData": "firstEntryJson", "options": { "responseData": "Request accepted", "responseCode": 200 } } }, { "id": "github-user-profile-to-slack-query", "name": "Prepare the bounded GitHub query", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 340, 240 ], "parameters": { "mode": "runOnceForEachItem", "jsCode": "function githubUserQuery({ body }) {\n if (!body || typeof body !== 'object' || Array.isArray(body)) throw new Error('Expected a JSON object containing text and channel_id');\n const username = body.text, channelId = body.channel_id;\n if (typeof username !== 'string' || !/^[a-z\\d](?:[a-z\\d-]{0,37}[a-z\\d])?$/i.test(username)) throw new Error('text must be one GitHub username, without whitespace');\n if (typeof channelId !== 'string' || !/^[CDG][A-Z0-9]{2,31}$/.test(channelId)) throw new Error('channel_id must identify a Slack conversation accessible to the selected connection');\n const query = 'query LookupGitHubUser($login: String!) { user(login: $login) { name company location avatarUrl email pullRequests(last: 25) { edges { node { commits(last: 25) { nodes { commit { author { email name } } } } } } } } }';\n return { username, channelId, request: { query, variables: { login: username }, operationName: 'LookupGitHubUser' } };\n}\n\nreturn {json:githubUserQuery({body:$json.body})};" } }, { "id": "github-user-profile-to-slack-read", "name": "Read the GitHub profile", "type": "n8n-nodes-base.httpRequest", "typeVersion": 4.2, "position": [ 580, 240 ], "parameters": { "method": "POST", "url": "https://api.github.com/graphql", "authentication": "predefinedCredentialType", "nodeCredentialType": "githubApi", "sendBody": true, "contentType": "json", "specifyBody": "json", "jsonBody": "={{ $json.request }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "Accept", "value": "application/vnd.github+json" }, { "name": "User-Agent", "value": "n8n" } ] }, "options": { "response": { "response": { "responseFormat": "json", "fullResponse": true } }, "timeout": 30000, "redirect": { "redirect": { "followRedirects": false } } } } }, { "id": "github-user-profile-to-slack-profile", "name": "Collect the matching profile emails", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 820, 240 ], "parameters": { "mode": "runOnceForEachItem", "jsCode": "function githubUserProfile({ body, statusCode, bodyEncoding, username }) {\n if (!Number.isInteger(statusCode) || statusCode < 200 || statusCode >= 300) throw new Error('GitHub profile query failed with HTTP ' + statusCode);\n if (bodyEncoding != null || !body || typeof body !== 'object' || Array.isArray(body)) throw new Error('Expected a JSON GitHub GraphQL response');\n if (body.errors != null && (!Array.isArray(body.errors) || body.errors.length)) throw new Error('GitHub returned GraphQL errors; partial profile data is not posted');\n const profile = body.data?.user;\n if (!profile || typeof profile !== 'object' || Array.isArray(profile)) throw new Error('GitHub did not return the requested user');\n for (const key of ['name', 'company', 'location']) if (profile[key] != null && typeof profile[key] !== 'string') throw new Error('Invalid GitHub profile ' + key);\n if (typeof profile.email !== 'string' || typeof profile.avatarUrl !== 'string' || !/^https:\\/\\/[^\\s/?#@<>\"'\\\\]+(?:[/?#][^\\s<>\"'\\\\]*)?$/.test(profile.avatarUrl)) throw new Error('Invalid GitHub profile email or avatar URL');\n const edges = profile.pullRequests?.edges;\n if (!Array.isArray(edges) || edges.length > 25) throw new Error('Expected at most 25 pull requests');\n const candidates = profile.email ? [profile.email] : []; let commitsScanned = 0;\n for (const edge of edges) {\n const nodes = edge?.node?.commits?.nodes;\n if (!Array.isArray(nodes) || nodes.length > 25) throw new Error('Expected at most 25 commits per pull request');\n for (const node of nodes) {\n commitsScanned++;\n if (!node?.commit || typeof node.commit !== 'object') throw new Error('Invalid GitHub pull-request commit');\n const author = node.commit.author;\n if (author == null) continue;\n if (typeof author !== 'object' || (author.name != null && typeof author.name !== 'string') || (author.email != null && typeof author.email !== 'string')) throw new Error('Invalid GitHub commit author');\n const matches = typeof author.name === 'string' && ((typeof profile.name === 'string' && profile.name.length > 0 && author.name.includes(profile.name)) || author.name === username);\n if (matches && author.email) candidates.push(author.email);\n }\n }\n const emails = [...new Set(candidates)].filter(email => !/^[^@]+@(?:users\\.)?noreply\\.github\\.com$/i.test(email));\n const escape = text => text.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');\n const text = 'Name: ' + (profile.name ?? '') + '\\nEmail: ' + emails.join(', ') + '\\nCompany: ' + (profile.company ?? '') + '\\nLocation: ' + (profile.location ?? '');\n const chunks = []; let chunk = '';\n for (const character of text) { const escaped = escape(character); if (chunk.length + escaped.length > 3800) { chunks.push(chunk); chunk = ''; } chunk += escaped; }\n if (chunk) chunks.push(chunk);\n const title = 'GitHub Details for: ' + username;\n const parts = chunks.map((content, index) => [{ title: chunks.length > 1 ? title + ' (' + (index + 1) + '/' + chunks.length + ')' : title, text: content, fallback: title, mrkdwn_in: [], ...(index === 0 ? { thumb_url: profile.avatarUrl } : {}) }]);\n return { profile, emails, parts, pullRequestsScanned: edges.length, commitsScanned };\n}\n\nconst request=$(\"Prepare the bounded GitHub query\").first().json;const result=githubUserProfile({body:$json.body,statusCode:$json.statusCode,bodyEncoding:null,username:request.username});return {json:{...result,channelId:request.channelId}};" } }, { "id": "github-user-profile-to-slack-parts", "name": "List the profile attachments", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1060, 240 ], "parameters": { "mode": "runOnceForAllItems", "jsCode": "const items=$input.all();if(items.length!==1)throw new Error(\"Expected one profile result\");const result=items[0].json;return result.parts.map((attachments,index)=>({json:{attachments,index:index+1,channelId:result.channelId},pairedItem:{item:0}}));" } }, { "id": "github-user-profile-to-slack-loop", "name": "Post profile parts one at a time", "type": "n8n-nodes-base.splitInBatches", "typeVersion": 3, "position": [ 1300, 240 ], "parameters": { "batchSize": 1, "options": {} } }, { "id": "github-user-profile-to-slack-slack", "name": "Post a profile attachment", "type": "n8n-nodes-base.slack", "typeVersion": 1, "position": [ 1540, 240 ], "parameters": { "authentication": "accessToken", "resource": "message", "operation": "post", "channel": "={{ $json.channelId }}", "text": "", "jsonParameters": false, "attachments": "={{ $json.attachments }}", "otherOptions": {} } }, { "id": "github-user-profile-to-slack-receipt", "name": "Confirm the Slack profile message", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 1780, 240 ], "parameters": { "mode": "runOnceForEachItem", "jsCode": "const requests=$(\"List the profile attachments\").all(),summary=$(\"Collect the matching profile emails\").first().json;if(!Number.isInteger($runIndex)||$runIndex<0||$runIndex>=requests.length)throw new Error(\"Invalid profile part index\");if($json.ok!==true||typeof $json.ts!==\"string\"||!/^\\d+\\.\\d+$/.test($json.ts)||$json.channel!==summary.channelId)throw new Error(\"Slack did not confirm the requested channel; a message may already exist\");return {json:{part:requests[$runIndex].json.index,profile:summary.profile,emails:summary.emails,pullRequestsScanned:summary.pullRequestsScanned,commitsScanned:summary.commitsScanned,message:$json}};" } } ], "active": false, "settings": { "executionOrder": "v1" }, "connections": { "Receive a GitHub profile request": { "main": [ [ { "node": "Prepare the bounded GitHub query", "type": "main", "index": 0 } ] ] }, "Prepare the bounded GitHub query": { "main": [ [ { "node": "Read the GitHub profile", "type": "main", "index": 0 } ] ] }, "Read the GitHub profile": { "main": [ [ { "node": "Collect the matching profile emails", "type": "main", "index": 0 } ] ] }, "Collect the matching profile emails": { "main": [ [ { "node": "List the profile attachments", "type": "main", "index": 0 } ] ] }, "List the profile attachments": { "main": [ [ { "node": "Post profile parts one at a time", "type": "main", "index": 0 } ] ] }, "Post profile parts one at a time": { "main": [ [], [ { "node": "Post a profile attachment", "type": "main", "index": 0 } ] ] }, "Post a profile attachment": { "main": [ [ { "node": "Confirm the Slack profile message", "type": "main", "index": 0 } ] ] }, "Confirm the Slack profile message": { "main": [ [ { "node": "Post profile parts one at a time", "type": "main", "index": 0 } ] ] } }, "pinData": {} }