diff --git a/packages/bot-utils/src/get.ts b/packages/bot-utils/src/get.ts index ad47ca3..005d15c 100644 --- a/packages/bot-utils/src/get.ts +++ b/packages/bot-utils/src/get.ts @@ -1,7 +1,7 @@ /* eslint-disable no-await-in-loop */ import got, { Headers, Response } from 'got'; -import type { MeasurementResponse } from './types.js'; +import type { Measurement } from './types.js'; import { userAgent } from './user-agent.js'; class MeasurementsFetcher { @@ -12,7 +12,7 @@ class MeasurementsFetcher { etags: Record; // caches Measurements by ETag - measurements: Record; + measurements: Record; constructor (apiUrl: string) { this.apiUrl = apiUrl; @@ -20,7 +20,7 @@ class MeasurementsFetcher { this.measurements = {}; } - async fetchMeasurement (id: string): Promise { + async fetchMeasurement (id: string): Promise { const headers: Headers = { 'User-Agent': userAgent(), 'Accept-Encoding': 'br', @@ -59,7 +59,7 @@ class MeasurementsFetcher { } } - const measurementResponse: MeasurementResponse = JSON.parse(res.body); + const measurementResponse: Measurement = JSON.parse(res.body); // save etag and response to cache etag = res.headers.etag; @@ -78,7 +78,7 @@ export const ApiUrl = 'https://api.globalping.io/v1/measurements'; // api poll interval in milliseconds const apiPollInterval = 1000; -export const getMeasurement = async (id: string): Promise => { +export const getMeasurement = async (id: string): Promise => { const measurementsFetcher = new MeasurementsFetcher(ApiUrl); let data = await measurementsFetcher.fetchMeasurement(id); @@ -93,11 +93,15 @@ export const getMeasurement = async (id: string): Promise = }; export const getTag = (tags: string[]): string | undefined => { - if (tags.length === 0) { return undefined; } + if (tags.length === 0) { + return undefined; + } // Iterarate through tags and return the first one that has its last character be a number for (const tag of tags) { - if (Number.isInteger(Number(tag.slice(-1)))) { return `${tag}`; } + if (Number.isInteger(Number(tag.slice(-1)))) { + return `${tag}`; + } } return undefined; diff --git a/packages/bot-utils/src/measurements-request.ts b/packages/bot-utils/src/measurements-request.ts index 9bf7e59..0c1321d 100644 --- a/packages/bot-utils/src/measurements-request.ts +++ b/packages/bot-utils/src/measurements-request.ts @@ -13,19 +13,19 @@ import { isHttpProtocol, isMtrProtocol, isTraceProtocol, - Locations, - PostMeasurement, + Location, + MeasurementCreate, } from './types.js'; import { throwArgError } from './utils.js'; -function buildLocations (from: string): Locations[] { +function buildLocations (from: string): Location[] { return from .split(',') .map(f => f.trim()) - .map((l): Locations => ({ magic: l })); + .map((l): Location => ({ magic: l })); } -export const buildPostMeasurements = (args: Flags): PostMeasurement => { +export const buildPostMeasurements = (args: Flags): MeasurementCreate => { const { cmd, target, diff --git a/packages/bot-utils/src/post.ts b/packages/bot-utils/src/post.ts index 752d442..094ffed 100644 --- a/packages/bot-utils/src/post.ts +++ b/packages/bot-utils/src/post.ts @@ -1,13 +1,17 @@ import got, { HTTPError } from 'got'; import { PostError } from './errors.js'; -import type { PostMeasurement, PostMeasurementResponse } from './types.js'; +import type { + Location, + MeasurementCreate, + MeasurementCreateResponse, +} from './types.js'; import { userAgent } from './user-agent.js'; export const postMeasurement = async ( - opts: PostMeasurement, + opts: MeasurementCreate, token?: string, -): Promise => { +): Promise => { try { const headers: { [key: string]: string } = { 'Content-Type': 'application/json', @@ -27,7 +31,7 @@ export const postMeasurement = async ( if (res.statusCode !== 202) { const body = JSON.parse(res.body); - body.location = opts.locations[0].magic; + body.location = (opts.locations as Location[])[0].magic; throw new Error(body); } @@ -35,7 +39,7 @@ export const postMeasurement = async ( } catch (error) { if (error instanceof HTTPError) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const location = opts.locations[0].magic!; + const location = (opts.locations as Location[])[0].magic!; const newError = new PostError(error.response, location, error.message); throw newError; diff --git a/packages/bot-utils/src/types.ts b/packages/bot-utils/src/types.ts index a26ff5f..3041e02 100644 --- a/packages/bot-utils/src/types.ts +++ b/packages/bot-utils/src/types.ts @@ -1,15 +1,20 @@ -// types +// Docs: https://globalping.io/docs/api.globalping.io + export const ALLOWED_QUERY_TYPES = [ 'ping', 'traceroute', 'dns', 'mtr', 'http', - 'auth', - 'limits', ] as const; export type QueryType = (typeof ALLOWED_QUERY_TYPES)[number]; -export const isQueryType = (type: string): type is QueryType => ALLOWED_QUERY_TYPES.includes(type as QueryType); + +export const ALLOWED_ADDITIONAL_QUERY_TYPES = [ 'auth', 'limits' ] as const; +export type AdditionalQueryType = + (typeof ALLOWED_ADDITIONAL_QUERY_TYPES)[number]; + +export const isQueryType = (type: string): type is QueryType => ALLOWED_QUERY_TYPES.includes(type as QueryType) + || ALLOWED_ADDITIONAL_QUERY_TYPES.includes(type as AdditionalQueryType); // filters export const ALLOWED_LOCATION_TYPES = [ @@ -38,6 +43,7 @@ export const ALLOWED_DNS_TYPES = [ 'CNAME', 'DNSKEY', 'DS', + 'HTTPS', 'MX', 'NS', 'NSEC', @@ -69,186 +75,311 @@ export type HttpMethod = (typeof ALLOWED_HTTP_METHODS)[number]; export const isHttpMethod = (type: string): type is HttpMethod => ALLOWED_HTTP_METHODS.includes(type as HttpMethod); // Post Types -export interface Locations { +export interface Location { continent?: string; region?: string; country?: string; state?: string; city?: string; - network?: string; asn?: number; + network?: string; + tags?: string[]; magic?: string; + limit?: number; } -interface SharedMeasurement { +interface MeasurementCreateOptionsBase { + ipVersion?: number; // 4 | 6. Only allowed if the target is a hostname. +} + +// https://globalping.io/docs/api.globalping.io#post-/v1/measurements +interface MeasurementCreateBase { target: string; - limit: number; inProgressUpdates: boolean; - locations: Locations[]; + locations: string | Location[]; + limit: number; } -export interface PingMeasurement extends SharedMeasurement { +export interface PingMeasurementCreateOptions + extends MeasurementCreateOptionsBase { + packets?: number; +} + +export interface PingMeasurementCreate extends MeasurementCreateBase { type: 'ping'; - measurementOptions?: { - packets?: number; - }; + measurementOptions?: PingMeasurementCreateOptions; +} + +export interface TracerouteMeasurementCreateOptions + extends MeasurementCreateOptionsBase { + port?: number; + protocol?: TraceProtocol; } -export interface TraceMeasurement extends SharedMeasurement { + +export interface TracerouteMeasurementCreate extends MeasurementCreateBase { type: 'traceroute'; - measurementOptions?: { - protocol?: TraceProtocol; - port?: number; + measurementOptions: TracerouteMeasurementCreateOptions; +} + +export interface DnsMeasurementCreateOptions + extends MeasurementCreateOptionsBase { + query?: { + type: DnsType; }; + resolver?: string; + port?: number; + protocol?: DnsProtocol; + trace?: boolean; } -export interface DnsMeasurement extends SharedMeasurement { +export interface DnsMeasurementCreate extends MeasurementCreateBase { type: 'dns'; - measurementOptions?: { - query?: { - type: DnsType; - }; - protocol?: DnsProtocol; - port?: number; - resolver?: string; - trace?: boolean; - }; + measurementOptions?: DnsMeasurementCreateOptions; } -export interface MtrMeasurement extends SharedMeasurement { +export interface MtrMeasurementCreateOptions + extends MeasurementCreateOptionsBase { + port?: number; + protocol?: MtrProtocol; + packets?: number; +} + +export interface MtrMeasurementCreate extends MeasurementCreateBase { type: 'mtr'; - measurementOptions?: { - protocol?: MtrProtocol; - port?: number; - packets?: number; + measurementOptions?: MtrMeasurementCreateOptions; +} + +export interface HttpMeasurementCreateOptions + extends MeasurementCreateOptionsBase { + request?: { + host?: string; + path?: string; + query?: string; + method?: HttpMethod; + headers?: Record; }; + resolver?: string; + port?: number; + protocol?: HttpProtocol; } -export interface HttpMeasurement extends SharedMeasurement { +export interface HttpMeasurementCreate extends MeasurementCreateBase { type: 'http'; - measurementOptions?: { - port?: number; - protocol?: HttpProtocol; - request?: { - path?: string; - query?: string; - method?: HttpMethod; - host?: string; - headers?: Record; - }; - }; + measurementOptions?: HttpMeasurementCreateOptions; } -export type PostMeasurement = - | PingMeasurement - | TraceMeasurement - | DnsMeasurement - | MtrMeasurement - | HttpMeasurement; +export type MeasurementCreate = + | PingMeasurementCreate + | TracerouteMeasurementCreate + | DnsMeasurementCreate + | MtrMeasurementCreate + | HttpMeasurementCreate; -export interface PostMeasurementResponse { +export interface MeasurementCreateResponse { id: string; probesCount: number; } -// Get Types - -interface SharedResults { - probe: { - continent: string; - region: string; - country: string; - state: string | null; - city: string; - asn: number; - longitude: number; - latitude: number; - network: string; - resolvers: string[]; - tags: string[]; - }; - result: { - rawOutput: string; - rawHeaders: string; - rawBody: string; - stats: { - loss: number; - min: number; - avg: number; - max: number; - }; - /* eslint-disable @typescript-eslint/no-explicit-any */ - timings: any; - }; +export interface ProbeDetails { + continent: string; + region: string; + country: string; + state: string | null; + city: string; + asn: number; + network: string; + latitude: number; + longitude: number; + tags: string[]; + resolvers: string[]; } -// Ping -export interface PingResult extends SharedResults { - resolvedAddress: string; - resolvedHostname: string; +export interface InProgressProbeResult { + status: 'in-progress'; + rawOutput: string; } -// Trace -interface TraceTimings { +export interface PingTiming { rtt: number; + ttl: number; } -interface TraceHops { - resolvedAddress: string; - resolvedHostname: string; - timings: TraceTimings[]; + +export interface PingStats { + min: number | null; + avg: number | null; + max: number | null; + total: number; + rcv: number; + drop: number; + loss: number; } -export interface TraceResult extends SharedResults { - resolvedAddress: string; - resolvedHostname: string; - hops: TraceHops[]; + +export interface PingProbeResult { + status: 'finished'; + rawOutput: string; + resolvedAddress: string | null; + resolvedHostname: string | null; + stats: PingStats; + timings: PingTiming[]; } -// DNS -interface DnsTimings { - total: number; +export interface TracerouteTiming { + ttl: number; +} + +export interface TracerouHop { + resolvedAddress: string | null; + resolvedHostname: string | null; + timings: TracerouteTiming[]; +} + +export interface TracerouteProbeResult { + status: 'finished'; + rawOutput: string; + resolvedAddress: string | null; + resolvedHostname: string | null; + hops: TracerouHop[]; } -interface DnsAnswers { +export interface DnsAnswer { name: string; - type: DnsType; + type: string; ttl: number; class: string; value: string; } -export interface DnsResultAnswer { - answers: DnsAnswers[]; +export interface DnsTimings { + total: number; +} + +export interface DnsProbeResult { + status: 'finished'; + rawOutput: string; + statusCode: number; + statusCodeName: string; resolver: string; + answers: DnsAnswer[]; timings: DnsTimings; } -export interface DnsResultBase extends SharedResults { - result: DnsResultAnswer; + +export interface TraceDnsHop { + resolver: string; + answers: DnsAnswer[]; + timings: DnsTimings; +} + +export interface TraceDnsProbeResult { + status: 'finished'; + rawOutput: string; + hops: TraceDnsHop[]; +} + +export interface MtrStats { + min: number; + avg: number; + max: number; + stDev: number; + jMin: number; + jAvg: number; + jMax: number; + total: number; + rcv: number; + drop: number; + loss: number; +} + +export interface MtrTiming { + rtt: number; +} + +export interface MtrHop { + resolvedAddress: string | null; + resolvedHostname: string | null; + asn: number[]; + stats: MtrStats; + timings: MtrTiming[]; +} + +export interface MtrProbeResult { + status: 'finished'; + rawOutput: string; + resolvedAddress: string | null; + resolvedHostname: string | null; + hops: MtrHop[]; } -export interface DnsResultTrace extends SharedResults { - result: { - hops: DnsResultAnswer[]; + +export type HttpHeaders = Record; + +export interface HttpTimings { + total: number; + dns: number; + tcp: number; + tls: number; + firstByte: number; + download: number; +} + +export interface HttpTLS { + authorized: boolean; + error?: string; + createdAt: string; + expiresAt: string; + subject: { + CN: string; + alt: string; + }; + issuer: { + C: string; + O: string; + CN: string; }; + keyType: string | null; + keyBits: number | null; + serialNumber: string; + fingerprint256: string; + publicKey: string | null; } -export type DnsResult = Trace extends boolean - ? DnsResultTrace - : DnsResultBase; +export interface HttpProbeResult { + status: 'finished'; + rawOutput: string; + rawHeaders: string; + rawBody: string | null; + truncated: boolean; + headers: HttpHeaders; + statusCode: number; + statusCodeName: string; + resolvedAddress: string | null; + timings: HttpTimings; + tls: HttpTLS | null; +} -interface SharedMeasurementResponse { +export interface ProbeMeasurement { + probe: ProbeDetails; + result: + | InProgressProbeResult + | PingProbeResult + | TracerouteProbeResult + | DnsProbeResult + | TraceDnsProbeResult + | MtrProbeResult + | HttpProbeResult; +} + +export interface Measurement { id: string; type: QueryType; + target: string; status: 'in-progress' | 'finished'; createdAt: string; updatedAt: string; + probesCount: number; + locations: Location[]; + limit: number; + results: ProbeMeasurement[]; } -export interface PingMeasurementResponse extends SharedMeasurementResponse { - type: 'ping'; - results: PingResult[]; -} - -// TODO: Update types -export type MeasurementResponse = PingMeasurementResponse; - export interface AuthToken { access_token: string; token_type: string; diff --git a/packages/bot-utils/tests/post.test.ts b/packages/bot-utils/tests/post.test.ts index a0522a5..ba62993 100644 --- a/packages/bot-utils/tests/post.test.ts +++ b/packages/bot-utils/tests/post.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'; import { postMeasurement } from '../src/post.js'; import { postMeasurementHandlers, setupAPIServer } from './mocks/index.js'; -import { PostMeasurement } from '../src/types.js'; +import { MeasurementCreate } from '../src/types.js'; describe('Post', () => { setupAPIServer(postMeasurementHandlers); @@ -14,7 +14,7 @@ describe('Post', () => { type: 'ping', limit: 1, locations: [{ magic: 'gb' }], - } as PostMeasurement); + } as MeasurementCreate); expect(res).toEqual({ id: 'testId', probesCount: 1 }); }); }); diff --git a/packages/discord/src/utils.ts b/packages/discord/src/utils.ts index 0c45035..e7a7f37 100644 --- a/packages/discord/src/utils.ts +++ b/packages/discord/src/utils.ts @@ -2,10 +2,15 @@ import { Flags, getTag, - help, + dnsHelpTexts, + pingHelpTexts, + tracerouteHelpTexts, + httpHelpTexts, + mtrHelpTexts, loggerInit, - MeasurementResponse, + Measurement, throwArgError, + generalHelpTexts, } from '@globalping/bot-utils'; import { ChatInputCommandInteraction, codeBlock } from 'discord.js'; import * as dotenv from 'dotenv'; @@ -78,7 +83,7 @@ export const expandFlags = (flags: Flags): string => { }; export const expandResults = async ( - response: MeasurementResponse, + response: Measurement, interaction: ChatInputCommandInteraction, ) => { const { results } = response; @@ -111,27 +116,27 @@ export const expandResults = async ( export const helpCmd = (cmd: string): string => { if (cmd === 'help') { - return `${help.help.preamble}\n\n**Usage:**\n\`\`\`${help.help.usage}\`\`\`\n**Arguments**:\n\`\`\`${help.help.args}\`\`\`\nMore help can be found here:\n\`\`\`${help.help.endDiscord}\`\`\``; + return `${generalHelpTexts.preamble}\n\n**Usage:**\n\`\`\`${generalHelpTexts.usage}\`\`\``; } if (cmd === 'ping') { - return `${help.ping.preamble}\n\n**Usage:**\n\`\`\`${help.ping.usage}\`\`\`\n**Options:**\n\`\`\`${help.ping.options}\`\`\`\n\n**Examples:**\n\`\`\`${help.ping.examples}\`\`\``; + return `${pingHelpTexts.preamble}\n\n**Usage:**\n\`\`\`${pingHelpTexts.usage}\`\`\`\n\n**Examples:**\n\`\`\`${pingHelpTexts.examples}\`\`\``; } if (cmd === 'traceroute') { - return `${help.traceroute.preamble}\n\n**Usage:**\n\`\`\`${help.traceroute.usage}\`\`\`\n**Options:**\n\`\`\`${help.traceroute.options}\`\`\`\n**Examples:**\n\`\`\`${help.traceroute.examples}\`\`\``; + return `${tracerouteHelpTexts.preamble}\n\n**Usage:**\n\`\`\`${tracerouteHelpTexts.usage}\`\`\`\n**Examples:**\n\`\`\`${tracerouteHelpTexts.examples}\`\`\``; } if (cmd === 'dns') { - return `${help.dns.preamble}\n\n**Usage:**\n\`\`\`${help.dns.usage}\`\`\`\n**Options:**\n\`\`\`${help.dns.options}\`\`\`\n\n**Examples:**\n\`\`\`${help.dns.examples}\`\`\``; + return `${dnsHelpTexts.preamble}\n\n**Usage:**\n\`\`\`${dnsHelpTexts.usage}\`\`\`\n\n**Examples:**\n\`\`\`${dnsHelpTexts.examples}\`\`\``; } if (cmd === 'mtr') { - return `${help.mtr.preamble}\n\n**Usage:**\n\`\`\`${help.mtr.usage}\`\`\`\n**Options:**\n\`\`\`${help.mtr.options}\`\`\`\n\n**Examples:**\n\`\`\`${help.mtr.examples}\`\`\``; + return `${mtrHelpTexts.preamble}\n\n**Usage:**\n\`\`\`${mtrHelpTexts.usage}\`\`\`\n\n**Examples:**\n\`\`\`${mtrHelpTexts.examples}\`\`\``; } if (cmd === 'http') { - return `${help.http.preamble}\n\n**Usage:**\n\`\`\`${help.http.usage}\`\`\`\n**Options:**\n\`\`\`${help.http.options}\`\`\`\n**Examples:**\n\`\`\`${help.http.examples}\`\`\``; + return `${httpHelpTexts.preamble}\n\n**Usage:**\n\`\`\`${httpHelpTexts.usage}\`\`\`\n**Examples:**\n\`\`\`${httpHelpTexts.examples}\`\`\``; } return 'Unknown command! Please call `/globalping help` for a list of commands.'; diff --git a/packages/slack/src/bot.ts b/packages/slack/src/bot.ts index 31cef5a..3fbc4ad 100644 --- a/packages/slack/src/bot.ts +++ b/packages/slack/src/bot.ts @@ -1,5 +1,15 @@ -import { AllMiddlewareArgs, SlackCommandMiddlewareArgs, SlackEventMiddlewareArgs } from '@slack/bolt'; -import { formatSeconds, getInstallationId, helpCmd, Logger, pluralize } from './utils.js'; +import { + AllMiddlewareArgs, + SlackCommandMiddlewareArgs, + SlackEventMiddlewareArgs, +} from '@slack/bolt'; +import { + formatSeconds, + getInstallationId, + helpCmd, + Logger, + pluralize, +} from './utils.js'; import { StringIndexed } from '@slack/bolt/dist/types/helpers.js'; import { formatAPIError, @@ -8,13 +18,25 @@ import { AuthSubcommand, buildPostMeasurements, PostError, - PostMeasurementResponse, Flags, - MeasurementResponse, - PostMeasurement, + MeasurementCreate, + Measurement, + MeasurementCreateResponse, } from '@globalping/bot-utils'; -import type { Block, GenericMessageEvent, KnownBlock, RichTextBlockElement, WebClient } from '@slack/web-api'; -import { AuthorizeErrorType, CreateLimitType, IntrospectionResponse, LimitsResponse, OAuthClient } from './auth.js'; +import type { + Block, + GenericMessageEvent, + KnownBlock, + RichTextBlockElement, + WebClient, +} from '@slack/web-api'; +import { + AuthorizeErrorType, + CreateLimitType, + IntrospectionResponse, + LimitsResponse, + OAuthClient, +} from './auth.js'; import { formatMeasurementResponse } from './response.js'; interface ChannelPayload { @@ -28,12 +50,19 @@ export class Bot { constructor ( private logger: Logger, private oauth: OAuthClient, - private postMeasurement: (opts: PostMeasurement, token?: string) => Promise, - private getMeasurement: (id: string) => Promise, - ) { } + private postMeasurement: ( + opts: MeasurementCreate, + token?: string, + ) => Promise, + private getMeasurement: (id: string) => Promise, + ) {} async HandleCommand ({ - ack, respond, client, payload, context, + ack, + respond, + client, + payload, + context, }: SlackCommandMiddlewareArgs & AllMiddlewareArgs) { const logData = { command: payload.command, @@ -100,7 +129,10 @@ export class Bot { ); await respond({ - text: 'Unable to run `' + payload.command + '` in a private DM! You can DM the Globalping App directly to run commands, or create a new group DM with the Globalping App to include multiple users.', + text: + 'Unable to run `' + + payload.command + + '` in a private DM! You can DM the Globalping App directly to run commands, or create a new group DM with the Globalping App to include multiple users.', }); } else { throw new Error('Unable to open a DM with the Globalping App.'); @@ -116,7 +148,10 @@ export class Bot { ); await respond({ - text: 'Unable to run `' + payload.command + '` in a private DM! You can DM the Globalping App directly to run commands, or create a new group DM with the Globalping App to include multiple users.', + text: + 'Unable to run `' + + payload.command + + '` in a private DM! You can DM the Globalping App directly to run commands, or create a new group DM with the Globalping App to include multiple users.', }); return; @@ -129,6 +164,7 @@ export class Bot { ); await respond('Please invite me to this channel to use this command. Run `/invite @Globalping` to invite me.'); + return; } @@ -160,7 +196,12 @@ export class Bot { } } - async HandleMention ({ event, context, client }: SlackEventMiddlewareArgs<'app_mention'> & AllMiddlewareArgs) { + async HandleMention ({ + event, + context, + client, + }: SlackEventMiddlewareArgs<'app_mention'> & + AllMiddlewareArgs) { await this.processMention( getRawTextFromBlocks(context.botUserId || '', event.blocks), event.team, @@ -173,7 +214,11 @@ export class Bot { ); } - async HandleMessage ({ event, context, client }: SlackEventMiddlewareArgs<'message'> & AllMiddlewareArgs) { + async HandleMessage ({ + event, + context, + client, + }: SlackEventMiddlewareArgs<'message'> & AllMiddlewareArgs) { if (event.channel_type !== 'im') { return; } @@ -268,11 +313,7 @@ export class Bot { await this.createMeasurement(client, payload, cmdText, flags); } - private async help ( - client: WebClient, - payload: ChannelPayload, - flags: Flags, - ) { + private async help (client: WebClient, payload: ChannelPayload, flags: Flags) { await client.chat.postEphemeral({ text: helpCmd(flags.cmd, flags.target, 'slack'), user: payload.user_id, @@ -290,7 +331,7 @@ export class Bot { const opts = buildPostMeasurements(flags); this.logger.debug(`Posting measurement: ${JSON.stringify(opts)}`); - let measurementResponse: PostMeasurementResponse; + let measurementResponse: MeasurementCreateResponse; const token = await this.oauth.GetToken(payload.installationId); @@ -361,7 +402,12 @@ export class Bot { const res = await this.getMeasurement(measurementResponse.id); this.logger.debug(`Get response: ${JSON.stringify(res)}`); - const blocks = formatMeasurementResponse(payload.user_id, cmdText, res, flags); + const blocks = formatMeasurementResponse( + payload.user_id, + cmdText, + res, + flags, + ); await client.chat.postMessage({ blocks, @@ -521,7 +567,11 @@ export function getRawTextFromBlocks ( let text = ''; traverseBlocks(blocks, (block) => { - if (block.type === 'user' && 'user_id' in block && block.user_id === botUserId) { + if ( + block.type === 'user' + && 'user_id' in block + && block.user_id === botUserId + ) { // ignore text before the bot mention text = ''; } else if ('text' in block && typeof block.text === 'string') { @@ -534,7 +584,10 @@ export function getRawTextFromBlocks ( return text.trim(); } -function traverseBlocks (blocks: (KnownBlock | Block)[], callback: (block: KnownBlock | Block | RichTextBlockElement) => void) { +function traverseBlocks ( + blocks: (KnownBlock | Block)[], + callback: (block: KnownBlock | Block | RichTextBlockElement) => void, +) { for (const block of blocks) { if ('text' in block || block.type === 'user') { callback(block); @@ -546,7 +599,6 @@ function traverseBlocks (blocks: (KnownBlock | Block)[], callback: (block: Known } } - export function getLimitsOutput ( limits: LimitsResponse, introspection: IntrospectionResponse, diff --git a/packages/slack/src/github/processing.ts b/packages/slack/src/github/processing.ts index a91d9f3..b6910d4 100644 --- a/packages/slack/src/github/processing.ts +++ b/packages/slack/src/github/processing.ts @@ -6,9 +6,9 @@ import { getAPIErrorMessage, getMeasurement, getTag, - PingMeasurementResponse, + Measurement, + MeasurementCreateResponse, postMeasurement, - PostMeasurementResponse, } from '@globalping/bot-utils'; import { Octokit } from 'octokit'; @@ -58,13 +58,10 @@ export const processCommand = async ( const opts = buildPostMeasurements(flags); - let measurementResponse: PostMeasurementResponse; + let measurementResponse: MeasurementCreateResponse; try { - measurementResponse = await postMeasurement( - opts, - config.globalpingToken, - ); + measurementResponse = await postMeasurement(opts, config.globalpingToken); } catch (error) { const errorMsg = getAPIErrorMessage(error); logger.error( @@ -81,7 +78,7 @@ export const processCommand = async ( throw error; } - let res: PingMeasurementResponse; + let res: Measurement; try { res = await getMeasurement(measurementResponse.id); @@ -130,7 +127,7 @@ async function measurementsResponse ( githubClient: Octokit, githubTarget: GithubTarget, measurementId: string, - res: PingMeasurementResponse, + res: Measurement, flags: Flags, cmdText: string, ) { @@ -146,7 +143,8 @@ async function measurementsResponse ( /* eslint-disable no-await-in-loop */ for (const result of resultsForDisplay) { const tag = getTag(result.probe.tags); - const text = `${responseHeader(result, tag, githubBoldSeparator) + const text = `${ + responseHeader(result, tag, githubBoldSeparator) + responseText(result, flags, githubTruncationLimit) }\r\n`; fullText += text; diff --git a/packages/slack/src/response.ts b/packages/slack/src/response.ts index 12723d9..65694e2 100644 --- a/packages/slack/src/response.ts +++ b/packages/slack/src/response.ts @@ -1,18 +1,23 @@ import { + DnsProbeResult, Flags, getTag, - PingMeasurementResponse, - PingResult, + HttpProbeResult, + Measurement, + PingProbeResult, + ProbeMeasurement, } from '@globalping/bot-utils'; import { KnownBlock } from '@slack/web-api'; export function responseHeader ( - result: PingResult, + result: ProbeMeasurement, tag: string | undefined, boldSeparator: string, ): string { - return `>${boldSeparator}${result.probe.city}${result.probe.state ? ` (${result.probe.state})` : '' - }, ${result.probe.country}, ${result.probe.continent}, ${result.probe.network + return `>${boldSeparator}${result.probe.city}${ + result.probe.state ? ` (${result.probe.state})` : '' + }, ${result.probe.country}, ${result.probe.continent}, ${ + result.probe.network } (AS${result.probe.asn})${tag ? `, (${tag})` : ''}${boldSeparator}\n`; } @@ -44,13 +49,15 @@ export function fullResultsFooter ( } const reponseTextRegular = ( - result: PingResult, + result: ProbeMeasurement, flags: Flags, truncationLimit: number, ): string => { - const responseText = isBodyOnlyHttpGet(flags) - ? result.result.rawBody - : result.result.rawOutput; + let responseText = result.result.rawOutput; + + if (isBodyOnlyHttpGet(flags)) { + responseText = (result.result as HttpProbeResult).rawBody || ''; + } // Slack has a limit of characters per block - truncate if necessary const finalResponseText @@ -68,38 +75,39 @@ const formatResponseText = (text: string): string => `\`\`\` ${text} \`\`\``; -const latencyText = (result: PingResult, flags: Flags): string => { +const latencyText = (result: ProbeMeasurement, flags: Flags): string => { let text = ''; - switch (flags.cmd) { - case 'ping': - text += `Min: ${result.result.stats.min} ms\n`; - text += `Max: ${result.result.stats.max} ms\n`; - text += `Avg: ${result.result.stats.min} ms\n`; - break; - - case 'dns': - text += `Total: ${result.result.timings.total} ms\n`; - break; - - case 'http': - text += `Total: ${result.result.timings.total} ms\n`; - text += `Download: ${result.result.timings.download} ms\n`; - text += `First byte: ${result.result.timings.firstByte} ms\n`; - text += `DNS: ${result.result.timings.dns} ms\n`; - text += `TLS: ${result.result.timings.tls} ms\n`; - text += `TCP: ${result.result.timings.tcp} ms\n`; - break; - - default: - throw new Error(`unknown command: ${flags.cmd}`); + if (flags.cmd === 'ping') { + const stats = (result.result as PingProbeResult).stats; + text += `Min: ${stats.min} ms\n`; + text += `Max: ${stats.max} ms\n`; + text += `Avg: ${stats.min} ms\n`; + return text; + } + + if (flags.cmd === 'dns') { + const timings = (result.result as DnsProbeResult).timings; + text += `Total: ${timings.total} ms\n`; + return text; + } + + if (flags.cmd === 'http') { + const timings = (result.result as HttpProbeResult).timings; + text += `Total: ${timings.total} ms\n`; + text += `Download: ${timings.download} ms\n`; + text += `First byte: ${timings.firstByte} ms\n`; + text += `DNS: ${timings.dns} ms\n`; + text += `TLS: ${timings.tls} ms\n`; + text += `TCP: ${timings.tcp} ms\n`; + return text; } - return text; + throw new Error(`unknown command: ${flags.cmd}`); }; export const responseText = ( - result: PingResult, + result: ProbeMeasurement, flags: Flags, truncationLimit: number, ): string => { @@ -115,7 +123,7 @@ const maxDisplayedResults = 4; export const formatMeasurementResponse = ( userId: string, cmdText: string, - res: PingMeasurementResponse, + res: Measurement, flags: Flags, ) => { const blocks: KnownBlock[] = [ diff --git a/packages/slack/src/tests/response.test.ts b/packages/slack/src/tests/response.test.ts index a66bd1c..44d728c 100644 --- a/packages/slack/src/tests/response.test.ts +++ b/packages/slack/src/tests/response.test.ts @@ -1,4 +1,4 @@ -import { PingResult } from '@globalping/bot-utils'; +import { ProbeMeasurement } from '@globalping/bot-utils'; import { describe, expect, it } from 'vitest'; import { @@ -28,9 +28,7 @@ describe('Response', () => { describe('responseHeader', () => { it('no state no tag', () => { - const pingResult: PingResult = { - resolvedAddress: '', - resolvedHostname: '', + const pingResult: ProbeMeasurement = { probe: { continent: 'EU', region: '', @@ -45,16 +43,8 @@ describe('Response', () => { tags: [], }, result: { + status: 'in-progress', rawOutput: '', - rawHeaders: '', - rawBody: '', - stats: { - loss: 0, - min: 0, - avg: 0, - max: 0, - }, - timings: undefined, }, }; const tag = undefined; @@ -65,9 +55,7 @@ describe('Response', () => { describe('responseHeader', () => { it('state and tags', () => { - const pingResult: PingResult = { - resolvedAddress: '', - resolvedHostname: '', + const pingResult: ProbeMeasurement = { probe: { continent: 'NA', region: '', @@ -82,16 +70,8 @@ describe('Response', () => { tags: [ 'tag-1', 'tag-2' ], }, result: { + status: 'in-progress', rawOutput: '', - rawHeaders: '', - rawBody: '', - stats: { - loss: 0, - min: 0, - avg: 0, - max: 0, - }, - timings: undefined, }, }; const text = responseHeader(pingResult, 'tag-1', boldSeparator); diff --git a/packages/slack/src/tests/utils.ts b/packages/slack/src/tests/utils.ts index eb88232..98aef48 100644 --- a/packages/slack/src/tests/utils.ts +++ b/packages/slack/src/tests/utils.ts @@ -4,13 +4,13 @@ import { InstallationStore } from '../db.js'; import { Logger, SlackClient } from '../utils.js'; import { OAuthClient } from '../auth.js'; import probeData from '../../../bot-utils/tests/mocks/probedata.json'; -import { MeasurementResponse } from '@globalping/bot-utils'; +import { Measurement } from '@globalping/bot-utils'; export const mockLogger = (): Logger => ({ info: vi.fn(), error: vi.fn(), debug: vi.fn(), -} as any); +}) as any; export const mockSlackClient = (): SlackClient => ({ chat: { @@ -20,14 +20,14 @@ export const mockSlackClient = (): SlackClient => ({ conversations: { info: vi.fn(), }, -} as any); +}) as any; export const mockInstallationStore = (): InstallationStore => ({ getToken: vi.fn(), updateToken: vi.fn(), updateAuthorizeSession: vi.fn(), getInstallationForAuthorization: vi.fn(), -} as any); +}) as any; export const mockPostMeasurement = () => vi.fn(); export const mockGetMeasurement = () => vi.fn(); @@ -39,24 +39,24 @@ export const mockAuthClient = (): OAuthClient => ({ Limits: vi.fn(), GetToken: vi.fn(), TryToRefreshToken: vi.fn(), -} as any); +}) as any; -export const getDefaultDnsResponse = (): MeasurementResponse => { +export const getDefaultDnsResponse = (): Measurement => { return probeData.dns1 as any; }; -export const getDefaultHttpResponse = (): MeasurementResponse => { +export const getDefaultHttpResponse = (): Measurement => { return probeData.http1 as any; }; -export const getDefaultMtrResponse = (): MeasurementResponse => { +export const getDefaultMtrResponse = (): Measurement => { return probeData.mtr1 as any; }; -export const getDefaultPingResponse = (): MeasurementResponse => { +export const getDefaultPingResponse = (): Measurement => { return probeData.ping1 as any; }; -export const getDefaultTracerouteResponse = (): MeasurementResponse => { +export const getDefaultTracerouteResponse = (): Measurement => { return probeData.traceroute1 as any; };