diff --git a/.example.env b/.example.env index 48fe010..c479716 100644 --- a/.example.env +++ b/.example.env @@ -10,6 +10,7 @@ KAKAO_CLIENT_ID= KAKAO_REDIRECT_URL= KAKAO_REST_API_KEY= CLIENT_URL= +GPT_KEY= PORT=8000 diff --git a/additional.d.ts b/additional.d.ts index 727e7df..762254f 100644 --- a/additional.d.ts +++ b/additional.d.ts @@ -12,6 +12,7 @@ declare namespace NodeJS { KAKAO_REDIRECT_URL: string; KAKAO_REST_API_KEY: string; CLIENT_URL: string; + GPT_KEY: string; // for test TEST_USER_ID?: number; diff --git a/package.json b/package.json index 0883c5a..ef86385 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ "cookie-parser": "^1.4.6", "joi": "^17.13.3", "moment-timezone": "^0.5.45", + "openai": "^4.52.2", "passport-custom": "^1.1.1", "passport-jwt": "^4.0.1", "passport-kakao": "^1.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97a02a8..c123327 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,6 +64,9 @@ importers: moment-timezone: specifier: ^0.5.45 version: 0.5.45 + openai: + specifier: ^4.52.2 + version: 4.52.2 passport-custom: specifier: ^1.1.1 version: 1.1.1 @@ -1532,6 +1535,18 @@ packages: integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==, } + '@types/node-fetch@2.6.11': + resolution: + { + integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==, + } + + '@types/node@18.19.39': + resolution: + { + integrity: sha512-nPwTRDKUctxw3di5b4TfT3I0sWDiWoPQCZjXhvdkINntwr8lcoVCKsTgnXeRubKIlfnV+eN/HYk6Jb40tbcEAQ==, + } + '@types/node@20.14.2': resolution: { @@ -1812,6 +1827,13 @@ packages: integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==, } + abort-controller@3.0.0: + resolution: + { + integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==, + } + engines: { node: '>=6.5' } + accepts@1.3.8: resolution: { @@ -1850,6 +1872,13 @@ packages: engines: { node: '>=0.4.0' } hasBin: true + agentkeepalive@4.5.0: + resolution: + { + integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==, + } + engines: { node: '>= 8.0.0' } + ajv-draft-04@1.0.0: resolution: { @@ -3007,6 +3036,13 @@ packages: } engines: { node: '>= 0.6' } + event-target-shim@5.0.1: + resolution: + { + integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==, + } + engines: { node: '>=6' } + events@3.3.0: resolution: { @@ -3253,6 +3289,12 @@ packages: typescript: '>3.6.0' webpack: ^5.11.0 + form-data-encoder@1.7.2: + resolution: + { + integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==, + } + form-data@4.0.0: resolution: { @@ -3260,6 +3302,13 @@ packages: } engines: { node: '>= 6' } + formdata-node@4.4.1: + resolution: + { + integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==, + } + engines: { node: '>= 12.20' } + formidable@3.5.1: resolution: { @@ -3570,6 +3619,12 @@ packages: } engines: { node: '>=10.17.0' } + humanize-ms@1.2.1: + resolution: + { + integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==, + } + husky@9.0.11: resolution: { @@ -4659,6 +4714,13 @@ packages: integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==, } + node-domexception@1.0.0: + resolution: + { + integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==, + } + engines: { node: '>=10.5.0' } + node-emoji@1.11.0: resolution: { @@ -4764,6 +4826,13 @@ packages: } engines: { node: '>=6' } + openai@4.52.2: + resolution: + { + integrity: sha512-mMc0XgFuVSkcm0lRIi8zaw++otC82ZlfkCur1qguXYWPETr/+ZwL9A/vvp3YahX+shpaT6j03dwsmUyLAfmEfg==, + } + hasBin: true + optionator@0.9.4: resolution: { @@ -6304,6 +6373,20 @@ packages: integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==, } + web-streams-polyfill@3.3.3: + resolution: + { + integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==, + } + engines: { node: '>= 8' } + + web-streams-polyfill@4.0.0-beta.3: + resolution: + { + integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==, + } + engines: { node: '>= 14' } + webidl-conversions@3.0.1: resolution: { @@ -7543,6 +7626,15 @@ snapshots: '@types/mime@1.3.5': {} + '@types/node-fetch@2.6.11': + dependencies: + '@types/node': 20.14.2 + form-data: 4.0.0 + + '@types/node@18.19.39': + dependencies: + undici-types: 5.26.5 + '@types/node@20.14.2': dependencies: undici-types: 5.26.5 @@ -7767,6 +7859,10 @@ snapshots: '@xtuc/long@4.2.2': {} + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -7786,6 +7882,10 @@ snapshots: acorn@8.12.0: {} + agentkeepalive@4.5.0: + dependencies: + humanize-ms: 1.2.1 + ajv-draft-04@1.0.0(ajv@8.13.0): optionalDependencies: ajv: 8.13.0 @@ -8448,6 +8548,8 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: {} + events@3.3.0: {} execa@0.7.0: @@ -8653,12 +8755,19 @@ snapshots: typescript: 5.3.3 webpack: 5.90.1(@swc/core@1.6.1) + form-data-encoder@1.7.2: {} + form-data@4.0.0: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 mime-types: 2.1.35 + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + formidable@3.5.1: dependencies: dezalgo: 1.0.4 @@ -8839,6 +8948,10 @@ snapshots: human-signals@2.1.0: {} + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + husky@9.0.11: {} iconv-lite@0.4.24: @@ -9610,6 +9723,8 @@ snapshots: node-addon-api@3.2.1: optional: true + node-domexception@1.0.0: {} + node-emoji@1.11.0: dependencies: lodash: 4.17.21 @@ -9655,6 +9770,19 @@ snapshots: dependencies: mimic-fn: 2.1.0 + openai@4.52.2: + dependencies: + '@types/node': 18.19.39 + '@types/node-fetch': 2.6.11 + abort-controller: 3.0.0 + agentkeepalive: 4.5.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + web-streams-polyfill: 3.3.3 + transitivePeerDependencies: + - encoding + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -10460,6 +10588,10 @@ snapshots: dependencies: defaults: 1.0.4 + web-streams-polyfill@3.3.3: {} + + web-streams-polyfill@4.0.0-beta.3: {} + webidl-conversions@3.0.1: {} webpack-node-externals@3.0.0: {} diff --git a/src/app.module.ts b/src/app.module.ts index 6998343..fd7ab94 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,6 +10,7 @@ import { AppService } from './app.service'; import { AuthModule } from './auth/auth.module'; import { getNodeEnv, isIgnoreEnvFile } from './common/helper/env.helper'; import { envValidation } from './common/helper/env.validation'; +import { GptModule } from './gpt/gpt.module'; import { MapModule } from './map/map.module'; import { SearchModule } from './search/search.module'; import { UserMapModule } from './user-map/user-map.module'; @@ -29,6 +30,7 @@ import { UserModule } from './user/user.module'; UserModule, MapModule, UserMapModule, + GptModule, SearchModule, ], controllers: [AppController], diff --git a/src/common/helper/env.validation.ts b/src/common/helper/env.validation.ts index 78bc2fd..03da53a 100644 --- a/src/common/helper/env.validation.ts +++ b/src/common/helper/env.validation.ts @@ -68,6 +68,9 @@ export class EnvironmentVariables { @IsString() CLIENT_URL: string; + + @IsString() + GPT_KEY: string; } export function envValidation(config: Record) { diff --git a/src/gpt/gpt.controller.ts b/src/gpt/gpt.controller.ts new file mode 100644 index 0000000..9e5b31a --- /dev/null +++ b/src/gpt/gpt.controller.ts @@ -0,0 +1,19 @@ +import { Controller, Get, Injectable, Param } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +import { UseAuthGuard } from 'src/common/decorators/auth-guard.decorator'; + +import { GptService } from './gpt.service'; + +@Injectable() +@ApiTags('gpt') +@Controller('gpt') +export class GptController { + constructor(private readonly gptService: GptService) {} + + @Get(':word') + @UseAuthGuard(['ADMIN']) + checkIfIsBadWordx(@Param('word') word: string) { + return this.gptService.checkIfIsBadwordWithGpt(word); + } +} diff --git a/src/gpt/gpt.module.ts b/src/gpt/gpt.module.ts new file mode 100644 index 0000000..fc94c4c --- /dev/null +++ b/src/gpt/gpt.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; + +import { GptController } from './gpt.controller'; +import { GptService } from './gpt.service'; + +@Module({ + imports: [], + controllers: [GptController], + providers: [GptService], + exports: [GptService], +}) +export class GptModule {} diff --git a/src/gpt/gpt.service.ts b/src/gpt/gpt.service.ts new file mode 100644 index 0000000..8032ff3 --- /dev/null +++ b/src/gpt/gpt.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; + +import OpenAI from 'openai'; + +@Injectable() +export class GptService { + private openai: OpenAI; + constructor(private readonly configService: ConfigService) { + this.openai = new OpenAI({ + apiKey: this.configService.get('GPT_KEY'), + }); + } + + async checkIfIsBadwordWithGpt(text: string): Promise { + const systemPrompt = ` + You are an AI assistant that check if a word user provide is badword in Korean. + + Return only the boolean type in javascript language as your response. + `; + + try { + const response = await this.openai.chat.completions.create({ + model: 'gpt-3.5-turbo', + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: text }, + ], + temperature: 0.2, + }); + + const res = response.choices[0].message?.content; + + if (!['true', 'false'].includes(res)) { + return false; + } + + return JSON.parse(res); + } catch (e) { + console.error(e); + return false; + } + } +}