import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { sign } from '../../common/jwt'; import sendgrid from '@sendgrid/mail'; import { getPrivateKey } from '../../common/jwt/jwt'; import { Context } from '../../common/log'; import { readFileSync } from 'node:fs'; import path from 'node:path'; import { PRIMARY_ADMIN_NAME, AUTHOR_NAME, CUSTOMER_NAME, DEALER_NAME, FILE_NAME, LICENSE_QUANTITY, PO_NUMBER, TOP_URL, USER_EMAIL, USER_NAME, TYPIST_NAME, VERIFY_LINK, } from '../../templates/constants'; @Injectable() export class SendGridService { private readonly logger = new Logger(SendGridService.name); private readonly emailConfirmLifetime: number; private readonly appDomain: string; private readonly mailFrom: string; private readonly templateEmailVerifyHtml: string; private readonly templateEmailVerifyText: string; private readonly templateU101Html: string; private readonly templateU101Text: string; private readonly templateU105Html: string; private readonly templateU105Text: string; private readonly templateU106Html: string; private readonly templateU106Text: string; private readonly templateU107Html: string; private readonly templateU107Text: string; private readonly templateU108Html: string; private readonly templateU108Text: string; private readonly templateU109Html: string; private readonly templateU109Text: string; private readonly templateU111Html: string; private readonly templateU111Text: string; private readonly templateU112Html: string; private readonly templateU112Text: string; // U-112のテンプレート差分(親アカウントがない場合) private readonly templateU112NoParentHtml: string; private readonly templateU112NoParentText: string; private readonly templateU114Html: string; private readonly templateU114Text: string; private readonly templateU117Html: string; private readonly templateU117Text: string; constructor(private readonly configService: ConfigService) { this.appDomain = this.configService.getOrThrow('APP_DOMAIN'); this.mailFrom = this.configService.getOrThrow('MAIL_FROM'); this.emailConfirmLifetime = this.configService.getOrThrow( 'EMAIL_CONFIRM_LIFETIME', ); const key = this.configService.getOrThrow('SENDGRID_API_KEY'); sendgrid.setApiKey(key); // メールテンプレートを読み込む { this.templateEmailVerifyHtml = readFileSync( path.resolve(__dirname, `../../templates/template_email_verify.html`), 'utf-8', ); this.templateEmailVerifyText = readFileSync( path.resolve(__dirname, `../../templates/template_email_verify.txt`), 'utf-8', ); this.templateU101Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_101.html`), 'utf-8', ); this.templateU101Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_101.txt`), 'utf-8', ); this.templateU105Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_105.html`), 'utf-8', ); this.templateU105Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_105.txt`), 'utf-8', ); this.templateU106Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_106.html`), 'utf-8', ); this.templateU106Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_106.txt`), 'utf-8', ); this.templateU107Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_107.html`), 'utf-8', ); this.templateU107Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_107.txt`), 'utf-8', ); this.templateU108Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_108.html`), 'utf-8', ); this.templateU108Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_108.txt`), 'utf-8', ); this.templateU109Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_109.html`), 'utf-8', ); this.templateU109Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_109.txt`), 'utf-8', ); this.templateU111Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_111.html`), 'utf-8', ); this.templateU111Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_111.txt`), 'utf-8', ); this.templateU112Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_112.html`), 'utf-8', ); this.templateU112Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_112.txt`), 'utf-8', ); this.templateU112NoParentHtml = readFileSync( path.resolve( __dirname, `../../templates/template_U_112_no_parent.html`, ), 'utf-8', ); this.templateU112NoParentText = readFileSync( path.resolve(__dirname, `../../templates/template_U_112_no_parent.txt`), 'utf-8', ); this.templateU114Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_114.html`), 'utf-8', ); this.templateU114Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_114.txt`), 'utf-8', ); this.templateU117Html = readFileSync( path.resolve(__dirname, `../../templates/template_U_117.html`), 'utf-8', ); this.templateU117Text = readFileSync( path.resolve(__dirname, `../../templates/template_U_117.txt`), 'utf-8', ); } } /** * Email認証用のメールコンテンツを作成する * @param accountId 認証対象のユーザーが所属するアカウントのID * @param userId 認証対象のユーザーのID * @param email 認証対象のユーザーのメールアドレス * @returns メールのサブジェクトとコンテンツ */ async createMailContentFromEmailConfirm( context: Context, accountId: number, userId: number, email: string, ): Promise<{ subject: string; text: string; html: string }> { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.createMailContentFromEmailConfirm.name } | params: { ` + `accountId: ${accountId},` + `userId: ${userId} };`, ); try { const privateKey = getPrivateKey(this.configService); const token = sign<{ accountId: number; userId: number; email: string }>( { accountId, userId, email, }, this.emailConfirmLifetime, privateKey, ); const path = 'mail-confirm/'; const html = this.templateEmailVerifyHtml .replace('VERIFY_LINK', `${this.appDomain}${path}?verify=${token}`) .replace( 'VERIFY_LINK_TEXT', `${this.appDomain}${path}?verify=${token}`, ); const text = this.templateEmailVerifyText .replace('VERIFY_LINK', `${this.appDomain}${path}?verify=${token}`) .replace( 'VERIFY_LINK_TEXT', `${this.appDomain}${path}?verify=${token}`, ); return { subject: 'Verify your new account', text: text, html: html, }; } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${ this.createMailContentFromEmailConfirm.name }`, ); } } /** * Email認証用のメールコンテンツを作成する(一般ユーザ向け) * @param accountId 認証対象のユーザーが所属するアカウントのID * @param userId 認証対象のユーザーのID * @param email 認証対象のユーザーのメールアドレス * @returns メールのサブジェクトとコンテンツ */ //TODO [Task2163] 中身が管理ユーザ向けのままなので、修正の必要あり async createMailContentFromEmailConfirmForNormalUser( context: Context, accountId: number, userId: number, email: string, ): Promise<{ subject: string; text: string; html: string }> { this.logger.log( `[IN] [${context.getTrackingId()}] ${ this.createMailContentFromEmailConfirmForNormalUser.name } | params: { ` + `accountId: ${accountId},` + `userId: ${userId} };`, ); const privateKey = getPrivateKey(this.configService); const token = sign<{ accountId: number; userId: number; email: string }>( { accountId, userId, email, }, this.emailConfirmLifetime, privateKey, ); const path = 'mail-confirm/user/'; return { subject: 'Verify your new account', text: `The verification URL. ${this.appDomain}${path}?verify=${token}`, html: `

The verification URL.

${this.appDomain}${path}?verify=${token}`, }; } /** * U-101のテンプレートを使用したメールを送信する * @param context * @param customerMail アカウント登録を行った管理者ユーザーのメールアドレス * @param customerAccountName アカウント登録した会社名 * @returns mail with u101 */ async sendMailWithU101( context: Context, customerMail: string, customerAccountName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU101.name}`, ); try { const subject = 'Account Registered Notification [U-101]'; const html = this.templateU101Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(TOP_URL, this.appDomain); const text = this.templateU101Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(TOP_URL, this.appDomain); await this.sendMail( context, [customerMail], [], this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU101.name}`, ); } } /** * U-105のテンプレートを使用したメールを送信する * @param context * @param customerMails 注文を行ったアカウントの管理者(primary/secondary)のメールアドレス * @param customerAccountName 送信対象の企業名 * @param lisenceCount 注文を行った対象の注文の内容(ライセンス数) * @param poNumber 注文を行った対象の注文の内容(PO番号) * @param dealerEmails 問題発生時に問い合わせする先の上位のディーラーの管理者(primary/secondary)のメールアドレス * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) * @returns mail with u105 */ async sendMailWithU105( context: Context, customerMails: string[], customerAccountName: string, lisenceCount: number, poNumber: string, dealerEmails: string[], dealerAccountName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU105.name}`, ); try { const subject = 'License Requested Notification [U-105]'; // メールの本文を作成する const html = this.templateU105Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PO_NUMBER, poNumber) .replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); const text = this.templateU105Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PO_NUMBER, poNumber) .replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); // メールを送信する this.sendMail( context, customerMails, dealerEmails, this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU105.name}`, ); } } /** * U-106のテンプレートを使用したメールを送信する * @param context * @param cancelUserEmailAddress 注文キャンセルを行ったアカウントの管理者(primary/secondary)のメールアドレス * @param customerAccountName 送信対象の企業名 * @param lisenceCount 注文キャンセルを行った対象の注文の内容(ライセンス数) * @param poNumber 注文キャンセルを行った対象の注文の内容(PO番号) * @param dealerEmails 問題発生時に問い合わせする先の上位のディーラーの管理者(primary/secondary)のメールアドレス * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) * @returns mail with u106 */ async sendMailWithU106( context: Context, customerMails: string[], customerAccountName: string, lisenceCount: number, poNumber: string, dealerEmails: string[], dealerAccountName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU106.name}`, ); try { const subject = 'Cancelled License Order Notification [U-106]'; // メールの本文を作成する const html = this.templateU106Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PO_NUMBER, poNumber) .replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); const text = this.templateU106Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PO_NUMBER, poNumber) .replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); // メールを送信する this.sendMail( context, customerMails, dealerEmails, this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU106.name}`, ); } } /** * U-107のテンプレートを使用したメールを送信する * @param context * @param cancelUserEmailAddress ライセンス発行をされたアカウントの管理者(primary/secondary)のメールアドレス * @param customerAccountName 送信対象の企業名 * @param lisenceCount ライセンス発行を行った対象の注文の内容(ライセンス数) * @param poNumber ライセンス発行を行った対象の注文の内容(PO番号) * @param dealerEmails 問題発生時に問い合わせする先の上位のディーラーの管理者(primary/secondary)のメールアドレス * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) * @returns mail with u107 */ async sendMailWithU107( context: Context, customerMails: string[], customerAccountName: string, lisenceCount: number, poNumber: string, dealerEmails: string[], dealerAccountName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU107.name}`, ); try { const subject = 'License Issued Notification [U-107]'; // メールの本文を作成する const html = this.templateU107Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PO_NUMBER, poNumber) .replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); const text = this.templateU107Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PO_NUMBER, poNumber) .replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); // メールを送信する this.sendMail( context, customerMails, dealerEmails, this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU107.name}`, ); } } /** * U-109のテンプレートを使用したメールを送信する * @param context context * @param dealerEmails ライセンス発行をキャンセルした上位アカウントの管理者(primary/secondary)のメールアドレス * @param dealerAccountName ライセンス発行をキャンセルした上位アカウントの会社名 * @param lisenceCount ライセンス発行をキャンセルした対象の注文の内容(ライセンス数) * @param poNumber ライセンス発行をキャンセルした対象の注文の内容(PO番号) * @param customerMails ライセンス発行をキャンセルされたアカウントの管理者(primary/secondary)のメールアドレス * @param customerAccountName ライセンス発行をキャンセルされたアカウントの会社名 * @returns */ async sendMailWithU109( context: Context, dealerEmails: string[], dealerAccountName: string, lisenceCount: number, poNumber: string, customerMails: string[], customerAccountName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU109.name}`, ); try { const subject = 'License Returned Notification [U-109]'; // メールの本文を作成する const html = this.templateU109Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PO_NUMBER, poNumber) .replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); const text = this.templateU109Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PO_NUMBER, poNumber) .replaceAll(LICENSE_QUANTITY, `${lisenceCount}`); // メールを送信する this.sendMail( context, dealerEmails, customerMails, this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU109.name}`, ); } } /** * U-108のテンプレートを使用したメールを送信する * @param context * @param userName ライセンス割り当てされたユーザーの名前 * @param userMail ライセンス割り当てされたユーザーのメールアドレス * @param customerAdminMails ライセンス割り当てされたユーザーの所属するアカウントの管理者(primary/secondary)のメールアドレス * @param customerAccountName ライセンス割り当てされたユーザーの所属するアカウントの名前 * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) * @returns mail with u108 */ async sendMailWithU108( context: Context, userName: string, userMail: string, customerAdminMails: string[], customerAccountName: string, dealerAccountName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU108.name}`, ); try { const subject = 'License Assigned Notification [U-108]'; // メールの本文を作成する const html = this.templateU108Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(USER_NAME, userName) .replaceAll(USER_EMAIL, userMail) .replaceAll(TOP_URL, this.appDomain); const text = this.templateU108Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(USER_NAME, userName) .replaceAll(USER_EMAIL, userMail) .replaceAll(TOP_URL, this.appDomain); // メールを送信する this.sendMail( context, [userMail], customerAdminMails, this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU108.name}`, ); } } /** * U-111のテンプレートを使用したメールを送信する * @param context * @param primaryAdminName 削除されたアカウントの管理者(primary)の名前 * @param primaryAdminMail 削除されたアカウントの管理者(primary)のメールアドレス * @param customerAccountName 削除されたアカウントの会社名 * @returns mail with u111 */ async sendMailWithU111( context: Context, primaryAdminName: string, primaryAdminMail: string, customerAccountName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU111.name}`, ); try { const subject = 'Account Deleted Notification [U-111]'; // メールの本文を作成する const html = this.templateU111Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(TOP_URL, this.appDomain); const text = this.templateU111Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(TOP_URL, this.appDomain); // メールを送信する this.sendMail( context, [primaryAdminMail], [], this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU111.name}`, ); } } /** * U-112のテンプレートを使用したメールを送信する * @param context * @param primaryAdminName 情報変更を行ったアカウントの管理者(primary)の名前 * @param primaryAdminMail 情報変更を行ったアカウントの管理者(primary)のメールアドレス * @param customerAccountName 情報変更を行ったアカウントの名前 * @param dealerAccountName 問題発生時に問い合わせする先の上位のディーラー名(会社名) * @returns mail with u112 */ async sendMailWithU112( context: Context, primaryAdminName: string, primaryAdminMail: string, customerAccountName: string, dealerAccountName: string | null, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU112.name}`, ); try { const subject = 'Account Edit Notification [U-112]'; let html: string; let text: string; // 親アカウントがない場合は別のテンプレートを使用する if (dealerAccountName === null) { // メールの本文を作成する html = this.templateU112NoParentHtml .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(TOP_URL, this.appDomain); text = this.templateU112NoParentText .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(TOP_URL, this.appDomain); } else { html = this.templateU112Html .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(TOP_URL, this.appDomain); text = this.templateU112Text .replaceAll(CUSTOMER_NAME, customerAccountName) .replaceAll(DEALER_NAME, dealerAccountName) .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(TOP_URL, this.appDomain); } // メールを送信する this.sendMail( context, [primaryAdminMail], [], this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU112.name}`, ); } } /** * U-114のテンプレートを使用したメールを送信する * @param context * @param accountId 認証対象のユーザーが所属するアカウントのID * @param userId 認証対象のユーザーのID * @param userMail 認証対象のユーザーのメールアドレス * @param primaryAdminName 認証対象のユーザーが所属するアカウントの管理者(primary)の名前 * @returns mail with u114 */ async sendMailWithU114( context: Context, accountId: number, userId: number, userMail: string, primaryAdminName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU114.name}`, ); try { // ユーザー認証用のトークンを作成する const privateKey = getPrivateKey(this.configService); const token = sign<{ accountId: number; userId: number; email: string }>( { accountId, userId, email: userMail, }, this.emailConfirmLifetime, privateKey, ); const path = 'mail-confirm/user/'; const verifyLink = `${this.appDomain}${path}?verify=${token}`; const subject = 'User Registration Notification [U-114]'; // メールの本文を作成する const html = this.templateU114Html .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(VERIFY_LINK, verifyLink); const text = this.templateU114Text .replaceAll(PRIMARY_ADMIN_NAME, primaryAdminName) .replaceAll(VERIFY_LINK, verifyLink); // メールを送信する this.sendMail( context, [userMail], [], this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU114.name}`, ); } } /** * U-117のテンプレートを使用したメールを送信する * @param context * @param authorEmail 文字起こしファイルのAuthorのメールアドレス * @param typistEmail 文字起こしを行ったTypistのメールアドレス * @param authorName 文字起こしファイルのAuthorの名前 * @param fileName 文字起こしファイルのファイル名 * @param typistName 文字起こしを行ったTypistの名前 * @param adminName アカウント管理者の名前(プライマリ) * @returns mail with u117 */ async sendMailWithU117( context: Context, authorEmail: string, typistEmail: string, authorName: string, fileName: string, typistName: string, adminName: string, ): Promise { this.logger.log( `[IN] [${context.getTrackingId()}] ${this.sendMailWithU117.name}`, ); try { const subject = 'Transcription Completion Notification [U-117]'; // メールの本文を作成する const html = this.templateU117Html .replaceAll(AUTHOR_NAME, authorName) .replaceAll(FILE_NAME, fileName) .replaceAll(TYPIST_NAME, typistName) .replaceAll(PRIMARY_ADMIN_NAME, adminName); const text = this.templateU117Text .replaceAll(AUTHOR_NAME, authorName) .replaceAll(FILE_NAME, fileName) .replaceAll(TYPIST_NAME, typistName) .replaceAll(PRIMARY_ADMIN_NAME, adminName); // メールを送信する this.sendMail( context, [authorEmail, typistEmail], [], this.mailFrom, subject, text, html, ); } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMailWithU117.name}`, ); } } /** * メールを送信する * @param context * @param to * @param cc * @param from * @param subject * @param text * @param html * @returns mail */ async sendMail( context: Context, to: string[], cc: string[], from: string, subject: string, text: string, html: string, ): Promise { this.logger.log(`[IN] [${context.getTrackingId()}] ${this.sendMail.name}`); try { const res = await sendgrid .send({ from: { email: from, }, to: to.map((v) => ({ email: v })), cc: cc.map((v) => ({ email: v })), subject: subject, text: text, html: html, }) .then((v) => v[0]); this.logger.log( `[${context.getTrackingId()}] status code: ${ res.statusCode } body: ${JSON.stringify(res.body)}`, ); } catch (e) { this.logger.error(`[${context.getTrackingId()}] error=${e}`); throw e; } finally { this.logger.log( `[OUT] [${context.getTrackingId()}] ${this.sendMail.name}`, ); } } }