UNPKG

@atomist/automation-client

Version:

Atomist API for software low-level client

368 lines (319 loc) • 12.8 kB
import { render, SlackMessage, } from "@atomist/slack-messages"; import * as _ from "lodash"; import { Config } from "winston/lib/winston/config"; import * as WebSocket from "ws"; import { Configuration } from "../../../configuration"; import { CommandReferencingAction, CustomEventDestination, Destination, isFileMessage, isSlackMessage, MessageMimeTypes, MessageOptions, RequiredMessageOptions, SlackDestination, SourceDestination, WebDestination, } from "../../../spi/message/MessageClient"; import { MessageClientSupport } from "../../../spi/message/MessageClientSupport"; import { logger } from "../../../util/logger"; import { redact } from "../../../util/redact"; import { guid, replacer, toStringArray, } from "../../util/string"; import { CommandIncoming, EventIncoming, isCommandIncoming, isEventIncoming, Source, } from "../RequestProcessor"; import { WebSocketLifecycle } from "./WebSocketLifecycle"; export abstract class AbstractMessageClient extends MessageClientSupport { constructor(protected readonly request: CommandIncoming | EventIncoming, protected readonly correlationId: string, protected readonly team: { id: string, name?: string }, protected readonly source: Source, protected readonly configuration: Configuration) { super(); } public async delete(destinations: Destination | Destination[], options: RequiredMessageOptions): Promise<void> { return this.doSend(undefined, destinations, { ...options, delete: true }); } protected async doSend(msg: string | SlackMessage, destinations: Destination | Destination[], options: MessageOptions = {}): Promise<any> { if (!!msg && (msg as HandlerResponse).content_type === "application/x-atomist-continuation+json") { return this.sendResponse(msg).then(() => msg); } const ts = this.ts(options); if (!Array.isArray(destinations)) { destinations = [destinations]; } let destinationIdentifier: "slack" | "ingester" | "web"; const responseDestinations = []; let thread_ts; if (options.thread === true && !!this.source) { thread_ts = _.get(this.source, "slack.message.ts"); } else if (typeof options.thread === "string") { thread_ts = options.thread; } destinations.forEach(d => { if (d.userAgent === SlackDestination.SLACK_USER_AGENT) { destinationIdentifier = "slack"; const sd = d as SlackDestination; toStringArray(sd.channels).filter(c => !!c).forEach(c => { responseDestinations.push({ user_agent: SlackDestination.SLACK_USER_AGENT, slack: { team: { id: sd.team, }, channel: { name: c, }, thread_ts, }, }); }); toStringArray(sd.users).filter(u => !!u).forEach(u => { responseDestinations.push({ user_agent: SlackDestination.SLACK_USER_AGENT, slack: { team: { id: sd.team, }, user: { name: u, }, thread_ts, }, }); }); } else if (d.userAgent === CustomEventDestination.INGESTER_USER_AGENT) { destinationIdentifier = "ingester"; responseDestinations.push({ user_agent: CustomEventDestination.INGESTER_USER_AGENT, ingester: { root_type: (d as CustomEventDestination).rootType, }, }); } else if (d.userAgent === WebDestination.WEB_USER_AGENT) { destinationIdentifier = "web"; } else if (d.userAgent === SourceDestination.SOURCE_USER_AGENT) { destinationIdentifier = (d as SourceDestination).system; responseDestinations.push((d as SourceDestination).source); } }); if (responseDestinations.length === 0 && this.source) { // TODO CD this is probably not always going to be valid destinationIdentifier = "slack"; const responseDestination = _.cloneDeep(this.source); if (responseDestination.slack) { delete responseDestination.slack.user; if (!!thread_ts) { responseDestination.slack.thread_ts = thread_ts; } } responseDestinations.push(responseDestination); } const response: HandlerResponse = { api_version: "1", correlation_id: this.correlationId, team: this.team, source: this.source ? this.source : undefined, command: isCommandIncoming(this.request) ? this.request.command : undefined, event: isEventIncoming(this.request) ? this.request.extensions.operationName : undefined, destinations: responseDestinations, id: options.id ? options.id : undefined, timestamp: ts, ttl: ts && options.ttl ? options.ttl : undefined, post_mode: options.post === "update_only" ? "update_only" : (options.post === "always" ? "always" : "ttl"), }; if (destinationIdentifier === "web") { return Promise.resolve(); } else if (destinationIdentifier === "slack") { if (isSlackMessage(msg)) { const msgClone = _.cloneDeep(msg); const actions = mapActions(msgClone); response.content_type = MessageMimeTypes.SLACK_JSON; response.body = render(msgClone, false); response.actions = actions; } else if (isFileMessage(msg)) { response.content_type = MessageMimeTypes.SLACK_FILE_JSON; response.body = JSON.stringify({ content: msg.content, filename: msg.fileName, filetype: msg.fileType, title: msg.title, initial_comment: msg.comment, }); } else if (typeof msg === "string") { response.content_type = MessageMimeTypes.PLAIN_TEXT; response.body = msg; } else if (!!options.delete) { response.content_type = "application/x-atomist-delete"; response.body === undefined; } if (_.get(this.configuration, "redact.messages", true) === true) { response.body = redact(response.body); } } else if (destinationIdentifier === "ingester") { response.content_type = MessageMimeTypes.APPLICATION_JSON; response.body = JSON.stringify(msg); response.id = (options.id ? options.id : guid()); } return this.sendResponse(response).then(() => response); } protected abstract sendResponse(response: any): Promise<void>; private ts(options: MessageOptions): number { if (options.id) { if (options.ts) { return options.ts; } else { return Date.now(); } } else { return undefined; } } } export class AbstractWebSocketMessageClient extends AbstractMessageClient { constructor(protected readonly ws: WebSocketLifecycle, protected readonly request: CommandIncoming | EventIncoming, protected readonly correlationId: string, protected readonly team: { id: string, name?: string }, protected readonly source: Source, protected readonly configuration: Configuration) { super(request, correlationId, team, source, configuration); } protected async sendResponse(response: any): Promise<void> { this.ws.send(response); } } export class WebSocketCommandMessageClient extends AbstractWebSocketMessageClient { constructor(request: CommandIncoming, ws: WebSocketLifecycle, configuration: Configuration) { super(ws, request, request.correlation_id, request.team, request.source, configuration); } protected async doSend(msg: string | SlackMessage, destinations: Destination | Destination[], options: MessageOptions = {}): Promise<any> { return super.doSend(msg, destinations, options); } } export class WebSocketEventMessageClient extends AbstractWebSocketMessageClient { constructor(request: EventIncoming, ws: WebSocketLifecycle, configuration: Configuration) { super(ws, request, request.extensions.correlation_id, { id: request.extensions.team_id, name: request.extensions.team_name }, null, configuration); } protected async doSend(msg: string | SlackMessage, destinations: Destination | Destination[], options: MessageOptions = {}): Promise<any> { if (!Array.isArray(destinations)) { destinations = [destinations]; } if (destinations.length === 0) { throw new Error("Response messages are not supported for event handlers"); } else { return super.doSend(msg, destinations, options); } } } export function mapActions(msg: SlackMessage): Action[] { const actions: Action[] = []; let counter = 0; if (msg.attachments) { msg.attachments.filter(attachment => attachment.actions).forEach(attachment => { attachment.actions.forEach(a => { if (!!a && !!(a as CommandReferencingAction).command) { const cra = a as CommandReferencingAction; const id = counter++; cra.command.id = `${cra.command.id}-${id}`; a.name = `${a.name}-${id}`; const action: Action = { id: cra.command.id, parameter_name: cra.command.parameterName, command: cra.command.name, parameters: mapParameters(cra.command.parameters), }; actions.push(action); // Lastly we need to delete our extension from the slack action cra.command = undefined; } }); }); return actions; } } function mapParameters(data: {}): Parameter[] { const parameters: Parameter[] = []; for (const key in data) { if (data.hasOwnProperty(key)) { const value = data[key]; if (value) { parameters.push({ name: key, value: value.toString(), }); } else { // logger.debug(`Parameter value for '${key}' is null`); } } } return parameters; } export function sendMessage(message: any, ws: WebSocket, log: boolean = true): void { if (log) { logger.debug(`Sending message '${JSON.stringify(message, replacer)}'`); } ws.send(JSON.stringify(message)); } export function clean(addresses: string[] | string): string[] { let na: string[] = toStringArray(addresses); if (na) { // Filter out any null addresses na = na.filter(nad => nad !== null && nad.length > 0); } return na; } export interface HandlerResponse { api_version: "1"; correlation_id: any; team: { id: string; name?: string; }; command?: string; event?: string; status?: { code: number; reason: string; }; source?: Source; destinations?: any[]; content_type?: string; body?: string; // Updatable messages id?: string; timestamp?: number; ttl?: number; post_mode?: "ttl" | "always" | "update_only"; actions?: Action[]; } export interface Action { id: string; parameter_name?: string; command: string; parameters: Parameter[]; } export interface Parameter { name: string; value: string; }