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..244cd301 --- /dev/null +++ b/auth/authCheck.js @@ -0,0 +1,161 @@ +const nimbella = require('@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..2172dba4 --- /dev/null +++ b/auth/commands.yaml @@ -0,0 +1,34 @@ +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: a + value: auth_url + - name: b + value: base_url + - name: c + value: callback_url + - name: d + value: duration + - name: g + value: grant_type + - name: i + value: client_id + - name: p + value: scope + - name: s + value: client_secret + - name: t + value: access_token_url + limits: + logs: 10 + 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/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..54962bc6 --- /dev/null +++ b/auth/packages/auth/auth/index.js @@ -0,0 +1,344 @@ +/* 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', 'getcallback'] +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/${process.env.__OW_ACTION_NAME.replace('/auth', '/callback')}`, + client_id, + client_secret, + scope = 'user:email,read:org', + duration = 1 // in seconds + } = params; + + 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 (!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; + 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','getcallback' *`) + } + 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) + return await postResult(webhook, response) + } + else { + 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 diff --git a/github/commands.yaml b/github/commands.yaml index e0a5228f..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 @@ -198,5 +198,58 @@ commands: value: milestone_number - name: h value: host + billing: + description: See Billing Details + parameters: + - name: entity + options: + - name: t + value: type + - name: o + value: org + - name: u + value: user + - name: h + value: host + hooks: + description: Manage Hooks + parameters: + - name: action + pulls: + description: Manage Pulls + parameters: + - 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: + - name: v diff --git a/github/package.json b/github/package.json new file mode 100644 index 00000000..6143f0ba --- /dev/null +++ b/github/package.json @@ -0,0 +1,9 @@ +{ + "name": "github_command_set", + "version": "1.0.0", + "description": "Nimbella Commander GitHub Command Set", + "main": "github.js", + "keywords": [], + "author": "", + "license": "ISC" +} 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 new file mode 100644 index 00000000..d2b53d22 --- /dev/null +++ b/github/packages/github/billing.js @@ -0,0 +1,213 @@ +// 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) { + 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 { + entity = 'package', + type, + org, + user, + host + } = params; + + let method = 'GET' + let data = {} + let list_path = '' + 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 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}s/${type === 'org' ? org : user}/settings/billing/${list_path}` + console.log(url); + 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 = `\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}`; + } + if (currReading === 0) { + header = `:warning: *The api rate limit has been exhausted.* ${tokenMessage}`; + return fail(header); + } + return success(entity, 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 _actions = (item, response) => { + const block = { + type: 'section', + fields: [ + 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}`), + ], + } + response.blocks.push(block) +}; + +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 (entity, header, data, secrets) => { + const response = { + response_type: 'in_channel', + blocks: [section(header)], + }; + if (entity === 'package') + _packages(data || [], response) + else if (entity === 'storage') + _storage(data || [], response) + else + _actions(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.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; 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/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 diff --git a/github/packages/github/github.js b/github/packages/github/github.js index 6ff7a47a..eb38e940 100644 --- a/github/packages/github/github.js +++ b/github/packages/github/github.js @@ -77,6 +77,20 @@ async function _command(params, commandText, secrets = {}) { }; }; + if (params.v) { + const pjson = require('../../package.json'); + return { + response_type: 'in_channel', + [client !== 'mattermost' ? 'blocks' : 'text']: + [mui( + section( + `GitHub Command Set Version: *${pjson.version}*` + ), + client + )] + }; + } + const result = [ mui( section( diff --git a/github/packages/github/hooks.js b/github/packages/github/hooks.js new file mode 100644 index 00000000..754861f1 --- /dev/null +++ b/github/packages/github/hooks.js @@ -0,0 +1,263 @@ +// 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) { + 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, + 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: events ? events.split(',').map(a => a.trim()) : ['push'], + 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.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; diff --git a/github/packages/github/issues.js b/github/packages/github/issues.js index 8c23731b..c771d9e2 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('@') @@ -43,13 +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; + list_option = list_option || 'repository' let method = 'GET' let data = {} let lock = false @@ -63,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' @@ -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..c3234a3f 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('@') @@ -42,7 +43,7 @@ async function command(params, commandText, secrets = {}) { color, description, labels = [], - list_option = 'repo', + list_option, milestone_number, since, sort = 'created', @@ -51,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/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 new file mode 100644 index 00000000..0ecc0203 --- /dev/null +++ b/github/packages/github/pulls.js @@ -0,0 +1,264 @@ +// jshint esversion: 9 + +const axios = require('axios'); + +const requestThreshold = 3 +const headers = { + 'Content-Type': 'application/json', +}; + + +async function Request(url, action, method, data, secrets) { + // get, list for public repos do not need access token + 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('@') + 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: d, + maintainer_can_modify = false, + assignees = '', + milestone = '', + labels = '', + state = '', + list_option, + org = '', + since = '', + per_page = 50, + page = 1, + host + } = params; + list_option = list_option || 'pulls' + let method = 'GET' + let data = {} + let merge = 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: d, + 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 === 'pulls') + listing = false + else + list_path = `/${list_option}` + break; + case 'ch': + case 'check': + action = 'check' + if (!repository) return fail('*please specify repository*') + if (!pr_number) return fail('*please specify pr number*') + break; + case 'm': + case 'merge': + action = 'merge' + method = 'PUT' + 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', '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}` : ''}${merge ? `/merge` : ''}` + 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.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; 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();