import { MessageSenderClient } from "./MatrixSender" ; import { IBridgeStorageProvider } from "./Stores/StorageProvider" ; import { UserNotificationsEvent } from "./Notifications/UserNotificationWatcher" ; import LogWrapper from "./LogWrapper" ; import { AdminRoom } from "./AdminRoom" ; import markdown from "markdown-it" ; import { FormatUtil } from "./FormatUtil" ; import { PullGetResponseData , IssuesGetResponseData , PullsListRequestedReviewersResponseData , PullsListReviewsResponseData , IssuesGetCommentResponseData } from "./Github/Types" ; import { GitHubUserNotification } from "./Github/Types" ; import { components } from "@octokit/openapi-types/types" ; import { NotifFilter } from "./NotificationFilters" ; const log = new LogWrapper ( "NotificationProcessor" ) ; const md = new markdown ( ) ; export interface IssueDiff { state : null | string ; assignee : null | ( components [ "schemas" ] [ "nullable-simple-user" ] [ ] ) ; title : null | string ; merged : boolean ; mergedBy : null | { login : string ; html_url : string ; } ; user : { login : string ; html_url : string ; } ; } export interface CachedReviewData { requested_reviewers : PullsListRequestedReviewersResponseData ; reviews : PullsListReviewsResponseData ; } type PROrIssue = IssuesGetResponseData | PullGetResponseData ; export class NotificationProcessor { private static formatUser ( user ) { return `**[${ user . login }](${ user . html_url })**` ; } private static formatNotification ( notif , diff , newComment ) { const user = diff ? ` by ${ this . formatUser ( diff ?. user ) }` : "" ; let plain = `${ this . getEmojiForNotifType ( notif ) } [${ notif . subject . title }](${ notif . subject . url_data ?. html_url })${ user }` ; const issueNumber = notif . subject . url_data ?. number ; if ( issueNumber ) { plain += ` #${ issueNumber }` ; } if ( notif . repository ) { plain += ` for **[${ notif . repository . full_name }](${ notif . repository . html_url })**` ; } if ( diff ) { plain += "\n\n " ; if ( diff . merged ) { plain += `\n\n PR was merged by ${ diff . mergedBy ? NotificationProcessor . formatUser ( diff . mergedBy ) : "" }` ; } else if ( diff . state ) { const state = diff . state [ 0 ] . toUpperCase ( ) + diff . state . slice ( 1 ) . toLowerCase ( ) ; plain += `\n\n State changed to: ${ state }` ; } if ( diff . title ) { plain += `\n\n Title changed to: ${ diff . title }` ; } if ( diff . assignee ) { plain += `\n\n Assigned to: ${ diff . assignee . map ( l => l ?. login ) . join ( ", " ) }` ; } } if ( newComment ) { const comment = notif . subject . latest_comment_url_data as IssuesGetCommentResponseData ; const user = comment . user ? NotificationProcessor . formatUser ( comment . user ) : 'user' ; if ( comment . body ) { plain += `\n\n ${ user }:\n\n > ${ comment . body }` ; } else { plain += `\n\n ${ user }:\n\n posted with no body` ; } } return { plain , html : md . render ( plain ) , } ; } private static getEmojiForNotifType ( notif ) { let reasonFlag = "" ; switch ( notif . reason ) { case "review_requested" : reasonFlag = "🚩" ; break ; } switch ( notif . subject . type ) { case "Issue" : return "📝" ; case "PullRequest" : return `⤵+${ reasonFlag }` ; case "RepositoryVulnerabilityAlert" : return "⚠️" ; default : return "🔔" ; } } constructor ( private storage , private matrixSender ) { } public async onUserEvents ( msg , adminRoom ) { log . info ( `Got new events for ${ adminRoom . userId } ${ msg . events . length }` ) ; for ( const event of msg . events ) { const isIssueOrPR = event . subject . type === "Issue" || event . subject . type === "PullRequest" ; try { await this . handleUserNotification ( msg . roomId , event , adminRoom . notifFilter ) ; if ( isIssueOrPR && event . subject . url_data ) { const issueNumber = event . subject . url_data . number . toString ( ) ; await this . storage . setGithubIssue ( event . repository . full_name , issueNumber , event . subject . url_data , msg . roomId ) ; if ( event . subject . latest_comment_url ) { await this . storage . setLastNotifCommentUrl ( event . repository . full_name , issueNumber , event . subject . latest_comment_url , msg . roomId ) ; } if ( event . subject . requested_reviewers && event . subject . reviews ) { await this . storage . setPRReviewData ( event . repository . full_name , issueNumber , event . subject as CachedReviewData , msg . roomId ) ; } } } catch ( ex ) { log . warn ( "Failed to handle event:" , ex ) ; } } try { await adminRoom . setNotifSince ( "github" , msg . lastReadTs ) ; } catch ( ex ) { log . error ( "Failed to update stream position for notifications:" , ex ) ; } } private formatSecurityAlert ( notif ) { const body = `⚠️ ${ notif . subject . title } - ` + `for **[${ notif . repository . full_name }](${ notif . repository . html_url })**` ; return { ... FormatUtil . getPartialBodyForGithubRepo ( notif . repository ) , msgtype : "m.text" , body , formatted_body : md . render ( body ) , format : "org.matrix.custom.html" , } ; } private diffIssueChanges ( curr , prev ) { let merged = false ; let mergedBy = null ; if ( ( curr as PullGetResponseData ) . merged !== ( prev as PullGetResponseData ) . merged ) { merged = true ; mergedBy = ( curr as PullGetResponseData ) . merged_by ; } if ( ! curr . user ) { throw Error ( 'No user for issue' ) ; } const diff = { state : curr . state === prev . state ? null : curr . state , merged , mergedBy , assignee : curr . assignee ?. id === prev . assignee ?. id ? null : [ curr . assignee ] , title : curr . title === prev . title ? null : curr . title , user : curr . user , } ; return diff ; } private async formatIssueOrPullRequest ( roomId , notif ) { const issueNumber = notif . subject . url_data ?. number . toString ( ) ; let diff = null ; if ( issueNumber ) { const prevIssue = await this . storage . getGithubIssue ( notif . repository . full_name , issueNumber , roomId ) ; if ( prevIssue && notif . subject . url_data ) { diff = this . diffIssueChanges ( notif . subject . url_data , prevIssue ) ; } } const newComment = ! ! notif . subject . latest_comment_url && ! ! issueNumber && notif . subject . latest_comment_url !== ( await this . storage . getLastNotifCommentUrl ( notif . repository . full_name , issueNumber , roomId ) ) ; const formatted = NotificationProcessor . formatNotification ( notif , diff , newComment ) ; let body = { msgtype : "m.text" , body : formatted . plain , formatted_body : formatted . html , format : "org.matrix.custom.html" , } ; if ( newComment && notif . subject . latest_comment_url_data && notif . repository ) { body = { ... body , ... FormatUtil . getPartialBodyForComment ( notif . subject . latest_comment_url_data , notif . repository , notif . subject . url_data ) , } ; } else if ( notif . subject . url_data && notif . repository ) { body = { ... body , ... FormatUtil . getPartialBodyForGithubIssue ( notif . repository , notif . subject . url_data ) , } ; } return this . matrixSender . sendMatrixMessage ( roomId , body ) ; } private async handleUserNotification ( roomId , notif , filter ) { log . debug ( "New notification event:" , notif ) ; if ( ! filter . shouldSendNotification ( notif . subject . latest_comment_url_data ?. user ?. login , notif . repository . full_name , notif . repository . owner ?. login ) ) { log . debug ( `Dropping notification because user is filtering it out` ) ; return ; } if ( notif . reason === "security_alert" ) { return this . matrixSender . sendMatrixMessage ( roomId , this . formatSecurityAlert ( notif ) ) ; } else if ( notif . subject . type === "Issue" || notif . subject . type === "PullRequest" ) { return this . formatIssueOrPullRequest ( roomId , notif ) ; } const genericNotif = NotificationProcessor . formatNotification ( notif , null , false ) ; return this . matrixSender . sendMatrixMessage ( roomId , { msgtype : "m.text" , body : genericNotif . plain , formatted_body : genericNotif . html , format : "org.matrix.custom.html" , } ) ; } }