From 0aaa9dc2c8dd56f6a9f3705af7d305c21f6661f4 Mon Sep 17 00:00:00 2001 From: Chandan Rai Date: Thu, 11 Feb 2021 19:03:29 +0530 Subject: [PATCH 01/19] Add auth command --- auth/README.md | 2 + auth/authCheck.js | 161 +++++++++++++ auth/commands.yaml | 32 +++ auth/encrypt.js | 24 ++ auth/kv.js | 214 +++++++++++++++++ auth/packages/auth/auth/.include | 3 + auth/packages/auth/auth/index.js | 347 +++++++++++++++++++++++++++ auth/packages/auth/auth/package.json | 12 + auth/packages/auth/callback/index.js | 108 +++++++++ auth/project.yml | 7 + 10 files changed, 910 insertions(+) create mode 100644 auth/README.md create mode 100644 auth/authCheck.js create mode 100644 auth/commands.yaml create mode 100644 auth/encrypt.js create mode 100644 auth/kv.js create mode 100644 auth/packages/auth/auth/.include create mode 100644 auth/packages/auth/auth/index.js create mode 100644 auth/packages/auth/auth/package.json create mode 100644 auth/packages/auth/callback/index.js create mode 100644 auth/project.yml diff --git a/auth/README.md b/auth/README.md new file mode 100644 index 00000000..67c82af7 --- /dev/null +++ b/auth/README.md @@ -0,0 +1,2 @@ +# Auth Command Set + diff --git a/auth/authCheck.js b/auth/authCheck.js new file mode 100644 index 00000000..611f463d --- /dev/null +++ b/auth/authCheck.js @@ -0,0 +1,161 @@ +const nimbella = require('./oauth/node_modules/@nimbella/sdk') +const kv = nimbella.redis() +const baseUrl = 'https://github.com' +const authorizePath = '/login/oauth/authorize' +const scope = 'user:email,read:org' +const allowSignup = 'false' +// const authorizationUrl = `${baseUrl}${authorizePath}?client_id=${process.env.OAUTH_CLIENT_ID}&scope=${scope}&allow_signup=${allowSignup}` +const sessionExpiration = 1 * 60 // ttl in seconds +const https = require('https') +const { URL } = require('url') + + +// Generates error response. This is not intended for slack and usually indicates +// an invalid request. +function generateErrorObject(message, statusCode) { + return { + statusCode: 400 || statusCode, + body: { + error: message + } + } +} + +// When the request contains an access token, it is the result of an asynchronous +// call back. This will indicate that the handler should rehydrate the state then +// call the command handler. When the handler completes, we must post the result +// back using the saved webhook. +// +// This function will attempt to save the access token for a predetermined amount +// of time so that it may be reused without forcing the same user to re-authenticate. +async function rehydrate(access_token, state) { + return kv + .getAsync(state) + .then(JSON.parse) + .then(async state => { + if (state && state.savedArgs) { + try { + // remember the access token for a some duration + const userid = state.savedArgs.params.__client.user_id + await kv.setexAsync(userid, sessionExpiration, JSON.stringify({ access_token })) + } catch (e) { + console.error('could not save access token', e) + } + + return state.savedArgs + } else { + return Promise.reject('Your session has expired') + } + }) +} + +// For the user identified in the request, check if there is a previously saved +// access token. If so, return it. Otherwise return undefined and allow handler +// to initiate a new oauth flow. +async function previouslyAuthorized(event) { + try { + const userid = event.params.__client.user_id + return kv + .getAsync(userid) + .then(JSON.parse) + .then(_ => { + if (_ && _.access_token) { + return _.access_token + } else return undefined + }) + } catch (e) { + console.error('Invalid request (missing expected properties)') + return undefined + } +} + +async function postResult(webhook, result) { + const url = new URL(webhook) + return new Promise(function (resolve, reject) { + const data = JSON.stringify(result) + https.request({ + method: 'POST', + host: url.host, + path: url.pathname, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length + } + }, (resp) => { + let buffer = '' + + resp.on('data', (chunk) => { + buffer += chunk + }) + + resp.on('end', () => { + try { + resolve({ body: buffer }) + } catch (e) { + console.error(e, buffer) + reject(generateErrorObject('Unexpected response')) + } + }) + }).on('error', reject) + .write(data) + .end() + }) +} + +function validEvent(event) { + return event.params + && event.params.__client + && event.params.__client.user_id + && event.params.__client.response_url + && event.params.__client.name === 'slack' +} + +const authCheck = async (event, _command) => { + try { + // check if this is an asynchronous call back from an oauth flow + // in which case the state and access_tokens are available in the event + if (event.state && event.access_token) { + // rehydrate, then execute the command with the access token + const savedArgs = await rehydrate(event.access_token, event.state) + const result = await _command(event.access_token, savedArgs.params, savedArgs.commandText, savedArgs.__secrets || {}) + const webhook = savedArgs.params.__client.response_url + return postResult(webhook, result) + } else if (validEvent(event)) { + // this is a synchronous inbound event from slack, check if there is an + // access token available for the slack user who initiated this request + const access_token = await previouslyAuthorized(event) + if (access_token) { + // previously authorized, execute the command and return the result + return _command(access_token, event.params, event.commandText, event.__secrets || {}) + } else { + // kick off authorization flow + const state = process.env.__OW_ACTIVATION_ID + return kv + .setexAsync(state, sessionExpiration, JSON.stringify({savedArgs: event, callback: `${process.env.__OW_ACTION_NAME}`})) + .then(result => { + if (result === 'OK') { + return { + response_type: 'ephemeral', + text: `You need to authenticate to perform this operation. Please click this <${authorizationUrl}&state=${state}|link> to continue.` + } + } else { + console.log('kv set failed with result:', result) + return { + response_type: 'ephemeral', + text: `This operation requires you to authenticate but could not create a session for you.\n` + + `If you are authorized to inspect the activation logs, check them for details or contact your Commander admin.` + } + } + }) + } + } else { + console.error('Invalid request', event) + return generateErrorObject('Invalid request') + } + } catch (error) { + console.error(error) + return generateErrorObject('Unexpected error') + } +} + +module.exports = authCheck diff --git a/auth/commands.yaml b/auth/commands.yaml new file mode 100644 index 00000000..a62c517e --- /dev/null +++ b/auth/commands.yaml @@ -0,0 +1,32 @@ +commands: + auth: + description: Add, Remove and List Identity Providers, Specify which provider to use for how long. + parameters: + - name: action + - name: entity + optional: true + options: + - name: n + value: provider_name + - name: g + value: grant_type + - name: c + value: callback_url + - name: a + value: auth_url + - name: b + value: base_url + - name: t + value: access_token_url + - name: i + value: client_id + - name: s + value: client_secret + - name: p + value: scope + - name: d + value: duration + callback: + description: callback for oauth flow. + annotations: + provide-api-key: true diff --git a/auth/encrypt.js b/auth/encrypt.js new file mode 100644 index 00000000..a3e7396c --- /dev/null +++ b/auth/encrypt.js @@ -0,0 +1,24 @@ +const crypto = require('crypto'); +const algorithm = 'aes-256-cbc'; +const iv = crypto.randomBytes(16); + + function encrypt(text, key) { + let cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv); + let encrypted = cipher.update(text); + encrypted = Buffer.concat([encrypted, cipher.final()]); + return { iv: iv.toString('hex'), encryptedData: encrypted.toString('hex') }; +} + +function decrypt(text, key) { + let iv = Buffer.from(text.iv, 'hex'); + let encryptedText = Buffer.from(text.encryptedData, 'hex'); + let decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv); + let decrypted = decipher.update(encryptedText); + decrypted = Buffer.concat([decrypted, decipher.final()]); + return decrypted.toString(); +} + +module.exports ={ + encrypt, + decrypt +} diff --git a/auth/kv.js b/auth/kv.js new file mode 100644 index 00000000..6a34f1d9 --- /dev/null +++ b/auth/kv.js @@ -0,0 +1,214 @@ +/* + * Nimbella CONFIDENTIAL + * --------------------- + * + * 2018 - present Nimbella Corp + * All Rights Reserved. + * + * NOTICE: + * + * All information contained herein is, and remains the property of + * Nimbella Corp and its suppliers, if any. The intellectual and technical + * concepts contained herein are proprietary to Nimbella Corp and its + * suppliers and may be covered by U.S. and Foreign Patents, patents + * in process, and are protected by trade secret or copyright law. + * + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from Nimbella Corp. + */ + +let kv = {} + +function clearTimeout(k) { + const x = kv[k] + if (x && x.timer) { + clearTimeout(x.timer) + } +} + +// https://redis.io/commands/flushall +const flushall = () => { + kv = {} + return Promise.resolve('OK') +} + +// https://redis.io/commands/keys +const keys = (p) => { + if (p) { + if (p.substring(p.length - 1) === '*') p = p.slice(0, -1); + return Object.keys(kv).filter(key => key.startsWith(p)) + } + else { return Object.keys(kv); } +} + +// https://redis.io/commands/get +const get = (k) => { + if (k in kv) { + return kv[k].value || kv[k] + } else return null +} + +// https://redis.io/commands/set +const set = (k, value) => { + clearTimeout(k) + kv[k] = { value } + return 'OK' +} + +// https://redis.io/commands/incr +const incr = (k) => { + let c = get(k) + if (c == null) { + set(k, 1) + return 1 + } else { + c = c + 1 + set(k, c) + return c + } +} + +// https://redis.io/commands/setex +const setex = (k, ttl, value) => { + const res = set(k, value) + expire(k, ttl) + return res +} + +// https://redis.io/commands/del +const del = (k) => { + if (k in kv) { + clearTimeout(k) + delete kv.k + return 1 + } else return 0 +} + +// https://redis.io/commands/expire +const expire = (k, t) => { + if (k in kv) { + clearTimeout(k) + kv[k].timer = setTimeout(() => del(k), t * 1000) + return 1 + } else return 0 +} + +// https://redis.io/commands/sadd +const sadd = (k, v) => { + if (kv[k] === undefined) { + kv[k] = new Set() + } + + const s = kv[k] + const n = s.size + s.add(v) + return s.size - n +} + +// https://redis.io/commands/srem +const srem = (k, v) => { + const s = kv[k] + if (s !== undefined) { + const n = s.size + s.remove(v) + return s.size - n + } else return 0 +} + +// https://redis.io/commands/smembers +const smembers = (k) => { + const s = kv[k] + if (s) return Array.from(s.keys()) + else return [] +} + +// https://redis.io/commands/rpush +const rpush = (k, v) => { + if (kv[k] === undefined) { + kv[k] = [] + } + + const l = kv[k] + l.push(v) + return l.length +} + +// https://redis.io/commands/lpush +const lpush = (k, v) => { + if (kv[k] === undefined) { + kv[k] = [] + } + + const l = kv[k] + l.unshift(v) + return l.length +} + +// https://redis.io/commands/lrange +const lrange = (k, i, j) => { + const l = kv[k] + if (l !== undefined) { + // TODO: this is not quite the same as redis + return l.slice(i, j) + } else return [] +} + +// https://redis.io/commands/llen +const llen = (k) => { + const l = kv[k] + if (l === undefined) { + return 0 + } else if (Array.isArray(l)) { + return l.length + } else { + throw new Error('not an array') + } +} + +module.exports = { + redis: () => ({ + flushall, + keys, + get, + set, + incr, + setex, + del, + expire, + sadd, + srem, + smembers, + rpush, + lpush, + lrange, + llen, + flushallAsync: flushall, + keysAsync: (k) => Promise.resolve(keys(k)), + getAsync: (k) => Promise.resolve(get(k)), + setAsync: (k, v) => Promise.resolve(set(k, v)), + incrAsync: (k) => Promise.resolve(incr(k)), + setexAsync: (k, t, v) => Promise.resolve(setex(k, t, v)), + delAsync: (k) => Promise.resolve(del(k)), + expireAsync: (k, t) => Promise.resolve(expire(k, t)), + smembersAsync: (s) => Promise.resolve(smembers(s)), + rpushAsync: (k, v) => Promise.resolve(rpush(k, v)), + lpushAsync: (k, v) => Promise.resolve(lpush(k, v)), + lrangeAsync: (k, i, j) => Promise.resolve(lrange(k, i, j)), + llenAsync: (k) => { + try { + const result = llen(k) + return Promise.resolve(result) + } catch (e) { + return Promise.reject(e.message) + } + } + }), + storage: () => ({ + id: `bucket`, + setMetadata: () => Promise.resolve({}), + file: (filename) => ({ + getSignedUrl: (options) => Promise.resolve([`signed-${filename}-${options.action}`]) + }) + }) +} diff --git a/auth/packages/auth/auth/.include b/auth/packages/auth/auth/.include new file mode 100644 index 00000000..102a574c --- /dev/null +++ b/auth/packages/auth/auth/.include @@ -0,0 +1,3 @@ +../../../encrypt.js +../../../authCheck.js +index.js diff --git a/auth/packages/auth/auth/index.js b/auth/packages/auth/auth/index.js new file mode 100644 index 00000000..e07832cf --- /dev/null +++ b/auth/packages/auth/auth/index.js @@ -0,0 +1,347 @@ +/* eslint-disable camelcase */ +// jshint esversion: 9 + +/* +/nc command_create auth [] [-n ] [-b ] [-i ] [-s ] [-p ] [-d ] +*/ + +const nimbella = require('@nimbella/sdk') +const kv = nimbella.redis() +const { encrypt, decrypt } = require('./encrypt') +const https = require('https') +const { URL } = require('url') +const entities = ['provider'] +const actions = ['list', 'add', 'get', 'remove', 'use'] +const grants = ['Authorization Code', 'Client Credentials', 'Password Credentials', 'Implicit'] +const authLinkValidity = 30 // in seconds +async function executer(action, data) { + if (action === 'list') { + const keys = await kv.keysAsync(`auth.${data.user_id}*`) + const requests = [] + for (const key of keys) { + requests.push( + JSON.parse(await kv.getAsync(key)) + ) + } + const providers = await Promise.all(requests) + return providers + } + if (action === 'add') { + await kv + .setAsync(`auth.${data.user_id}.${data.provider_name}`, JSON.stringify(data)) + return data + } + if (action === 'get') { + const toGet = await kv + .getAsync(`auth.${data.user_id}.${data.provider_name}`) + return JSON.parse(toGet) + } + if (action === 'remove') { + const toDelete = await kv + .get(`auth.${data.user_id}.${data.provider_name}`) + await kv + .delAsync(`auth.${data.user_id}.${data.provider_name}`) + return toDelete + } + if (action === 'use') { + const authData = await kv.getAsync(`auth.${data.user_id}.${data.provider_name}`) + if (authData) { + const toUse = JSON.parse(authData) + const state = process.env.__OW_ACTIVATION_ID || '' + const authUrl = `${toUse.auth_url}?client_id=${toUse.client_id}&scope=${toUse.scope}&allow_signup=false` + toUse.callback = process.env.__OW_ACTION_NAME + toUse.duration = data.duration + toUse.webhook = data.webhook + const result = await kv + .setexAsync(state, authLinkValidity * 60, JSON.stringify(toUse)) + if (result === 'OK') { + return `For authentication, you have opted to use *${data.provider_name}* for *${Number(data.duration)}* Seconds. Please click this <${authUrl}&state=${state}|link> to continue.` + } + else { + console.log('kv set failed with result:', result) + return `This operation requires you to authenticate but could not create a session for you.\n + If you are authorized to inspect the activation logs, check them for details or contact your Commander admin.` + } + } + else { + return `Couldn't find provider with name *${data.provider_name}*, please ensure the name is correct or add using _/nc auth add_` + } + } +} + + +/** + * @description + * @param {ParamsType} params list of command parameters + * @param {?string} commandText text message + * @param {!object} [secrets = {}] list of secrets + * @return {Promise} Response body + */ +async function command(params, commandText, secrets = {}) { + let { + entity = 'provider', + action = 'use', + provider_name, + grant_type = 'Authorization Code', + auth_url, + access_token_url, + base_url, + callback_url = 'https://apigcp.nimbella.io/api/v1/web/pslat20n-memkpwiprzv/auth/callback', + client_id, + client_secret, + scope = 'user:email,read:org', + duration = 1 // in seconds + } = params; + + // const key = 'vOVH6pripNWjRRIqRodrdxchalwHzfr3' + // const e = encrypt('testEncrypt', key) + // console.log(decrypt(e, key)) + + auth_url = extractURL(auth_url) + access_token_url = extractURL(access_token_url) + base_url = extractURL(base_url) + let data = {} + if (!entities.includes(entity)) return fail(`*valid entities are: ${entities.join(', ')}*`) + if (!actions.includes(action)) return fail(`*valid actions are: ${actions.join(', ')}*`) + if (!grants.includes(grant_type)) return fail(`*valid grant_types are: ${grants.join(', ')}*`) + switch (action) { + case 'a': + case 'add': + action = 'add' + if (!provider_name) return fail('*please specify provider name* e.g. -n twitter') + if (!(auth_url || base_url)) return fail('*please specify auth_url or base_url* e.g. -b github.com') + if (!(access_token_url || base_url)) return fail('*please specify access_token_url or base_url* e.g. -b github.com') + if (!callback_url) return fail('*please specify callback_url*') + if (!client_id) return fail('*please specify client_id* e.g. -i xxxxx261xxxxx016xxxx') + if (!client_secret) return fail('*please specify client_secret* e.g. -s xxxxx6edfdd975dxxxxx354680xxxxxb3b9xxxxx') + if (!scope) return fail('*please specify scope*') + + // set default + auth_url = auth_url || `${base_url}/login/oauth/authorize` + access_token_url = access_token_url || `${base_url}/login/oauth/access_token` + + data = { + provider_name, + grant_type, + auth_url, + base_url, + callback_url, + access_token_url, + client_id, + client_secret, + scope, + user_id: params.__client.user_id + } + break; + case 'r': + case 'remove': + action = 'remove' + if (!provider_name) return fail('*please specify provider name* e.g. -n twitter') + data = { + provider_name, + user_id: params.__client.user_id + } + break; + case 'g': + case 'get': + action = 'get' + if (!provider_name) return fail('*please specify provider name* e.g. -n twitter') + data = { + provider_name, + user_id: params.__client.user_id + } + break; + case 'u': + case 'use': + action = 'use' + if (!provider_name) return fail('*please specify provider name* e.g. -n twitter') + if (!duration) return fail('*please specify duration*') + data = { + provider_name, + user_id: params.__client.user_id, + webhook: params.__client.response_url, + duration + } + break; + case 'l': + case 'ls': + case 'list': + action = 'list' + data = { + user_id: params.__client.user_id, + } + break; + default: + return fail(`*Invalid Action. Expected options: 'add', 'remove', 'use', 'list' *`) + } + const res = await executer(action, data) + if (res) { + let header = `\nAuth *${action.charAt(0).toUpperCase() + action.substr(1)}* Request Result:`; + return success(action, header, res); + } + return fail(undefined, res); +} + +const mdText = (text) => ({ + type: 'mrkdwn', + text: text +}); + +const section = (text) => ({ + type: 'section', + text: mdText(text), +}); + +const fail = (msg, err) => { + let errMsg + if (err) errMsg = getErrorMessage(err) + const response = { + response_type: 'in_channel', + blocks: [section(`${msg || errMsg || '*couldn\'t process requested action*'}`)], + }; + return response +}; + +const getErrorMessage = (error) => { + console.error(error) + return error.message +} + +const _provider = (item, response) => { + const block = { + type: 'section', + fields: [ + mdText(`Name: *${item.provider_name}*\n Auth URL: ${item.auth_url}\n Access Token URL: ${item.access_token_url}\n Callback URL: ${item.callback_url}\n Scopes: ${item.scope}\n`) + ], + } + response.blocks.push(block) +}; + +const _providers = (items, response) => (items).forEach((item) => { + _provider(item, response) + response.blocks.push({ + "type": "divider" + }) +}); + +const success = async (action, header, data) => { + const response = { + response_type: 'ephemeral', + blocks: [section(header)], + }; + if (action === 'list') + _providers(data || [], response) + else if (action === 'add' || action === 'get') + _provider(data, response) + else if (action === 'remove') + _provider(data || [], response) + else if (action === 'use') + response.blocks.push(section(data)) + if (action !== 'use') + response.blocks.push({ + type: 'context', + elements: [ + mdText('add _auth command-set_ to your Slack with '), + ], + }); + return response +}; + +const extractURL = (url) => { + if (url) { + if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } + else { url = url.replace('<', '').replace('>', '') } + if (!url.startsWith('http')) { url = 'https://' + url; } + } + return url +} + +async function saveToken(access_token, state) { + let response='', webhook + const savedState = await kv.getAsync(state) + console.log(savedState); + if (savedState) { + try { + const parsedState = JSON.parse(savedState) + // remember the access token for specified duration + const userid = parsedState.user_id + const duration = parsedState.duration + webhook = parsedState.webhook + await kv.setexAsync(`${userid}.tk`, Number(duration) * 60, JSON.stringify({ access_token })) + response = await success(undefined, `Authentication Successful!`, undefined) + } catch (e) { + console.error('could not save access token', e) + response = fail('Couldn\'t Authenticate') + } + } else { + response = fail('Couldn\'t Authenticate') + } + console.log(response); + return { webhook, response } +} + +function generateErrorObject(message, statusCode) { + return { + statusCode: 400 || statusCode, + body: { + error: message + } + } +} + +async function postResult(webhook, result) { + const url = new URL(webhook) + return new Promise(function (resolve, reject) { + const data = JSON.stringify(result) + https.request({ + method: 'POST', + host: url.host, + path: url.pathname, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length + } + }, (resp) => { + let buffer = '' + + resp.on('data', (chunk) => { + buffer += chunk + }) + + resp.on('end', () => { + try { + resolve({ body: buffer }) + } catch (e) { + console.error(e, buffer) + reject(generateErrorObject('Unexpected response')) + } + }) + }).on('error', reject) + .write(data) + .end() + }) +} + +const main = async (args) => { + console.log(process.env.__OW_NAMESPACE); + console.log(process.env.__OW_ACTION_NAME); + console.log(args); + if (args.state && args.access_token) { + const { webhook, response } = await saveToken(args.access_token, args.state) + console.log('webhook post:' + webhook); + console.log(response) + return await postResult(webhook, response) + } + else { + console.log('webhook pre:' + args.params.__client.response_url); + const _command = command(args.params, args.commandText, args.__secrets || {}) + return { + body: await _command.catch((error) => ({ + response_type: 'ephemeral', + text: `Error: ${error.message}`, + })), + } + } +} + +exports.main = main; diff --git a/auth/packages/auth/auth/package.json b/auth/packages/auth/auth/package.json new file mode 100644 index 00000000..80a74cdc --- /dev/null +++ b/auth/packages/auth/auth/package.json @@ -0,0 +1,12 @@ +{ + "name": "auth", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/auth/packages/auth/callback/index.js b/auth/packages/auth/callback/index.js new file mode 100644 index 00000000..2a41b8fc --- /dev/null +++ b/auth/packages/auth/callback/index.js @@ -0,0 +1,108 @@ + +const wsk = require('openwhisk')() +const nimbella = require('@nimbella/sdk') +const kv = nimbella.redis() +const https = require('https') +const { URL } = require('url') + +async function exchangeCodeForToken(code, params) { + const api = new URL(params.access_token_url) + api.searchParams.set('client_id', params.client_id) + api.searchParams.set('client_secret', params.client_secret) + api.searchParams.set('code', code) + + return asyncHttpsPostRequest(api) +} + +async function asyncHttpsPostRequest(url) { + return new Promise(function (resolve, reject) { + https.request({ + method: 'POST', + host: url.host, + path: url.pathname + url.search, + headers: { + 'Accept': 'application/json' + } + }, (resp) => { + let data = '' + resp.on('data', (chunk) => { + data += chunk + }) + resp.on('end', () => { + try { + let parsed = JSON.parse(data) + resolve(parsed) + } catch (e) { + reject(data) + } + }) + }).on('error', reject) + .end() + }) +} + +function generateErrorObject(message, statusCode) { + return { + statusCode: 400 || statusCode, + body: { + error: message + } + } +} + + +exports.main = async (event) => { + if (!event.code) { + return generateErrorObject('You did not authenticate') + } + let response, state + try { + const auth = await kv.getAsync(event.state) + if (auth) { + state = JSON.parse(auth) + if (!state.client_id || !state.client_secret || !state.access_token_url) { + return generateErrorObject('API is not properly configured') + } + response = await exchangeCodeForToken(event.code, state) + } + else { + return generateErrorObject('Couldn\'t authenticate') + } + } catch (e) { + console.log(e) + return generateErrorObject('Failed to exchange code for access_token') + } + + if (!response || !response.access_token) { + return generateErrorObject('Missing access_token') + } + try { + if (state && state.callback) { + return wsk.actions + .invoke({ + actionName: state.callback, + params: { + access_token: response.access_token, + state: event.state, + }, + blocking: false + }) + .then(activation => { + console.log('forked', activation) + return { + body: 'You are authorized to perform the requested operation. Check Slack for progress. You may close this browser tab.' + } + }) + .catch(error => { + console.error(error) + return generateErrorObject('Your session expired') + }) + } else { + return generateErrorObject('Your session expired') + } + } + catch (e) { + console.log(e) + return generateErrorObject('API encountered an unexpected error') + } +} diff --git a/auth/project.yml b/auth/project.yml new file mode 100644 index 00000000..363514fd --- /dev/null +++ b/auth/project.yml @@ -0,0 +1,7 @@ +packages: + - name: auth + actions: + - name: callback + annotations: + provide-api-key: true + - name: auth From d1e1ef0af63abb6b26c69eff10f62f3df67ca65a Mon Sep 17 00:00:00 2001 From: Chandan Rai Date: Thu, 18 Feb 2021 13:01:43 +0530 Subject: [PATCH 02/19] Add auth command --- auth/commands.yaml | 2 ++ auth/packages/auth/auth/index.js | 8 +++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/auth/commands.yaml b/auth/commands.yaml index a62c517e..bb3cfa57 100644 --- a/auth/commands.yaml +++ b/auth/commands.yaml @@ -26,6 +26,8 @@ commands: value: scope - name: d value: duration + limits: + logs: 10 callback: description: callback for oauth flow. annotations: diff --git a/auth/packages/auth/auth/index.js b/auth/packages/auth/auth/index.js index e07832cf..e7398b98 100644 --- a/auth/packages/auth/auth/index.js +++ b/auth/packages/auth/auth/index.js @@ -86,7 +86,7 @@ async function command(params, commandText, secrets = {}) { auth_url, access_token_url, base_url, - callback_url = 'https://apigcp.nimbella.io/api/v1/web/pslat20n-memkpwiprzv/auth/callback', + callback_url = `https://apigcp.nimbella.io/api/v1/web/${process.env.__OW_ACTION_NAME.replace('/auth','/callback')}`, client_id, client_secret, scope = 'user:email,read:org', @@ -111,7 +111,6 @@ async function command(params, commandText, secrets = {}) { if (!provider_name) return fail('*please specify provider name* e.g. -n twitter') if (!(auth_url || base_url)) return fail('*please specify auth_url or base_url* e.g. -b github.com') if (!(access_token_url || base_url)) return fail('*please specify access_token_url or base_url* e.g. -b github.com') - if (!callback_url) return fail('*please specify callback_url*') if (!client_id) return fail('*please specify client_id* e.g. -i xxxxx261xxxxx016xxxx') if (!client_secret) return fail('*please specify client_secret* e.g. -s xxxxx6edfdd975dxxxxx354680xxxxxb3b9xxxxx') if (!scope) return fail('*please specify scope*') @@ -145,6 +144,8 @@ async function command(params, commandText, secrets = {}) { case 'g': case 'get': action = 'get' + if (!callback_url) + return success(undefined, `https://apigcp.nimbella.io/api/v1/web/${process.env.__OW_ACTION_NAME.replace('/auth','/callback')}`, undefined); if (!provider_name) return fail('*please specify provider name* e.g. -n twitter') data = { provider_name, @@ -328,12 +329,9 @@ const main = async (args) => { console.log(args); if (args.state && args.access_token) { const { webhook, response } = await saveToken(args.access_token, args.state) - console.log('webhook post:' + webhook); - console.log(response) return await postResult(webhook, response) } else { - console.log('webhook pre:' + args.params.__client.response_url); const _command = command(args.params, args.commandText, args.__secrets || {}) return { body: await _command.catch((error) => ({ From 2358298f96497d787801345b53c645291c788fa1 Mon Sep 17 00:00:00 2001 From: Chandan Rai Date: Mon, 22 Feb 2021 17:51:18 +0530 Subject: [PATCH 03/19] Add get callback url option --- auth/commands.yaml | 20 ++++++++++---------- auth/packages/auth/auth/index.js | 15 +++++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/auth/commands.yaml b/auth/commands.yaml index bb3cfa57..2172dba4 100644 --- a/auth/commands.yaml +++ b/auth/commands.yaml @@ -8,24 +8,24 @@ commands: options: - name: n value: provider_name - - name: g - value: grant_type - - name: c - value: callback_url - name: a value: auth_url - name: b value: base_url - - name: t - value: access_token_url + - name: c + value: callback_url + - name: d + value: duration + - name: g + value: grant_type - name: i value: client_id - - name: s - value: client_secret - name: p value: scope - - name: d - value: duration + - name: s + value: client_secret + - name: t + value: access_token_url limits: logs: 10 callback: diff --git a/auth/packages/auth/auth/index.js b/auth/packages/auth/auth/index.js index e7398b98..86fc82a8 100644 --- a/auth/packages/auth/auth/index.js +++ b/auth/packages/auth/auth/index.js @@ -11,7 +11,7 @@ const { encrypt, decrypt } = require('./encrypt') const https = require('https') const { URL } = require('url') const entities = ['provider'] -const actions = ['list', 'add', 'get', 'remove', 'use'] +const actions = ['list', 'add', 'get', 'remove', 'use', 'getcallback'] const grants = ['Authorization Code', 'Client Credentials', 'Password Credentials', 'Implicit'] const authLinkValidity = 30 // in seconds async function executer(action, data) { @@ -86,7 +86,7 @@ async function command(params, commandText, secrets = {}) { auth_url, access_token_url, base_url, - callback_url = `https://apigcp.nimbella.io/api/v1/web/${process.env.__OW_ACTION_NAME.replace('/auth','/callback')}`, + callback_url = `https://apigcp.nimbella.io/api/v1/web/${process.env.__OW_ACTION_NAME.replace('/auth', '/callback')}`, client_id, client_secret, scope = 'user:email,read:org', @@ -144,8 +144,6 @@ async function command(params, commandText, secrets = {}) { case 'g': case 'get': action = 'get' - if (!callback_url) - return success(undefined, `https://apigcp.nimbella.io/api/v1/web/${process.env.__OW_ACTION_NAME.replace('/auth','/callback')}`, undefined); if (!provider_name) return fail('*please specify provider name* e.g. -n twitter') data = { provider_name, @@ -172,8 +170,13 @@ async function command(params, commandText, secrets = {}) { user_id: params.__client.user_id, } break; + case 'gc': + case 'callback': + case 'getcallback': + action = 'getcallback' + return success(undefined, `https://apigcp.nimbella.io/api/v1/web/${process.env.__OW_ACTION_NAME.replace('/auth', '/callback')}`, undefined); default: - return fail(`*Invalid Action. Expected options: 'add', 'remove', 'use', 'list' *`) + return fail(`*Invalid Action. Expected options: 'add', 'remove', 'use', 'list','getcallback' *`) } const res = await executer(action, data) if (res) { @@ -258,7 +261,7 @@ const extractURL = (url) => { } async function saveToken(access_token, state) { - let response='', webhook + let response = '', webhook const savedState = await kv.getAsync(state) console.log(savedState); if (savedState) { From e2173a81436f19bdcf52135443c627d8d1d3093c Mon Sep 17 00:00:00 2001 From: Chandan Rai Date: Mon, 22 Feb 2021 17:52:14 +0530 Subject: [PATCH 04/19] Add command to manage pulls --- github/packages/github/pulls.js | 277 ++++++++++++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 github/packages/github/pulls.js diff --git a/github/packages/github/pulls.js b/github/packages/github/pulls.js new file mode 100644 index 00000000..d86ad49c --- /dev/null +++ b/github/packages/github/pulls.js @@ -0,0 +1,277 @@ +// jshint esversion: 9 + +const axios = require('axios'); + +const requestThreshold = 3 +const headers = { + 'Content-Type': 'application/json', +}; + + +async function Request(url, action, method, data, secrets) { + if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + if (secrets.github_token) { + let token + [token,] = secrets.github_token.split('@') + headers.Authorization = `Bearer ${token}`; + } + return axios({ + method: method, + url, + headers, + data + }) +} + +/** + * @description + * @param {ParamsType} params list of command parameters + * @param {?string} commandText text message + * @param {!object} [secrets = {}] list of secrets + * @return {Promise} Response body + */ +async function command(params, commandText, secrets = {}) { + let tokenHost, baseURL = 'https://api.github.com' + let { + action, + repository, + pr_number = '', + issue='', + title = '', + head='', + base='', + body = '', + draft =false, + maintainer_can_modify = false, + assignees = '', + milestone = '', + labels = '', + state = '', + list_option = 'pulls', + org = '', + since = '', + per_page = 50, + page = 1, + host + } = params; + let method = 'GET' + let data = {} + let lock = false + let listing = false + let list_path = '' + const { github_repos, github_host } = secrets; + const default_repos = repository ? repository : github_repos; + if (default_repos) { + repository = default_repos.split(',').map(repo => repo.trim())[0]; + } + switch (action) { + case 'c': + case 'cr': + case 'add': + case 'create': + action = 'create' + method = 'POST' + if (!repository) return fail('*please specify repository*') + if (!head) return fail('*please enter head branch*') + if (!base) return fail('*please enter base branch*') + data = { + title, + head, + base, + body, + draft, + issue, + maintainer_can_modify + } + break; + case 'u': + case 'up': + case 'update': + action = 'update' + method = 'PATCH' + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify pr number*') + data = { + title, + body, + assignees: assignees ? assignees.split(',').map(a => a.trim()) : [], + milestone: milestone ? milestone : null, + labels: labels ? labels.split(',').map(l => l.trim()) : [], + state + } + break; + case 'g': + case 'get': + action = 'get'; + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify pr number*') + break; + case 'l': + case 'ls': + case 'list': + action = 'list' + listing = true + if (!['commits', 'files', 'reviews', 'comments','pulls'].includes(list_option)) + return fail(`*expected list_option to be one of 'commits', 'files', 'reviews', 'comments','pulls'*`) + if (list_option === 'org') { + if (!org) + return fail('*please specify org name*') + list_path = `/orgs/${org}` + } + if (list_option === 'user') + list_path = `/user` + if (list_option === 'repository') + listing = false + break; + case 'ch': + case 'check': + action = 'check' + lock = true + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify pr number*') + data = { + locked: true, + } + break; + case 'm': + case 'merge': + action = 'merge' + method = 'PUT' + lock = true + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify a pr number*') + break; + default: + return fail(`*Invalid Action. Expected options: 'add', 'update', 'get', 'list', 'lock', 'unlock' *`) + } + if (secrets.github_token) { + [, tokenHost] = secrets.github_token.split('@') + } + baseURL = host || tokenHost || github_host || baseURL + baseURL = updateURL(baseURL) + const url = `${baseURL}/${listing ? list_path : `repos/${repository}`}/pulls${pr_number ? `/${pr_number}` : ''}${lock ? `/lock` : ''}` + console.log(url); + const res = await Request(url, action, method, data, secrets) + + if (res) { + const tokenMessage = secrets.github_token ? '' : '*For greater limits you can add using*\n `/nc secret_create`'; + const currReading = parseInt(res.headers['x-ratelimit-remaining']); + let header = `\nPull *${action.charAt(0).toUpperCase() + action.substr(1)}* Request Result:`; + if (currReading < requestThreshold) { + header = `:warning: *You are about to reach the api rate limit.* ${tokenMessage}`; + } + if (currReading === 0) { + header = `:warning: *The api rate limit has been exhausted.* ${tokenMessage}`; + return fail(header); + } + return success(action, header, res.data, secrets); + } + return fail(undefined, res); +} + +const image = (source, alt) => ({ + type: 'image', + image_url: source, + alt_text: alt, +}); + +const mdText = (text) => ({ + type: 'mrkdwn', + text: text + // Convert markdown links to slack format. + .replace(/!*\[(.*)\]\((.*)\)/g, '<$2|$1>') + // Replace markdown headings with slack bold + .replace(/#+\s(.+)(?:R(?!#(?!#)).*)*/g, '*$1*'), +}); + +const section = (text) => ({ + type: 'section', + text: mdText(text), +}); + +const fail = (msg, err) => { + let errMsg + if (err) errMsg = getErrorMessage(err) + const response = { + response_type: 'in_channel', + blocks: [section(`${msg || errMsg || '*couldn\'t get action results*'}`)], + }; + return response +}; + +const getErrorMessage = (error) => { + console.error(error) + if (error.response && error.response.status === 403) { + return `:warning: *The api rate limit has been exhausted.*` + } else if (error.response && error.response.status && error.response.data) { + return `Error: ${error.response.status} ${error.response.data.message}` + } else { + return error.message + } +}; + +const _get = (item, response) => { + const block = { + type: 'section', + fields: [ + mdText(`<${item.html_url}|${item.number}> \n ${item.title} ${item.milestone ? `\n ${item.milestone.title}` : ''} + ${item.assignees.length > 0 ? `\n ${item.assignees.map(a => `<${a.html_url}|${a.login}>`).join()}` : ''} + ${item.labels.length > 0 ? `\n ${item.labels.map(l => l.name).join()}` : ''} + `), + mdText(`*State:* ${item.state.charAt(0).toUpperCase() + item.state.substr(1)} + \n*Created:* + \n*Updated:* + ${item.closed_at ? `\n*Closed:* ` : ''}`), + ], + } + if (item.assignee) block.accessory = image(item.assignee.avatar_url, item.assignee.login) + response.blocks.push(block) + if (item.body) response.blocks.push(section(`${item.body.length > 500 ? item.body.substr(0, 500) + '...' : item.body}`.replace(/#(\d+)/g, `<${item.html_url.split('/').splice(0, 5).join('/')}/pulls/$1|#$1>`))); +}; + +const _list = (items, response) => (items).forEach((item) => { + _get(item, response) +}); + + +const success = async (action, header, data, secrets) => { + const response = { + response_type: 'in_channel', + blocks: [section(header)], + }; + if (action === 'list') + _list(data || [], response) + else if (action === 'lock') + response.blocks.push(section(`Pull Locked.`)) + else if (action === 'unlock') + response.blocks.push(section(`Pull Unlocked.`)) + else + _get(data, response) + + response.blocks.push({ + type: 'context', + elements: [ + mdText('add _github command-set_ to your Slack with '), + ], + }); + return response +}; + +const updateURL = (url) => { + if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } + else { url = url.replace('<', '').replace('>', '') } + if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } + else { url = url.replace('<', '').replace('>', '') } + if (!url.startsWith('http')) { url = 'https://' + url; } + if (!url.includes('api')) { url += '/api/v3'; } + return url +} + +const main = async (args) => ({ + body: await command(args.params, args.commandText, args.__secrets || {}).catch((error) => ({ + response_type: 'ephemeral', + text: `Error: ${error.message}`, + })), +}); + +module.exports = main; From 800261b6e9077348ad0ff70531521e66b3580471 Mon Sep 17 00:00:00 2001 From: Chandan Rai Date: Tue, 9 Mar 2021 20:44:42 +0530 Subject: [PATCH 05/19] [github:hooks] manage hooks --- github/packages/github/hooks.js | 266 ++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 github/packages/github/hooks.js diff --git a/github/packages/github/hooks.js b/github/packages/github/hooks.js new file mode 100644 index 00000000..93da56f7 --- /dev/null +++ b/github/packages/github/hooks.js @@ -0,0 +1,266 @@ +// jshint esversion: 9 + +const axios = require('axios'); + +const requestThreshold = 3 +const headers = { + 'Content-Type': 'application/json', +}; + + +async function Request(url, action, method, data, secrets) { + if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + if (secrets.github_token) { + let token + [token,] = secrets.github_token.split('@') + headers.Authorization = `Bearer ${token}`; + } + return axios({ + method: method, + url, + headers, + data + }) +} + +/** + * @description + * @param {ParamsType} params list of command parameters + * @param {?string} commandText text message + * @param {!object} [secrets = {}] list of secrets + * @return {Promise} Response body + */ +async function command(params, commandText, secrets = {}) { + let tokenHost, baseURL = 'https://api.github.com' + let { + type="repos", + id, + name ='web', + repository, + config= { + "content_type": "json", + "insecure_ssl": "0", + "secret":"", + "url": "https://example.com/webhook" + } , + events= e ['push'], + add_events= [], + remove_events =[], + active = true, + } = params; + let method = 'GET' + let data = {} + let listing = false + let list_path = '' + const { github_repos, github_host } = secrets; + const default_repos = repository ? repository : github_repos; + if (default_repos) { + repository = default_repos.split(',').map(repo => repo.trim())[0]; + } + switch (action) { + case 'c': + case 'cr': + case 'add': + case 'create': + action = 'create' + method = 'POST' + if (!name) return fail('*please specify a name*') + data = { + name, + config, + events, + active, + } + break; + case 'u': + case 'up': + case 'update': + action = 'update' + method = 'PATCH' + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify pr number*') + data = { + title, + body, + assignees: assignees ? assignees.split(',').map(a => a.trim()) : [], + milestone: milestone ? milestone : null, + labels: labels ? labels.split(',').map(l => l.trim()) : [], + state + } + break; + case 'g': + case 'get': + action = 'get'; + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify pr number*') + break; + case 'l': + case 'ls': + case 'list': + action = 'list' + listing = true + if (!['commits', 'files', 'reviews', 'comments','pulls'].includes(list_option)) + return fail(`*expected list_option to be one of 'commits', 'files', 'reviews', 'comments','pulls'*`) + if (list_option === 'org') { + if (!org) + return fail('*please specify org name*') + list_path = `/orgs/${org}` + } + if (list_option === 'user') + list_path = `/user` + if (list_option === 'repository') + listing = false + break; + case 'ch': + case 'check': + action = 'check' + lock = true + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify pr number*') + data = { + locked: true, + } + break; + case 'm': + case 'merge': + action = 'merge' + method = 'PUT' + lock = true + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify a pr number*') + break; + default: + return fail(`*Invalid Action. Expected options: 'add', 'update', 'get', 'list', 'lock', 'unlock' *`) + } + if (secrets.github_token) { + [, tokenHost] = secrets.github_token.split('@') + } + baseURL = host || tokenHost || github_host || baseURL + baseURL = updateURL(baseURL) + const url = `${baseURL}/${type ==='repos' ? `repos/${repository}` : `orgs/${org}`}/hooks${id ? `/${id}` : ''}${ping ? `/ping` : ''}` + // /orgs/{org}/hooks + console.log(url); + const res = await Request(url, action, method, data, secrets) + + if (res) { + const tokenMessage = secrets.github_token ? '' : '*For greater limits you can add using*\n `/nc secret_create`'; + const currReading = parseInt(res.headers['x-ratelimit-remaining']); + let header = `\nPull *${action.charAt(0).toUpperCase() + action.substr(1)}* Request Result:`; + if (currReading < requestThreshold) { + header = `:warning: *You are about to reach the api rate limit.* ${tokenMessage}`; + } + if (currReading === 0) { + header = `:warning: *The api rate limit has been exhausted.* ${tokenMessage}`; + return fail(header); + } + return success(action, header, res.data, secrets); + } + return fail(undefined, res); +} + +const image = (source, alt) => ({ + type: 'image', + image_url: source, + alt_text: alt, +}); + +const mdText = (text) => ({ + type: 'mrkdwn', + text: text + // Convert markdown links to slack format. + .replace(/!*\[(.*)\]\((.*)\)/g, '<$2|$1>') + // Replace markdown headings with slack bold + .replace(/#+\s(.+)(?:R(?!#(?!#)).*)*/g, '*$1*'), +}); + +const section = (text) => ({ + type: 'section', + text: mdText(text), +}); + +const fail = (msg, err) => { + let errMsg + if (err) errMsg = getErrorMessage(err) + const response = { + response_type: 'in_channel', + blocks: [section(`${msg || errMsg || '*couldn\'t get action results*'}`)], + }; + return response +}; + +const getErrorMessage = (error) => { + console.error(error) + if (error.response && error.response.status === 403) { + return `:warning: *The api rate limit has been exhausted.*` + } else if (error.response && error.response.status && error.response.data) { + return `Error: ${error.response.status} ${error.response.data.message}` + } else { + return error.message + } +}; + +const _get = (item, response) => { + const block = { + type: 'section', + fields: [ + mdText(`<${item.html_url}|${item.number}> \n ${item.title} ${item.milestone ? `\n ${item.milestone.title}` : ''} + ${item.assignees.length > 0 ? `\n ${item.assignees.map(a => `<${a.html_url}|${a.login}>`).join()}` : ''} + ${item.labels.length > 0 ? `\n ${item.labels.map(l => l.name).join()}` : ''} + `), + mdText(`*State:* ${item.state.charAt(0).toUpperCase() + item.state.substr(1)} + \n*Created:* + \n*Updated:* + ${item.closed_at ? `\n*Closed:* ` : ''}`), + ], + } + if (item.assignee) block.accessory = image(item.assignee.avatar_url, item.assignee.login) + response.blocks.push(block) + if (item.body) response.blocks.push(section(`${item.body.length > 500 ? item.body.substr(0, 500) + '...' : item.body}`.replace(/#(\d+)/g, `<${item.html_url.split('/').splice(0, 5).join('/')}/pulls/$1|#$1>`))); +}; + +const _list = (items, response) => (items).forEach((item) => { + _get(item, response) +}); + + +const success = async (action, header, data, secrets) => { + const response = { + response_type: 'in_channel', + blocks: [section(header)], + }; + if (action === 'list') + _list(data || [], response) + else if (action === 'lock') + response.blocks.push(section(`Pull Locked.`)) + else if (action === 'unlock') + response.blocks.push(section(`Pull Unlocked.`)) + else + _get(data, response) + + response.blocks.push({ + type: 'context', + elements: [ + mdText('add _github command-set_ to your Slack with '), + ], + }); + return response +}; + +const updateURL = (url) => { + if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } + else { url = url.replace('<', '').replace('>', '') } + if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } + else { url = url.replace('<', '').replace('>', '') } + if (!url.startsWith('http')) { url = 'https://' + url; } + if (!url.includes('api')) { url += '/api/v3'; } + return url +} + +const main = async (args) => ({ + body: await command(args.params, args.commandText, args.__secrets || {}).catch((error) => ({ + response_type: 'ephemeral', + text: `Error: ${error.message}`, + })), +}); + +module.exports = main; From b2e06167fca394e030fd3144bd9abd4e8df2b770 Mon Sep 17 00:00:00 2001 From: crai Date: Wed, 10 Mar 2021 21:22:12 +0530 Subject: [PATCH 06/19] [github:hooks] add multiple events --- github/packages/github/hooks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/packages/github/hooks.js b/github/packages/github/hooks.js index 93da56f7..72ca3ed6 100644 --- a/github/packages/github/hooks.js +++ b/github/packages/github/hooks.js @@ -43,7 +43,7 @@ async function command(params, commandText, secrets = {}) { "secret":"", "url": "https://example.com/webhook" } , - events= e ['push'], + events, add_events= [], remove_events =[], active = true, @@ -68,7 +68,7 @@ async function command(params, commandText, secrets = {}) { data = { name, config, - events, + events: events ? events.split(',').map(a => a.trim()) : ['push'], active, } break; From 35931592c307875466e02b99cc8c3c966b99c4c8 Mon Sep 17 00:00:00 2001 From: crai Date: Wed, 10 Mar 2021 21:22:58 +0530 Subject: [PATCH 07/19] [github] add billing command --- github/packages/github/billing.js | 197 ++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 github/packages/github/billing.js diff --git a/github/packages/github/billing.js b/github/packages/github/billing.js new file mode 100644 index 00000000..18ee3a1e --- /dev/null +++ b/github/packages/github/billing.js @@ -0,0 +1,197 @@ +// jshint esversion: 9 + +const axios = require('axios'); + +const requestThreshold = 3 +const headers = { + 'Content-Type': 'application/json', +}; + + +async function Request(url, action, method, data, secrets) { + if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + if (secrets.github_token) { + let token + [token,] = secrets.github_token.split('@') + headers.Authorization = `Bearer ${token}`; + } + return axios({ + method: method, + url, + headers, + data + }) +} + +/** + * @description + * @param {ParamsType} params list of command parameters + * @param {?string} commandText text message + * @param {!object} [secrets = {}] list of secrets + * @return {Promise} Response body + */ +async function command(params, commandText, secrets = {}) { + let tokenHost, baseURL = 'https://api.github.com' + let { + type = 'orgs', + entity, + action, + repository, + org = '', + since = '', + per_page = 50, + page = 1, + host + } = params; + let method = 'GET' + let data = {} + let lock = false + let listing = false + let list_path = '' + const { github_repos, github_host } = secrets; + const default_repos = repository ? repository : github_repos; + if (default_repos) { + repository = default_repos.split(',').map(repo => repo.trim())[0]; + } + switch (action) { + case 'g': + case 'get': + action = 'get'; + if (!repository) return fail('*please specify repository*') + break; + case 'l': + case 'ls': + case 'list': + action = 'list' + + break; + default: + return fail(`*Invalid Action. Expected options: 'get', 'list' *`) + } + if (secrets.github_token) { + [, tokenHost] = secrets.github_token.split('@') + } + baseURL = host || tokenHost || github_host || baseURL + baseURL = updateURL(baseURL) + const url = `${baseURL}/${type}/${type==='orgs' ? org : user}/settings/billing/${entity}` + console.log(url); + const res = await Request(url, action, method, data, secrets) + + if (res) { + const tokenMessage = secrets.github_token ? '' : '*For greater limits you can add using*\n `/nc secret_create`'; + const currReading = parseInt(res.headers['x-ratelimit-remaining']); + let header = `\nPull *${action.charAt(0).toUpperCase() + action.substr(1)}* Request Result:`; + if (currReading < requestThreshold) { + header = `:warning: *You are about to reach the api rate limit.* ${tokenMessage}`; + } + if (currReading === 0) { + header = `:warning: *The api rate limit has been exhausted.* ${tokenMessage}`; + return fail(header); + } + return success(action, header, res.data, secrets); + } + return fail(undefined, res); +} + +const image = (source, alt) => ({ + type: 'image', + image_url: source, + alt_text: alt, +}); + +const mdText = (text) => ({ + type: 'mrkdwn', + text: text + // Convert markdown links to slack format. + .replace(/!*\[(.*)\]\((.*)\)/g, '<$2|$1>') + // Replace markdown headings with slack bold + .replace(/#+\s(.+)(?:R(?!#(?!#)).*)*/g, '*$1*'), +}); + +const section = (text) => ({ + type: 'section', + text: mdText(text), +}); + +const fail = (msg, err) => { + let errMsg + if (err) errMsg = getErrorMessage(err) + const response = { + response_type: 'in_channel', + blocks: [section(`${msg || errMsg || '*couldn\'t get action results*'}`)], + }; + return response +}; + +const getErrorMessage = (error) => { + console.error(error) + if (error.response && error.response.status === 403) { + return `:warning: *The api rate limit has been exhausted.*` + } else if (error.response && error.response.status && error.response.data) { + return `Error: ${error.response.status} ${error.response.data.message}` + } else { + return error.message + } +}; + +const _get = (item, response) => { + const block = { + type: 'section', + fields: [ + mdText(`<${item.html_url}|${item.number}> \n ${item.title} ${item.milestone ? `\n ${item.milestone.title}` : ''} + ${item.assignees.length > 0 ? `\n ${item.assignees.map(a => `<${a.html_url}|${a.login}>`).join()}` : ''} + ${item.labels.length > 0 ? `\n ${item.labels.map(l => l.name).join()}` : ''} + `), + mdText(`*State:* ${item.state.charAt(0).toUpperCase() + item.state.substr(1)} + \n*Created:* + \n*Updated:* + ${item.closed_at ? `\n*Closed:* ` : ''}`), + ], + } + if (item.assignee) block.accessory = image(item.assignee.avatar_url, item.assignee.login) + response.blocks.push(block) + if (item.body) response.blocks.push(section(`${item.body.length > 500 ? item.body.substr(0, 500) + '...' : item.body}`.replace(/#(\d+)/g, `<${item.html_url.split('/').splice(0, 5).join('/')}/pulls/$1|#$1>`))); +}; + +const _list = (items, response) => (items).forEach((item) => { + _get(item, response) +}); + + +const success = async (action, header, data, secrets) => { + const response = { + response_type: 'in_channel', + blocks: [section(header)], + }; + if (action === 'list') + _list(data || [], response) + else + _get(data, response) + + response.blocks.push({ + type: 'context', + elements: [ + mdText('add _github command-set_ to your Slack with '), + ], + }); + return response +}; + +const updateURL = (url) => { + if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } + else { url = url.replace('<', '').replace('>', '') } + if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } + else { url = url.replace('<', '').replace('>', '') } + if (!url.startsWith('http')) { url = 'https://' + url; } + if (!url.includes('api')) { url += '/api/v3'; } + return url +} + +const main = async (args) => ({ + body: await command(args.params, args.commandText, args.__secrets || {}).catch((error) => ({ + response_type: 'ephemeral', + text: `Error: ${error.message}`, + })), +}); + +module.exports = main; From e8fde544323ccd2fd0621006c4c3caae3c40e60b Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 15:14:17 +0530 Subject: [PATCH 08/19] Add version option in github command --- github/commands.yaml | 3 +++ github/packages/github/github.js | 14 +++++++++++++- github/packages/github/package.json | 9 +++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 github/packages/github/package.json diff --git a/github/commands.yaml b/github/commands.yaml index e0a5228f..75395fa4 100644 --- a/github/commands.yaml +++ b/github/commands.yaml @@ -200,3 +200,6 @@ commands: value: host github: description: View GitHub command set documentation + options: + - name: v + value: version diff --git a/github/packages/github/github.js b/github/packages/github/github.js index 6ff7a47a..e650d0c5 100644 --- a/github/packages/github/github.js +++ b/github/packages/github/github.js @@ -76,7 +76,19 @@ async function _command(params, commandText, secrets = {}) { } }; }; - + if (params.version) { + const pjson = require('package.json'); + return { + response_type: 'in_channel', + [client !== 'mattermost' ? 'blocks' : 'text']: + mui( + section( + `*${pjson.version}*` + ), + client + ) + }; + } const result = [ mui( section( diff --git a/github/packages/github/package.json b/github/packages/github/package.json new file mode 100644 index 00000000..ad021268 --- /dev/null +++ b/github/packages/github/package.json @@ -0,0 +1,9 @@ +{ + "name": "GithubCommandSet", + "version": "1.0.0", + "description": "Nimbella Commander GitHub Command Set", + "main": "github.js", + "keywords": [], + "author": "", + "license": "ISC" +} From a82f254db435acaf64a3e5b6db873f5cdae46b78 Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 15:37:16 +0530 Subject: [PATCH 09/19] Change path of package.json in github command --- github/packages/github/github.js | 2 +- github/packages/github/package.json | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 github/packages/github/package.json diff --git a/github/packages/github/github.js b/github/packages/github/github.js index e650d0c5..29545cd8 100644 --- a/github/packages/github/github.js +++ b/github/packages/github/github.js @@ -77,7 +77,7 @@ async function _command(params, commandText, secrets = {}) { }; }; if (params.version) { - const pjson = require('package.json'); + const pjson = require('../../package.json'); return { response_type: 'in_channel', [client !== 'mattermost' ? 'blocks' : 'text']: diff --git a/github/packages/github/package.json b/github/packages/github/package.json deleted file mode 100644 index ad021268..00000000 --- a/github/packages/github/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "GithubCommandSet", - "version": "1.0.0", - "description": "Nimbella Commander GitHub Command Set", - "main": "github.js", - "keywords": [], - "author": "", - "license": "ISC" -} From 4427fb779f423727e826b71f0c746f16fca0edf5 Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 15:40:02 +0530 Subject: [PATCH 10/19] Add billing, hooks and pulls to github command-set --- github/commands.yaml | 12 ++++++++++++ github/package.json | 9 +++++++++ 2 files changed, 21 insertions(+) create mode 100644 github/package.json diff --git a/github/commands.yaml b/github/commands.yaml index 75395fa4..94298961 100644 --- a/github/commands.yaml +++ b/github/commands.yaml @@ -198,6 +198,18 @@ commands: value: milestone_number - name: h value: host + billing: + description: Manage Billing + parameters: + - name: action + hooks: + description: Manage Hooks + parameters: + - name: action + pulls: + description: Manage Pulls + parameters: + - name: action github: description: View GitHub command set documentation options: diff --git a/github/package.json b/github/package.json new file mode 100644 index 00000000..ad021268 --- /dev/null +++ b/github/package.json @@ -0,0 +1,9 @@ +{ + "name": "GithubCommandSet", + "version": "1.0.0", + "description": "Nimbella Commander GitHub Command Set", + "main": "github.js", + "keywords": [], + "author": "", + "license": "ISC" +} From 9120586817ecaf95016486137658c74bb96ce060 Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 16:13:36 +0530 Subject: [PATCH 11/19] Add -v flag to github command --- github/commands.yaml | 1 - github/packages/github/github.js | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/github/commands.yaml b/github/commands.yaml index 94298961..618ae7d4 100644 --- a/github/commands.yaml +++ b/github/commands.yaml @@ -214,4 +214,3 @@ commands: description: View GitHub command set documentation options: - name: v - value: version diff --git a/github/packages/github/github.js b/github/packages/github/github.js index 29545cd8..3584124f 100644 --- a/github/packages/github/github.js +++ b/github/packages/github/github.js @@ -76,19 +76,21 @@ async function _command(params, commandText, secrets = {}) { } }; }; - if (params.version) { + + if (params.v) { const pjson = require('../../package.json'); return { response_type: 'in_channel', [client !== 'mattermost' ? 'blocks' : 'text']: - mui( + [mui( section( `*${pjson.version}*` ), client - ) + )] }; } + const result = [ mui( section( From 6fa1b4390508ae5489b01643664ed84fbc5dbb18 Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 16:17:43 +0530 Subject: [PATCH 12/19] Improve github command version output --- github/packages/github/github.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/packages/github/github.js b/github/packages/github/github.js index 3584124f..eb38e940 100644 --- a/github/packages/github/github.js +++ b/github/packages/github/github.js @@ -84,7 +84,7 @@ async function _command(params, commandText, secrets = {}) { [client !== 'mattermost' ? 'blocks' : 'text']: [mui( section( - `*${pjson.version}*` + `GitHub Command Set Version: *${pjson.version}*` ), client )] From 9d1b58b49b5af48c68f16ad90bf61ffd0f8d8da9 Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 17:32:18 +0530 Subject: [PATCH 13/19] Remove duplicate lines from updateUrl --- github/package.json | 2 +- github/packages/github/assignees.js | 3 ++- github/packages/github/billing.js | 9 ++++----- github/packages/github/comments.js | 5 +++-- github/packages/github/hooks.js | 23 ++++++++++------------- github/packages/github/issues.js | 6 +++--- github/packages/github/labels.js | 3 ++- github/packages/github/milestones.js | 3 ++- github/packages/github/pulls.js | 17 ++++++++--------- 9 files changed, 35 insertions(+), 36 deletions(-) diff --git a/github/package.json b/github/package.json index ad021268..6143f0ba 100644 --- a/github/package.json +++ b/github/package.json @@ -1,5 +1,5 @@ { - "name": "GithubCommandSet", + "name": "github_command_set", "version": "1.0.0", "description": "Nimbella Commander GitHub Command Set", "main": "github.js", diff --git a/github/packages/github/assignees.js b/github/packages/github/assignees.js index ed8a4248..9d1e6113 100644 --- a/github/packages/github/assignees.js +++ b/github/packages/github/assignees.js @@ -10,7 +10,8 @@ const headers = { async function Request(url, action, method, data, secrets) { - if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + // get, list for public repos do not need access token + if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') diff --git a/github/packages/github/billing.js b/github/packages/github/billing.js index 18ee3a1e..79dfdadc 100644 --- a/github/packages/github/billing.js +++ b/github/packages/github/billing.js @@ -9,7 +9,8 @@ const headers = { async function Request(url, action, method, data, secrets) { - if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + // get, list for public repos do not need access token + if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') @@ -63,7 +64,7 @@ async function command(params, commandText, secrets = {}) { case 'ls': case 'list': action = 'list' - + break; default: return fail(`*Invalid Action. Expected options: 'get', 'list' *`) @@ -73,7 +74,7 @@ async function command(params, commandText, secrets = {}) { } baseURL = host || tokenHost || github_host || baseURL baseURL = updateURL(baseURL) - const url = `${baseURL}/${type}/${type==='orgs' ? org : user}/settings/billing/${entity}` + const url = `${baseURL}/${type}/${type === 'orgs' ? org : user}/settings/billing/${entity}` console.log(url); const res = await Request(url, action, method, data, secrets) @@ -178,8 +179,6 @@ const success = async (action, header, data, secrets) => { }; const updateURL = (url) => { - if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } - else { url = url.replace('<', '').replace('>', '') } if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } else { url = url.replace('<', '').replace('>', '') } if (!url.startsWith('http')) { url = 'https://' + url; } diff --git a/github/packages/github/comments.js b/github/packages/github/comments.js index c5290f01..8822fceb 100644 --- a/github/packages/github/comments.js +++ b/github/packages/github/comments.js @@ -9,7 +9,8 @@ const headers = { async function Request(url, action, method, data, secrets) { - if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + // get, list for public repos do not need access token + if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') @@ -165,7 +166,7 @@ const _get = (item, response) => { const block = { type: 'section', fields: [ - mdText(item.id?`<${item.html_url}|${item.id}>`:''), + mdText(item.id ? `<${item.html_url}|${item.id}>` : ''), mdText(`*Created:* ${item.created_at ? `` : '-'} \n*Updated:* ${item.updated_at ? `` : '-'} `), ], diff --git a/github/packages/github/hooks.js b/github/packages/github/hooks.js index 72ca3ed6..754861f1 100644 --- a/github/packages/github/hooks.js +++ b/github/packages/github/hooks.js @@ -9,7 +9,6 @@ const headers = { async function Request(url, action, method, data, secrets) { - if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') @@ -33,19 +32,19 @@ async function Request(url, action, method, data, secrets) { async function command(params, commandText, secrets = {}) { let tokenHost, baseURL = 'https://api.github.com' let { - type="repos", + type = "repos", id, - name ='web', + name = 'web', repository, - config= { + config = { "content_type": "json", "insecure_ssl": "0", - "secret":"", + "secret": "", "url": "https://example.com/webhook" - } , + }, events, - add_events= [], - remove_events =[], + add_events = [], + remove_events = [], active = true, } = params; let method = 'GET' @@ -60,7 +59,7 @@ async function command(params, commandText, secrets = {}) { switch (action) { case 'c': case 'cr': - case 'add': + case 'add': case 'create': action = 'create' method = 'POST' @@ -99,7 +98,7 @@ async function command(params, commandText, secrets = {}) { case 'list': action = 'list' listing = true - if (!['commits', 'files', 'reviews', 'comments','pulls'].includes(list_option)) + if (!['commits', 'files', 'reviews', 'comments', 'pulls'].includes(list_option)) return fail(`*expected list_option to be one of 'commits', 'files', 'reviews', 'comments','pulls'*`) if (list_option === 'org') { if (!org) @@ -137,7 +136,7 @@ async function command(params, commandText, secrets = {}) { } baseURL = host || tokenHost || github_host || baseURL baseURL = updateURL(baseURL) - const url = `${baseURL}/${type ==='repos' ? `repos/${repository}` : `orgs/${org}`}/hooks${id ? `/${id}` : ''}${ping ? `/ping` : ''}` + const url = `${baseURL}/${type === 'repos' ? `repos/${repository}` : `orgs/${org}`}/hooks${id ? `/${id}` : ''}${ping ? `/ping` : ''}` // /orgs/{org}/hooks console.log(url); const res = await Request(url, action, method, data, secrets) @@ -247,8 +246,6 @@ const success = async (action, header, data, secrets) => { }; const updateURL = (url) => { - if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } - else { url = url.replace('<', '').replace('>', '') } if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } else { url = url.replace('<', '').replace('>', '') } if (!url.startsWith('http')) { url = 'https://' + url; } diff --git a/github/packages/github/issues.js b/github/packages/github/issues.js index 8c23731b..b002c93c 100644 --- a/github/packages/github/issues.js +++ b/github/packages/github/issues.js @@ -9,7 +9,8 @@ const headers = { async function Request(url, action, method, data, secrets) { - if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + // get, list for public repos do not need access token + if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') @@ -50,6 +51,7 @@ async function command(params, commandText, secrets = {}) { page = 1, host } = params; + console.log(params); let method = 'GET' let data = {} let lock = false @@ -255,8 +257,6 @@ const success = async (action, header, data, secrets) => { }; const updateURL = (url) => { - if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } - else { url = url.replace('<', '').replace('>', '') } if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } else { url = url.replace('<', '').replace('>', '') } if (!url.startsWith('http')) { url = 'https://' + url; } diff --git a/github/packages/github/labels.js b/github/packages/github/labels.js index d4154746..84c97f9f 100644 --- a/github/packages/github/labels.js +++ b/github/packages/github/labels.js @@ -9,7 +9,8 @@ const headers = { async function Request(url, action, method, data, secrets) { - if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + // get, list for public repos do not need access token + if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') diff --git a/github/packages/github/milestones.js b/github/packages/github/milestones.js index fde5a1a6..9677b3ec 100644 --- a/github/packages/github/milestones.js +++ b/github/packages/github/milestones.js @@ -9,7 +9,8 @@ const headers = { async function Request(url, action, method, data, secrets) { - if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + // get, list for public repos do not need access token + if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') diff --git a/github/packages/github/pulls.js b/github/packages/github/pulls.js index d86ad49c..c468eeaf 100644 --- a/github/packages/github/pulls.js +++ b/github/packages/github/pulls.js @@ -9,7 +9,8 @@ const headers = { async function Request(url, action, method, data, secrets) { - if (!secrets.github_token && (action !== 'list' || action !== 'get')) { return fail('*please add github_token secret*') } + // get, list for public repos do not need access token + if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') @@ -36,12 +37,12 @@ async function command(params, commandText, secrets = {}) { action, repository, pr_number = '', - issue='', + issue = '', title = '', - head='', - base='', + head = '', + base = '', body = '', - draft =false, + draft = false, maintainer_can_modify = false, assignees = '', milestone = '', @@ -67,7 +68,7 @@ async function command(params, commandText, secrets = {}) { switch (action) { case 'c': case 'cr': - case 'add': + case 'add': case 'create': action = 'create' method = 'POST' @@ -111,7 +112,7 @@ async function command(params, commandText, secrets = {}) { case 'list': action = 'list' listing = true - if (!['commits', 'files', 'reviews', 'comments','pulls'].includes(list_option)) + if (!['commits', 'files', 'reviews', 'comments', 'pulls'].includes(list_option)) return fail(`*expected list_option to be one of 'commits', 'files', 'reviews', 'comments','pulls'*`) if (list_option === 'org') { if (!org) @@ -258,8 +259,6 @@ const success = async (action, header, data, secrets) => { }; const updateURL = (url) => { - if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } - else { url = url.replace('<', '').replace('>', '') } if (url.includes('|')) { url = (url.split('|')[1] || '').replace('>', '') } else { url = url.replace('<', '').replace('>', '') } if (!url.startsWith('http')) { url = 'https://' + url; } From ef9e48e3d91bdbe5fecc0498ec927c47de65920a Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 18:09:49 +0530 Subject: [PATCH 14/19] Add default list_option value --- github/packages/github/issues.js | 6 +++--- github/packages/github/labels.js | 3 ++- github/packages/github/pulls.js | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/github/packages/github/issues.js b/github/packages/github/issues.js index b002c93c..c771d9e2 100644 --- a/github/packages/github/issues.js +++ b/github/packages/github/issues.js @@ -44,14 +44,14 @@ async function command(params, commandText, secrets = {}) { labels = '', state = '', reason = '', - list_option = 'repository', + list_option, org = '', since = '', per_page = 50, page = 1, host } = params; - console.log(params); + list_option = list_option || 'repository' let method = 'GET' let data = {} let lock = false @@ -65,7 +65,7 @@ async function command(params, commandText, secrets = {}) { switch (action) { case 'c': case 'cr': - case 'add': + case 'add': case 'create': action = 'create' method = 'POST' diff --git a/github/packages/github/labels.js b/github/packages/github/labels.js index 84c97f9f..c3234a3f 100644 --- a/github/packages/github/labels.js +++ b/github/packages/github/labels.js @@ -43,7 +43,7 @@ async function command(params, commandText, secrets = {}) { color, description, labels = [], - list_option = 'repo', + list_option, milestone_number, since, sort = 'created', @@ -52,6 +52,7 @@ async function command(params, commandText, secrets = {}) { page = 1, host } = params; + list_option = list_option || 'repo' let method = 'GET' let data = {} let list_path, listing = false diff --git a/github/packages/github/pulls.js b/github/packages/github/pulls.js index c468eeaf..04769925 100644 --- a/github/packages/github/pulls.js +++ b/github/packages/github/pulls.js @@ -48,13 +48,14 @@ async function command(params, commandText, secrets = {}) { milestone = '', labels = '', state = '', - list_option = 'pulls', + list_option, org = '', since = '', per_page = 50, page = 1, host } = params; + list_option = list_option || 'pulls' let method = 'GET' let data = {} let lock = false From fa79abce47dfe95747004f6a4525f4d769f61b77 Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 19:43:16 +0530 Subject: [PATCH 15/19] Remove kv.js file from auth command --- auth/authCheck.js | 2 +- auth/kv.js | 214 ------------------------------- auth/packages/auth/auth/index.js | 4 - 3 files changed, 1 insertion(+), 219 deletions(-) delete mode 100644 auth/kv.js diff --git a/auth/authCheck.js b/auth/authCheck.js index 611f463d..244cd301 100644 --- a/auth/authCheck.js +++ b/auth/authCheck.js @@ -1,4 +1,4 @@ -const nimbella = require('./oauth/node_modules/@nimbella/sdk') +const nimbella = require('@nimbella/sdk') const kv = nimbella.redis() const baseUrl = 'https://github.com' const authorizePath = '/login/oauth/authorize' diff --git a/auth/kv.js b/auth/kv.js deleted file mode 100644 index 6a34f1d9..00000000 --- a/auth/kv.js +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Nimbella CONFIDENTIAL - * --------------------- - * - * 2018 - present Nimbella Corp - * All Rights Reserved. - * - * NOTICE: - * - * All information contained herein is, and remains the property of - * Nimbella Corp and its suppliers, if any. The intellectual and technical - * concepts contained herein are proprietary to Nimbella Corp and its - * suppliers and may be covered by U.S. and Foreign Patents, patents - * in process, and are protected by trade secret or copyright law. - * - * Dissemination of this information or reproduction of this material - * is strictly forbidden unless prior written permission is obtained - * from Nimbella Corp. - */ - -let kv = {} - -function clearTimeout(k) { - const x = kv[k] - if (x && x.timer) { - clearTimeout(x.timer) - } -} - -// https://redis.io/commands/flushall -const flushall = () => { - kv = {} - return Promise.resolve('OK') -} - -// https://redis.io/commands/keys -const keys = (p) => { - if (p) { - if (p.substring(p.length - 1) === '*') p = p.slice(0, -1); - return Object.keys(kv).filter(key => key.startsWith(p)) - } - else { return Object.keys(kv); } -} - -// https://redis.io/commands/get -const get = (k) => { - if (k in kv) { - return kv[k].value || kv[k] - } else return null -} - -// https://redis.io/commands/set -const set = (k, value) => { - clearTimeout(k) - kv[k] = { value } - return 'OK' -} - -// https://redis.io/commands/incr -const incr = (k) => { - let c = get(k) - if (c == null) { - set(k, 1) - return 1 - } else { - c = c + 1 - set(k, c) - return c - } -} - -// https://redis.io/commands/setex -const setex = (k, ttl, value) => { - const res = set(k, value) - expire(k, ttl) - return res -} - -// https://redis.io/commands/del -const del = (k) => { - if (k in kv) { - clearTimeout(k) - delete kv.k - return 1 - } else return 0 -} - -// https://redis.io/commands/expire -const expire = (k, t) => { - if (k in kv) { - clearTimeout(k) - kv[k].timer = setTimeout(() => del(k), t * 1000) - return 1 - } else return 0 -} - -// https://redis.io/commands/sadd -const sadd = (k, v) => { - if (kv[k] === undefined) { - kv[k] = new Set() - } - - const s = kv[k] - const n = s.size - s.add(v) - return s.size - n -} - -// https://redis.io/commands/srem -const srem = (k, v) => { - const s = kv[k] - if (s !== undefined) { - const n = s.size - s.remove(v) - return s.size - n - } else return 0 -} - -// https://redis.io/commands/smembers -const smembers = (k) => { - const s = kv[k] - if (s) return Array.from(s.keys()) - else return [] -} - -// https://redis.io/commands/rpush -const rpush = (k, v) => { - if (kv[k] === undefined) { - kv[k] = [] - } - - const l = kv[k] - l.push(v) - return l.length -} - -// https://redis.io/commands/lpush -const lpush = (k, v) => { - if (kv[k] === undefined) { - kv[k] = [] - } - - const l = kv[k] - l.unshift(v) - return l.length -} - -// https://redis.io/commands/lrange -const lrange = (k, i, j) => { - const l = kv[k] - if (l !== undefined) { - // TODO: this is not quite the same as redis - return l.slice(i, j) - } else return [] -} - -// https://redis.io/commands/llen -const llen = (k) => { - const l = kv[k] - if (l === undefined) { - return 0 - } else if (Array.isArray(l)) { - return l.length - } else { - throw new Error('not an array') - } -} - -module.exports = { - redis: () => ({ - flushall, - keys, - get, - set, - incr, - setex, - del, - expire, - sadd, - srem, - smembers, - rpush, - lpush, - lrange, - llen, - flushallAsync: flushall, - keysAsync: (k) => Promise.resolve(keys(k)), - getAsync: (k) => Promise.resolve(get(k)), - setAsync: (k, v) => Promise.resolve(set(k, v)), - incrAsync: (k) => Promise.resolve(incr(k)), - setexAsync: (k, t, v) => Promise.resolve(setex(k, t, v)), - delAsync: (k) => Promise.resolve(del(k)), - expireAsync: (k, t) => Promise.resolve(expire(k, t)), - smembersAsync: (s) => Promise.resolve(smembers(s)), - rpushAsync: (k, v) => Promise.resolve(rpush(k, v)), - lpushAsync: (k, v) => Promise.resolve(lpush(k, v)), - lrangeAsync: (k, i, j) => Promise.resolve(lrange(k, i, j)), - llenAsync: (k) => { - try { - const result = llen(k) - return Promise.resolve(result) - } catch (e) { - return Promise.reject(e.message) - } - } - }), - storage: () => ({ - id: `bucket`, - setMetadata: () => Promise.resolve({}), - file: (filename) => ({ - getSignedUrl: (options) => Promise.resolve([`signed-${filename}-${options.action}`]) - }) - }) -} diff --git a/auth/packages/auth/auth/index.js b/auth/packages/auth/auth/index.js index 86fc82a8..54962bc6 100644 --- a/auth/packages/auth/auth/index.js +++ b/auth/packages/auth/auth/index.js @@ -93,10 +93,6 @@ async function command(params, commandText, secrets = {}) { duration = 1 // in seconds } = params; - // const key = 'vOVH6pripNWjRRIqRodrdxchalwHzfr3' - // const e = encrypt('testEncrypt', key) - // console.log(decrypt(e, key)) - auth_url = extractURL(auth_url) access_token_url = extractURL(access_token_url) base_url = extractURL(base_url) From 6730a67f48caeac1a0c78189c414e72347c7937e Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 21:19:25 +0530 Subject: [PATCH 16/19] Improves github pull command --- github/packages/github/pulls.js | 35 +++++++++++---------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/github/packages/github/pulls.js b/github/packages/github/pulls.js index 04769925..0ecc0203 100644 --- a/github/packages/github/pulls.js +++ b/github/packages/github/pulls.js @@ -10,7 +10,7 @@ const headers = { async function Request(url, action, method, data, secrets) { // get, list for public repos do not need access token - if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } + if (!secrets.github_token && !['list', 'get', 'check'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') @@ -42,7 +42,7 @@ async function command(params, commandText, secrets = {}) { head = '', base = '', body = '', - draft = false, + draft: d, maintainer_can_modify = false, assignees = '', milestone = '', @@ -55,10 +55,10 @@ async function command(params, commandText, secrets = {}) { page = 1, host } = params; - list_option = list_option || 'pulls' + list_option = list_option || 'pulls' let method = 'GET' let data = {} - let lock = false + let merge = false let listing = false let list_path = '' const { github_repos, github_host } = secrets; @@ -81,7 +81,7 @@ async function command(params, commandText, secrets = {}) { head, base, body, - draft, + draft: d, issue, maintainer_can_modify } @@ -115,43 +115,34 @@ async function command(params, commandText, secrets = {}) { listing = true if (!['commits', 'files', 'reviews', 'comments', 'pulls'].includes(list_option)) return fail(`*expected list_option to be one of 'commits', 'files', 'reviews', 'comments','pulls'*`) - if (list_option === 'org') { - if (!org) - return fail('*please specify org name*') - list_path = `/orgs/${org}` - } - if (list_option === 'user') - list_path = `/user` - if (list_option === 'repository') + if (list_option === 'pulls') listing = false + else + list_path = `/${list_option}` break; case 'ch': case 'check': action = 'check' - lock = true if (!repository) return fail('*please specify repository*') if (!pr_number) return fail('*please specify pr number*') - data = { - locked: true, - } break; case 'm': case 'merge': action = 'merge' method = 'PUT' - lock = true + merge = true if (!repository) return fail('*please specify repository*') if (!pr_number) return fail('*please specify a pr number*') break; default: - return fail(`*Invalid Action. Expected options: 'add', 'update', 'get', 'list', 'lock', 'unlock' *`) + return fail(`*Invalid Action. Expected options: 'add', 'update', 'get', 'list', 'check', 'merge' *`) } if (secrets.github_token) { [, tokenHost] = secrets.github_token.split('@') } baseURL = host || tokenHost || github_host || baseURL baseURL = updateURL(baseURL) - const url = `${baseURL}/${listing ? list_path : `repos/${repository}`}/pulls${pr_number ? `/${pr_number}` : ''}${lock ? `/lock` : ''}` + const url = `${baseURL}/${listing ? list_path : `repos/${repository}`}/pulls${pr_number ? `/${pr_number}` : ''}${merge ? `/merge` : ''}` console.log(url); const res = await Request(url, action, method, data, secrets) @@ -243,10 +234,6 @@ const success = async (action, header, data, secrets) => { }; if (action === 'list') _list(data || [], response) - else if (action === 'lock') - response.blocks.push(section(`Pull Locked.`)) - else if (action === 'unlock') - response.blocks.push(section(`Pull Unlocked.`)) else _get(data, response) From beec92cde5108f6ba2ed617a72d1d83d6864bd73 Mon Sep 17 00:00:00 2001 From: crai Date: Fri, 19 Mar 2021 21:19:42 +0530 Subject: [PATCH 17/19] Improves github billing command --- github/commands.yaml | 51 +++++++++++-- github/packages/github/billing.js | 117 +++++++++++++++++------------- 2 files changed, 112 insertions(+), 56 deletions(-) diff --git a/github/commands.yaml b/github/commands.yaml index 618ae7d4..344a8ed9 100644 --- a/github/commands.yaml +++ b/github/commands.yaml @@ -81,7 +81,7 @@ commands: optional: true options: - name: q - value: query + value: query - name: r value: repositories - name: l @@ -119,11 +119,11 @@ commands: - name: c value: comment_id - name: b - value: body + value: body - name: h value: host issues: - description: Manage Issues and PRs + description: Manage Issues parameters: - name: action optional: true @@ -199,9 +199,18 @@ commands: - name: h value: host billing: - description: Manage Billing + description: See Billing Details parameters: - - name: action + - name: entity + options: + - name: t + value: type + - name: o + value: org + - name: u + value: user + - name: h + value: host hooks: description: Manage Hooks parameters: @@ -209,7 +218,37 @@ commands: pulls: description: Manage Pulls parameters: - - name: action + - name: action + options: + - name: r + value: repository + - name: p + value: pr_number + - name: i + value: issue + - name: a + value: assignees + - name: t + value: title + - name: b + value: body + - name: head + value: head + - name: base + value: base + - name: l + value: labels + - name: s + value: state + - name: reason + value: reason + - name: list_option + value: list_option + - name: o + value: org + - name: h + value: host + - name: d github: description: View GitHub command set documentation options: diff --git a/github/packages/github/billing.js b/github/packages/github/billing.js index 79dfdadc..d2b53d22 100644 --- a/github/packages/github/billing.js +++ b/github/packages/github/billing.js @@ -9,8 +9,6 @@ const headers = { async function Request(url, action, method, data, secrets) { - // get, list for public repos do not need access token - if (!secrets.github_token && !['list', 'get'].includes(action)) { return fail('*please add github_token secret*') } if (secrets.github_token) { let token [token,] = secrets.github_token.split('@') @@ -34,54 +32,57 @@ async function Request(url, action, method, data, secrets) { async function command(params, commandText, secrets = {}) { let tokenHost, baseURL = 'https://api.github.com' let { - type = 'orgs', - entity, - action, - repository, - org = '', - since = '', - per_page = 50, - page = 1, + entity = 'package', + type, + org, + user, host } = params; + let method = 'GET' let data = {} - let lock = false - let listing = false let list_path = '' - const { github_repos, github_host } = secrets; - const default_repos = repository ? repository : github_repos; - if (default_repos) { - repository = default_repos.split(',').map(repo => repo.trim())[0]; - } - switch (action) { - case 'g': - case 'get': - action = 'get'; - if (!repository) return fail('*please specify repository*') - break; - case 'l': - case 'ls': - case 'list': - action = 'list' + const { github_host } = secrets; + type = type || 'user' + if (type === 'org' && !org) return fail('*please specify organization*') + if (type === 'user' && !user) return fail('*please specify user*') + + if (!['org', 'user'].includes(type)) + return fail(`*expected type to be one of 'org', 'user'*`) + + switch (entity) { + case 'a': + case 'action': + entity = 'action'; + list_path = 'actions' + break; + case 'p': + case 'package': + entity = 'package' + list_path = 'packages' + break; + case 's': + case 'storage': + entity = 'storage' + list_path = 'shared-storage' break; default: - return fail(`*Invalid Action. Expected options: 'get', 'list' *`) + return fail(`*Invalid Entity. Expected options: 'action', 'package', 'storage' *`) } if (secrets.github_token) { [, tokenHost] = secrets.github_token.split('@') } baseURL = host || tokenHost || github_host || baseURL baseURL = updateURL(baseURL) - const url = `${baseURL}/${type}/${type === 'orgs' ? org : user}/settings/billing/${entity}` + const url = `${baseURL}/${type}s/${type === 'org' ? org : user}/settings/billing/${list_path}` console.log(url); - const res = await Request(url, action, method, data, secrets) + const res = await Request(url, entity, method, data, secrets) if (res) { const tokenMessage = secrets.github_token ? '' : '*For greater limits you can add using*\n `/nc secret_create`'; const currReading = parseInt(res.headers['x-ratelimit-remaining']); - let header = `\nPull *${action.charAt(0).toUpperCase() + action.substr(1)}* Request Result:`; + let header = `\nBilling *${entity.charAt(0).toUpperCase() + entity.substr(1)}* Request Result:`; if (currReading < requestThreshold) { header = `:warning: *You are about to reach the api rate limit.* ${tokenMessage}`; } @@ -89,7 +90,7 @@ async function command(params, commandText, secrets = {}) { header = `:warning: *The api rate limit has been exhausted.* ${tokenMessage}`; return fail(header); } - return success(action, header, res.data, secrets); + return success(entity, header, res.data, secrets); } return fail(undefined, res); } @@ -135,39 +136,55 @@ const getErrorMessage = (error) => { } }; -const _get = (item, response) => { +const _actions = (item, response) => { const block = { type: 'section', fields: [ - mdText(`<${item.html_url}|${item.number}> \n ${item.title} ${item.milestone ? `\n ${item.milestone.title}` : ''} - ${item.assignees.length > 0 ? `\n ${item.assignees.map(a => `<${a.html_url}|${a.login}>`).join()}` : ''} - ${item.labels.length > 0 ? `\n ${item.labels.map(l => l.name).join()}` : ''} - `), - mdText(`*State:* ${item.state.charAt(0).toUpperCase() + item.state.substr(1)} - \n*Created:* - \n*Updated:* - ${item.closed_at ? `\n*Closed:* ` : ''}`), + mdText(`*Total Minutes Used:* ${item.total_minutes_used} + \n*Total Paid Minutes Used:* ${item.total_paid_minutes_used} + \n*Included Minutes:* ${item.included_minutes} + \n*Minutes Used Breakdown:* _Ubuntu_ ${item.minutes_used_breakdown.UBUNTU}, _Mac_ ${item.minutes_used_breakdown.MACOS}, + _Windows_ ${item.minutes_used_breakdown.WINDOWS}`), ], } - if (item.assignee) block.accessory = image(item.assignee.avatar_url, item.assignee.login) response.blocks.push(block) - if (item.body) response.blocks.push(section(`${item.body.length > 500 ? item.body.substr(0, 500) + '...' : item.body}`.replace(/#(\d+)/g, `<${item.html_url.split('/').splice(0, 5).join('/')}/pulls/$1|#$1>`))); }; -const _list = (items, response) => (items).forEach((item) => { - _get(item, response) -}); +const _packages = (item, response) => { + const block = { + type: 'section', + fields: [ + mdText(`*Total Gigabytes Bandwidth Used:* ${item.total_gigabytes_bandwidth_used} + \n*Total Paid Gigabytes Bandwidth Used:* ${item.total_paid_gigabytes_bandwidth_used} + \n*Included Gigabytes Bandwidth:* ${item.included_gigabytes_bandwidth}`), + ], + } + response.blocks.push(block) +}; +const _storage = (item, response) => { + const block = { + type: 'section', + fields: [ + mdText(`*Days Left In Billing Cycle:* ${item.days_left_in_billing_cycle} + \n*Estimated Paid Storage For Month:* ${item.estimated_paid_storage_for_month} + \n*Estimated Storage For Month:* ${item.estimated_storage_for_month}`), + ], + } + response.blocks.push(block) +}; -const success = async (action, header, data, secrets) => { +const success = async (entity, header, data, secrets) => { const response = { response_type: 'in_channel', blocks: [section(header)], }; - if (action === 'list') - _list(data || [], response) + if (entity === 'package') + _packages(data || [], response) + else if (entity === 'storage') + _storage(data || [], response) else - _get(data, response) + _actions(data, response) response.blocks.push({ type: 'context', From 0ade8723ccf35e9afe1b2c34d2145a888a69b795 Mon Sep 17 00:00:00 2001 From: crai Date: Mon, 22 Mar 2021 20:49:03 +0530 Subject: [PATCH 18/19] [weather] allow multi word city name w/o quotes --- weather/commands.yaml | 3 +-- weather/packages/weather/weather.js | 8 ++++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/weather/commands.yaml b/weather/commands.yaml index 383a59c3..cb5c7d84 100644 --- a/weather/commands.yaml +++ b/weather/commands.yaml @@ -2,5 +2,4 @@ commands: weather: description: weather conditions for a city parameters: - - name: city - optional: false + - name: ... diff --git a/weather/packages/weather/weather.js b/weather/packages/weather/weather.js index 4f9b0238..350ba639 100644 --- a/weather/packages/weather/weather.js +++ b/weather/packages/weather/weather.js @@ -6,7 +6,7 @@ const fail = (msg) => { console.log(msg); return { response_type: 'in_channel', - text: 'Couldn\'t get the weather conditions.', + text: msg || 'Couldn\'t get the weather conditions.', }; }; @@ -87,12 +87,16 @@ const success = (data) => { async function _command(params, commandText, secrets = {}) { let response; + const { varArgs: city } = params + if (!city) return fail('Please specify city name'); try { - response = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${params.city}&appid=fbbf85ca2d738ae130d6c070c7df7bb7`); + response = await axios.get(`https://api.openweathermap.org/data/2.5/weather?q=${encodeURI(city)}&appid=fbbf85ca2d738ae130d6c070c7df7bb7`); if (response.status !== 200) { return fail(response.status); } } catch (err) { + if (err.response && err.response.status === 404) + return fail(); return fail(err.message); } if (!response) return fail(); From 6eb95706ef0208556a526478c2b2b1bbc95a99d4 Mon Sep 17 00:00:00 2001 From: crai Date: Thu, 12 Aug 2021 12:09:02 +0530 Subject: [PATCH 19/19] add deploy command --- github/packages/github/deploy.js | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 github/packages/github/deploy.js diff --git a/github/packages/github/deploy.js b/github/packages/github/deploy.js new file mode 100644 index 00000000..dd99ba47 --- /dev/null +++ b/github/packages/github/deploy.js @@ -0,0 +1,57 @@ +// jshint esversion: 9 + +/** + * @description null + * @param {ParamsType} params list of command parameters + * @param {?string} commandText text message + * @param {!object} [secrets = {}] list of secrets + * @return {Promise} Response body + */ + async function _command(params, commandText, secrets = {}) { + const { + // event here is the event type specified under github action. + // See https://docs.github.com/en/actions/reference/events-that-trigger-workflows#repository_dispatch for more info + event + } = params; + + var options = { + headers: { + Authorization: 'token ' + secrets.github_token, + Accept: 'application/vnd.github.everest-preview+json' + }, + }; + try { + await axios.post(`https://api.github.com/repos/${secrets.user}/${secrets.repo}/dispatches`, + { event_type: event }, options); + return { + response_type: 'in_channel', + text: "Deployed github action having the event: " + event + }; + } catch (e) { + return { + response_type: 'in_channel', + text: e.message + }; + } +} + +/** + * @typedef {object} SlackBodyType + * @property {string} text + * @property {'in_channel'|'ephemeral'} [response_type] + */ + +const main = async (args) => ({ + body: await _command(args.params, args.commandText, args.__secrets || {}).catch(error => ({ + // To get more info, run `/nc activation_log` after your command executes + response_type: 'ephemeral', + text: `Error: ${error.message}` + })) +}); + +module.exports = main; + + +// repository_dispatch: +// branches: [ master ] +// types: [ accounts-prod ] \ No newline at end of file