Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions bots/bugbuster/bot.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,6 @@ import telegram from './bp_modules/telegram'

export default new sdk.BotDefinition({
states: {
recentlyLinted: {
type: 'bot',
schema: sdk.z.object({
issues: sdk.z
.array(
sdk.z.object({
id: sdk.z.string(),
lintedAt: sdk.z.string().datetime(),
})
)
.title('Recently Linted Issues')
.describe('List of recently linted issues'),
}),
},
watchedTeams: {
type: 'bot',
schema: sdk.z.object({
Expand Down
2 changes: 1 addition & 1 deletion bots/bugbuster/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const bootstrap = (props: types.CommonHandlerProps) => {

const linear = utils.linear.LinearApi.create()
const teamsManager = new TeamsManager(linear, client, ctx.botId)
const recentlyLintedManager = new RecentlyLintedManager(client, ctx.botId)
const recentlyLintedManager = new RecentlyLintedManager(linear)
const issueProcessor = new IssueProcessor(logger, linear, teamsManager)

return {
Expand Down
9 changes: 2 additions & 7 deletions bots/bugbuster/src/handlers/linear-issue-updated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,9 @@ export const handleLinearIssueUpdated: bp.EventHandlers['linear:issueUpdated'] =
return
}

const recentlyLinted = await recentlyLintedManager
.getRecentlyLinted()
const isRecentlyLinted = await recentlyLintedManager
.isRecentlyLinted(issue)
.catch(_handleError('trying to get recently linted issues'))

const isRecentlyLinted = recentlyLinted.some(({ id: issueId }) => issue.id === issueId)

await issueProcessor.lintIssue(issue, isRecentlyLinted).catch(_handleError('trying to lint the updated Linear issue'))
await recentlyLintedManager
.setRecentlyLinted([...recentlyLinted, { id: issue.id, lintedAt: new Date().toISOString() }])
.catch(_handleError('trying to update recently linted issues'))
}
2 changes: 1 addition & 1 deletion bots/bugbuster/src/services/issue-processor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class IssueProcessor {
return { identifier: issue.identifier, result: 'ignored' }
}

const errors = await lintIssue(issue, status)
const errors = lintIssue(issue, status)

if (errors.length === 0) {
this._logger.info(`Issue ${issue.identifier} passed all lint checks.`)
Expand Down
4 changes: 2 additions & 2 deletions bots/bugbuster/src/services/issue-processor/lint-issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type IssueLint = {
message: string
}

export const lintIssue = async (issue: lin.Issue, status: lin.StateKey): Promise<IssueLint[]> => {
export const lintIssue = (issue: lin.Issue, status: lin.StateKey): IssueLint[] => {
const lints: string[] = []

if (!_hasLabelOfCategory(issue, 'type')) {
Expand Down Expand Up @@ -57,7 +57,7 @@ export const lintIssue = async (issue: lin.Issue, status: lin.StateKey): Promise
)
}

const issueProject = await issue.project
const issueProject = issue.project
if (issueProject && issueProject.completedAt) {
lints.push(
`Issue ${issue.identifier} is associated with a completed project (${issueProject.name}). Consider removing the project association.`
Expand Down
47 changes: 13 additions & 34 deletions bots/bugbuster/src/services/recently-linted-manager.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,21 @@
import * as bp from '.botpress'
import * as lin from '../utils/linear-utils'

const RECENT_THRESHOLD: number = 1000 * 60 * 10 // 10 minutes
type IssueLintEntry = bp.states.recentlyLinted.RecentlyLinted['payload']['issues'][number]

export class RecentlyLintedManager {
public constructor(
private _client: bp.Client,
private _botId: string
) {}
public constructor(private _linear: lin.LinearApi) {}

public async getRecentlyLinted(): Promise<bp.states.recentlyLinted.RecentlyLinted['payload']['issues']> {
const {
state: {
payload: { issues },
},
} = await this._client.getOrSetState({
id: this._botId,
type: 'bot',
name: 'recentlyLinted',
payload: { issues: [] },
})
return issues.filter(this._isRecentlyLinted)
}

public async setRecentlyLinted(issues: bp.states.recentlyLinted.RecentlyLinted['payload']['issues']): Promise<void> {
await this._client.setState({
id: this._botId,
type: 'bot',
name: 'recentlyLinted',
payload: {
issues: issues.filter(this._isRecentlyLinted),
},
})
}

private _isRecentlyLinted = (issue: IssueLintEntry): boolean => {
const lintedAt = new Date(issue.lintedAt).getTime()
public async isRecentlyLinted(issue: lin.Issue): Promise<boolean> {
const me = await this._linear.getMe()
const timestamps = issue.comments.nodes
.filter((comment) => comment.user.id === me.id)
.map((comment) => new Date(comment.createdAt).getTime())
const now = new Date().getTime()
return now - lintedAt < RECENT_THRESHOLD
for (const timestamp of timestamps) {
if (now - timestamp < RECENT_THRESHOLD) {
return true
}
}
return false
}
}
3 changes: 3 additions & 0 deletions bots/bugbuster/src/utils/linear-utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ export class LinearApi {
}

public async getMe(): Promise<lin.User> {
if (this._viewer) {
return this._viewer
}
const me = await this._client.viewer
if (!me) {
throw new Error('Viewer not found. Please ensure you are authenticated.')
Expand Down
5 changes: 4 additions & 1 deletion bots/bugbuster/src/utils/linear-utils/graphql-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type Issue = {
nodes: {
id: string
resolvedAt: string | null
createdAt: string
user: {
id: string
}
Expand Down Expand Up @@ -95,7 +96,9 @@ export const GRAPHQL_QUERIES = {
user {
id
},
parentId
parentId,
resolvedAt,
createdAt
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion integrations/teams/integration.definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { actions, channels, user, states } from 'definitions'

export default new IntegrationDefinition({
name: 'teams',
version: '2.0.0',
version: '2.0.1',
title: 'Microsoft Teams',
description: 'Interact with users, deliver notifications, and perform actions within Microsoft Teams.',
icon: 'icon.svg',
Expand Down
2 changes: 1 addition & 1 deletion integrations/teams/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"@azure/identity": "^4.10.2",
"@botpress/client": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*",
"@botpress/sdk-addons": "workspace:*",
"@microsoft/microsoft-graph-client": "^3.0.7",
Expand All @@ -26,7 +27,6 @@
},
"devDependencies": {
"@botpress/cli": "workspace:*",
"@botpress/common": "workspace:*",
"@botpress/sdk": "workspace:*",
"@sentry/cli": "^2.39.1",
"@types/jsonwebtoken": "^9.0.3",
Expand Down
32 changes: 25 additions & 7 deletions integrations/teams/src/channels/outbound.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,29 @@ const _makeCard = (card: BotpressCard): Attachment => {
return CardFactory.heroCard(title, images, buttons, { subtitle })
}

const _makeDropdownCard = (text: string, options: DropdownOption[]): Attachment => {
const choices = options.map((option: DropdownOption) => ({
title: option.label,
value: option.value,
}))
const _distinctByValue = (choice: { value: string }, index: number, arr: { value: string }[]): boolean => {
return arr.findIndex((otherChoice) => otherChoice.value === choice.value) === index
}

const _makeDropdownCard = (text: string, options: DropdownOption[], logger: bp.Logger): Attachment => {
const uniqueChoices = options
.map((option: DropdownOption) => ({
title: option.label,
value: option.value,
}))
// Hotfix: This exists because some client's code was
// duplicating the options everytime the workflow was ran
.filter(_distinctByValue)

if (uniqueChoices.length < options.length) {
// Normally, it's not the responsibility of the integration
// to warn the user of issues in their workflow.
logger
.forBot()
.warn(
`The dropdown options contained duplicates (This is likely due to a misconfiguration in the bot workflow).\nReduced from ${options.length} to ${uniqueChoices.length} unique options.`
)
}

return CardFactory.adaptiveCard({
// documentation here https://learn.microsoft.com/en-us/adaptive-cards/authoring-cards/text-features
Expand All @@ -108,7 +126,7 @@ const _makeDropdownCard = (text: string, options: DropdownOption[]): Attachment
type: 'Input.ChoiceSet',
placeholder: 'Select...',
style: 'compact',
choices,
choices: uniqueChoices,
},
],
actions: [
Expand Down Expand Up @@ -202,7 +220,7 @@ const _handleDropdownMessage = async (props: MessageHandlerProps<'dropdown'>) =>
const { options, text } = props.payload
const activity: Partial<Activity> = {
type: 'message',
attachments: [_makeDropdownCard(text, options)],
attachments: [_makeDropdownCard(text, options, props.logger)],
}
await _renderTeams(props, activity)
}
Expand Down
84 changes: 17 additions & 67 deletions integrations/teams/src/markdown/markdown-to-teams-xml.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { transformMarkdown, type MarkdownHandlers } from '@botpress/common'
import type { Parent } from 'mdast'
import { remark } from 'remark'
import remarkGfm from 'remark-gfm'
import { escapeAndSanitizeHtml, isNaughtyUrl, sanitizeHtml } from './sanitize-utils'
import type {
DefinedLinkReference,
DefinitionNodeData,
MarkdownHandlers,
NodeHandler,
TableCellWithHeaderInfo,
} from './types'

type DefinitionNodeData = {
identifier: string
url: string
label?: string | null
title?: string | null
}

export const defaultHandlers: MarkdownHandlers = {
blockquote: (node, visit) => `<blockquote>\n\n${visit(node)}\n</blockquote>`,
Expand Down Expand Up @@ -38,8 +37,9 @@ export const defaultHandlers: MarkdownHandlers = {
image: (node) => _createSanitizedImage(node),
inlineCode: (node) => `<code>${node.value}</code>`,
link: (node, visit) => _createSanitizedHyperlink(node, visit(node)),
linkReference: (node, visit) => {
const { linkDefinition } = node
linkReference: (node, visit, _parents, _handlers, data) => {
const linkDefinitions = (data.linkDefinitions ?? {}) as Record<string, DefinitionNodeData>
const linkDefinition = linkDefinitions[node.identifier]
const nodeContent = visit(node)
if (!linkDefinition) {
return nodeContent
Expand All @@ -61,14 +61,7 @@ export const defaultHandlers: MarkdownHandlers = {
},
paragraph: (node, visit, parents) => `${visit(node)}${parents.at(-1)?.type === 'root' ? '\n' : ''}`,
strong: (node, visit) => `<strong>${visit(node)}</strong>`,
table: (node, visit) => {
const headerRow = node.children[0]
headerRow?.children.forEach((cell: TableCellWithHeaderInfo) => {
cell.isHeader = true
})

return `<table>\n${visit(node)}</table>`
},
table: (node, visit) => `<table>\n${visit(node)}</table>`,
tableRow: (node, visit) => `<tr>\n${visit(node)}</tr>\n`,
tableCell: (node, visit) => {
const tag = node.isHeader === true ? 'th' : 'td'
Expand Down Expand Up @@ -109,8 +102,6 @@ const _createSanitizedImage = (props: ImageProps): string => {
return `<img src="${url}"${optionalAttrs} />`
}

const _isNodeType = (s: string, handlers: MarkdownHandlers): s is keyof MarkdownHandlers => s in handlers

const _extractDefinitions = (parentNode: Parent): Record<string, DefinitionNodeData> => {
let definitions: Record<string, DefinitionNodeData> = {}
for (const node of parentNode.children) {
Expand All @@ -133,52 +124,11 @@ const _extractDefinitions = (parentNode: Parent): Record<string, DefinitionNodeD
return definitions
}

const _visitTree = (
tree: Parent,
handlers: MarkdownHandlers,
parents: Parent[],
definitions: Record<string, DefinitionNodeData>
): string => {
let tmp = ''
let footnoteTmp = ''
parents.push(tree)

for (const node of tree.children) {
if (!_isNodeType(node.type, handlers)) {
throw new Error(`The Markdown node type [${node.type}] is not supported`)
}

const handler = handlers[node.type] as NodeHandler

if (node.type === 'linkReference') {
const linkReferenceNode = node as DefinedLinkReference
const def = definitions[node.identifier]
linkReferenceNode.linkDefinition = def

tmp += handler(
linkReferenceNode,
(n) => _visitTree(n, handlers, parents, definitions),
parents,
handlers,
definitions
)
continue
}

if (node.type === 'footnoteDefinition') {
footnoteTmp += handler(node, (n) => _visitTree(n, handlers, parents, definitions), parents, handlers, definitions)
continue
}
tmp += handler(node, (n) => _visitTree(n, handlers, parents, definitions), parents, handlers, definitions)
}
parents.pop()
return `${tmp}${footnoteTmp}`
}

export const transformMarkdownToTeamsXml = (markdown: string, handlers: MarkdownHandlers = defaultHandlers): string => {
const tree = remark().use(remarkGfm).parse(markdown)
const definitions = _extractDefinitions(tree)
let html = _visitTree(tree, handlers, [], definitions).trim()
export const transformMarkdownToTeamsXml = (markdown: string): string => {
let html = transformMarkdown(markdown, defaultHandlers, (root) => {
const linkDefinitions = _extractDefinitions(root)
return { linkDefinitions }
}).trim()
_replacers.forEach((replacer) => {
html = replacer(html)
})
Expand Down
38 changes: 0 additions & 38 deletions integrations/teams/src/markdown/types.ts

This file was deleted.

Loading
Loading