Last active
March 4, 2019 22:56
-
-
Save ipcrm/69ad4a40b280afc0d380339157f96552 to your computer and use it in GitHub Desktop.
Custom list Skills
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright © 2019 Atomist, Inc. | |
* | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
import { | |
Configuration, | |
configurationValue, | |
HandlerResult, | |
HttpMethod, | |
logger, | |
NoParameters, | |
} from "@atomist/automation-client"; | |
import { | |
CommandHandlerRegistration, | |
CommandListenerInvocation, | |
slackErrorMessage, | |
} from "@atomist/sdm"; | |
import {Attachment, Field} from "@atomist/slack-messages"; | |
import * as slack from "@atomist/slack-messages"; | |
export interface Ingester { | |
root_type: string; | |
url: string; | |
} | |
export interface Command { | |
name: string; | |
description: string; | |
intent: string[]; | |
} | |
export interface Registration { | |
name: string; | |
commands: Command[]; | |
ingesters: Ingester[]; | |
groups?: string[]; | |
version: string; | |
team_ids: string[]; | |
} | |
export interface RegObject { | |
registration: Registration; | |
} | |
export interface ListSkill { | |
sdm: string; | |
intent: string; | |
description: string; | |
} | |
export interface SdmSkills { | |
name: string; | |
version: string; | |
skills: ListSkill[]; | |
} | |
export interface ListSkillsConfig { | |
explicitSdmAllow?: string[]; // If populated, only show intents from these SDMs | |
filterIntents?: string[]; // If populated, for each intent, only display if it includes one of the strings supplied | |
excludeIntentsFilter?: string[]; // If populated, for each intent, only display intent if not excluded | |
} | |
/** | |
* Based on the supplied config, filter intents and return updated SdmSkills | |
* @param {SdmSkills[]} skills | |
* @param {ListSkillsConfig} config | |
* @returns {SdmSkills[]} Array of SdmSkills objects | |
*/ | |
export function applyIntentFilter( | |
skills: SdmSkills[], config: ListSkillsConfig, exclude: boolean = false): SdmSkills[] { | |
const newSkills = skills.map(s => { | |
const tmpSkills = s.skills.filter(i => { | |
if (exclude) { | |
return !(config.excludeIntentsFilter.filter(fi => i.intent.includes(fi)).length > 0); | |
} else { | |
return config.filterIntents.filter(fi => i.intent.includes(fi)).length > 0; | |
} | |
}); | |
return { | |
name: s.name, | |
version: s.version, | |
skills: tmpSkills, | |
}; | |
}); | |
return newSkills.filter(nS => nS.skills.length > 0); | |
} | |
/** | |
* Take a SdmSkills object and filter it based on the configuration supplied ListSkillsConfig | |
* @param {SdmSkills[]} skills | |
* @returns {SdmSkills[]} Filtered SDM Skills | |
*/ | |
export function filterSkills(skills: SdmSkills[]): SdmSkills[] { | |
const config = configurationValue<ListSkillsConfig>("sdm.listSkills", {}); | |
let returnSkills: SdmSkills[] = skills; | |
// Apply SDM Filter | |
if (config.explicitSdmAllow && config.explicitSdmAllow.length > 0) { | |
const lowerExplicitSdmAllow = config.explicitSdmAllow.map(e => e.toLowerCase()); | |
returnSkills = returnSkills.filter(s => lowerExplicitSdmAllow.includes(s.name.toLowerCase())); | |
} | |
// Apply intent filters | |
if (config.filterIntents && config.filterIntents.length > 0) { | |
returnSkills = applyIntentFilter(returnSkills, config); | |
} | |
if (config.excludeIntentsFilter && config.excludeIntentsFilter.length > 0) { | |
returnSkills = applyIntentFilter(returnSkills, config, true); | |
} | |
return returnSkills; | |
} | |
/** | |
* Listener for displaying the list of skills known to Atomist, filtered based on your configuration | |
* @param {CommandListenerInvocation<NoParameters>} cli | |
* @returns {Promise<HandlerResult>} Handler Result | |
*/ | |
export async function listSkillsListener(cli: CommandListenerInvocation<NoParameters>): Promise<HandlerResult> { | |
return new Promise<HandlerResult>(async (resolve, reject) => { | |
interface TeamInfo { | |
id: string; | |
} | |
interface ChatTeamInfo { | |
team: TeamInfo; | |
provider: string; | |
} | |
interface TeamInfo { | |
ChatTeam: ChatTeamInfo[]; | |
} | |
try { | |
const teamInfo = await cli.context.graphClient.query<TeamInfo, {}>( | |
{query: `{ChatTeam{team {id} provider}}`}); | |
let inProgressMsg: any; | |
if (teamInfo.ChatTeam[0].provider === "slack") { | |
inProgressMsg = slack.emoji("hourglass") + "Searching..."; | |
} else if (teamInfo.ChatTeam[0].provider === "msteams") { | |
inProgressMsg = "**Searching...**"; | |
} | |
// In-progress message | |
await cli.addressChannels({ | |
attachments: [ | |
{ | |
pretext: `Here are the skills known to *Atomist*:`, | |
text: inProgressMsg, | |
fallback: `Here are the skills known to *Atomist*:`, | |
}, | |
], | |
}, { | |
id: `list/skills/${cli.configuration.name}`, | |
ttl: 60 * 120, | |
}); | |
const allSkills = await buildSkillsList(cli.configuration, teamInfo.ChatTeam[0].team.id); | |
const skills = filterSkills(allSkills); | |
if (teamInfo.ChatTeam[0].provider === "slack") { | |
const msgBody: any = []; | |
skills.forEach(s => { | |
msgBody.push(`\n*${s.name}:${s.version}*`); | |
s.skills.forEach(i => { | |
msgBody.push(`${slack.codeLine(i.intent)} ${i.description}`); | |
}); | |
}); | |
await cli.addressChannels({ | |
attachments: [ | |
{ | |
pretext: `Here are the skills known to *Atomist*:`, | |
text: msgBody.join("\n"), | |
fallback: `Here are the skills known to *Atomist*:`, | |
}, | |
], | |
}, { | |
id: `list/skills/${cli.configuration.name}`, | |
ttl: 60 * 120, | |
}); | |
} | |
if (teamInfo.ChatTeam[0].provider === "msteams") { | |
const attachments: Attachment[] = []; | |
skills.forEach(s => { | |
const fields: Field[] = []; | |
s.skills.forEach(i => { | |
fields.push({ | |
value: `${i.description}`, | |
title: `${i.intent}`, | |
}); | |
}); | |
attachments.push({ | |
fallback: `${s.name}:${s.version}`, | |
pretext: `**SDM: ${s.name}:${s.version}**`, | |
fields, | |
}); | |
}); | |
await cli.addressChannels({ | |
attachments, | |
}, { | |
id: `list/skills/${cli.configuration.name}`, | |
ttl: 60 * 120, | |
}); | |
} | |
resolve({ | |
code: 0, | |
}); | |
} catch (e) { | |
await cli.addressChannels(slackErrorMessage( | |
`Failed to run list skills`, | |
`${e}`, | |
cli.context), | |
{ id: `list/skills/${cli.configuration.name}`, ttl: 60 * 60}, | |
); | |
reject({ | |
code: 1, | |
message: e, | |
}); | |
} | |
}); | |
} | |
/** Build Skill List | |
* @param {string} Atomist TeamID (Workspace ID) | |
* @param {Configuration} config | |
* @return {ListSkills} Skills to be printed | |
*/ | |
export function buildSkillsList(config: Configuration, teamId: string): Promise<SdmSkills[]> { | |
const skills: SdmSkills[] = []; | |
return new Promise<SdmSkills[]>(async (resolve, reject) => { | |
try { | |
const regInfo = await getRegistrationInfo(config); | |
regInfo.forEach(r => { | |
logger.info(`buildSkillsList: Processing ${r.registration.name}`); | |
if (!r.registration.hasOwnProperty("commands")) { | |
return; | |
} | |
if ( | |
!(r.registration.hasOwnProperty("team_ids") && r.registration.team_ids.includes(teamId)) && | |
!(r.registration.hasOwnProperty("groups") && r.registration.groups.includes("all")) | |
) { | |
return; | |
} | |
const mySkills: ListSkill[] = []; | |
r.registration.commands.forEach(c => { | |
if (c.hasOwnProperty("intent") && c.intent && c.intent.length > 0 ) { | |
c.intent.forEach(i => { | |
mySkills.push({ | |
sdm: r.registration.name, | |
intent: i, | |
description: c.description, | |
}); | |
}); | |
} | |
}); | |
if (mySkills.length > 0) { | |
skills.push({name: r.registration.name, version: r.registration.version, skills: mySkills}); | |
} | |
}); | |
} catch (e) { | |
logger.error(`buildSkillsList: Failed to lookup skill list! Error => ${e}`); | |
reject(e); | |
} | |
resolve(skills); | |
}); | |
} | |
/** | |
* Gets registration info for this API key | |
* @param {Configuration} config | |
* @return {JSON} registration info | |
*/ | |
export const getRegistrationInfo = async (config: Configuration): Promise<RegObject[]> => { | |
logger.debug(`Starting getRegistrationInfo, using URL ${config.endpoints.api}`); | |
const httpClient = config.http.client.factory.create(config.endpoints.api); | |
const authorization = `Bearer ${config.apiKey}`; | |
try { | |
const result = await httpClient.exchange(config.endpoints.api, { | |
method: HttpMethod.Get, | |
headers: { Authorization: authorization }, | |
}); | |
return(result.body as RegObject[]); | |
} catch (e) { | |
logger.error("getRegistrationInfo: Error! Failed to retrieve data. Failure: " + | |
`[Status ${JSON.stringify(e.response.status)}] => ` + | |
JSON.stringify(e.response.data)); | |
throw new Error(e); | |
} | |
}; | |
/** | |
* Command Handler Registration for customized List Skills intent | |
*/ | |
export const listSkills: CommandHandlerRegistration<NoParameters> = { | |
name: "ListSkills", | |
intent: [ | |
"list skills", | |
"show skills", | |
"list skill", | |
"show skill", | |
"ls", | |
], | |
listener: listSkillsListener, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Configuration
Within your client.config.json, add to SDM:
There are three options (all arrays) -
explicitSdmAllow
.filterIntents
, andexcludeIntentsFilter
.explicitSdmAllow
: This array supplies a list of SDMs you want to display intents for. Simply add the name.filterIntents
: This array allows you to supply strings that should filter the displayed intents. Example; if I include a string ofadd
only intents that include the wordadd
will be displayed. Can be used independently or combined with other options.excludeIntentsFilter
: This array allows you to supply strings that should exclude the displayed intents. This will display all intents except the ones matching a string in this array. Can be used independently or combined with other options.Example: Only show intents from the
ipcrmdemo-sdm
and@Atomist/lifecycle-automation
SDMs. Of those intents, only display intents that include the wordadd
. Of those resulting intents, do not display intents that include the stringMaven
.To add to your SDM; simply register the command handler: