commit ce93877e8d95d9b4e97c28c78f03a2551114c77a Author: JianMiau Date: Tue Jan 17 11:38:17 2023 +0800 [add] first diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..de625be --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +node_modules +npm-debug.log \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84a9f86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +.env +package-lock.json +*.pem +.foreverignore +.vscode \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1b7093b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +# sudo docker build -t linebotts . +# sudo docker exec -it 2e8e3995aa52 /bin/bash + +# 選擇node +FROM node:19.4.0 + +# 指定NODE_ENV為production +ENV NODE_ENV=production + +# 指定預設/工作資料夾 +WORKDIR /app + +# 只copy package.json檔案 +COPY ["package.json", "./"] + +# 安裝dependencies +# If you are building your code for production +# RUN npm ci --only=production +RUN npm install + +# copy其餘目錄及檔案 +COPY . . + +# 指定啟動container後執行命令 +CMD [ "npm", "start" ] \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..84e5e95 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "line-bot-ts", + "version": "1.0.0", + "description": "", + "main": "src/app.ts", + "scripts": { + "start": "nodemon src/app.ts", + "test": "nodemon src/app.ts", + "dev": "nodemon --exec \"node --require ts-node/register --inspect=192.168.1.15:9229 src/app.ts\"", + "build": "tsc --project ./" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@types/dateformat": "^5.0.0", + "@types/express": "^4.17.15", + "@types/node": "^18.11.18", + "typescript": "^4.9.4" + }, + "dependencies": { + "@line/bot-sdk": "^7.5.2", + "dateformat": "^4.5.1", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "fs": "^0.0.1-security", + "linebot": "^1.6.1", + "nodemon": "^2.0.20", + "openai": "^3.1.0", + "ts-node": "^10.9.1", + "xmlhttprequest": "^1.8.0" + } +} diff --git a/src/LineBotClass.ts b/src/LineBotClass.ts new file mode 100644 index 0000000..d329f35 --- /dev/null +++ b/src/LineBotClass.ts @@ -0,0 +1,118 @@ +import * as line from "@line/bot-sdk"; +import dateFormat from "dateformat"; +import express from "express"; +import fs from "fs"; +import { IncomingMessage, ServerResponse } from "http"; +import https from "https"; +import MemberJoinedClass from "./MemberJoinedClass"; +import MessageClass from "./MessageClass"; + +/** + * LineBot + */ +export default class LineBotClass { + + //#region private + + private bot: line.Client = null; + + private message: MessageClass; + private memberJoined: MemberJoinedClass; + + //#endregion + + //#region Lifecycle + + /** + * + */ + constructor() { + //讀取憑證及金鑰 + const prikey: string = fs.readFileSync("./certificate/RSA-privkey.pem", "utf8"); + const cert: string = fs.readFileSync("./certificate/RSA-cert.pem", "utf8"); + const cafile: string = fs.readFileSync("./certificate/RSA-chain.pem", "utf-8"); + + //建立憑證及金鑰 + const credentials: Object = { + key: prikey, + cert: cert, + ca: cafile + }; + + // 用於辨識Line Channel的資訊 + const config: any = { + channelSecret: process.env.channelSecret, + channelAccessToken: process.env.channelAccessToken || "" + }; + this.bot = new line.Client(config); + this.message = new MessageClass(this.bot); + this.memberJoined = new MemberJoinedClass(this.bot); + // const ZhuHanbot: linebot.Client = new linebot.Client({ + // channelSecret: process.env.ZhuHanchannelSecret, + // channelAccessToken: process.env.ZhuHanchannelAccessToken || "" + // }); + + + // Bot所監聽的webhook路徑與port + const path: string = process.env.URLPATH || "/"; + const port: number = +process.env.PORT || 3000; + // tslint:disable-next-line:typedef + const app = express(); + const httpsServer: https.Server = https.createServer(credentials, app); + app.post(path, line.middleware(config), (req, res) => { + Promise + .all(req.body.events.map(this.handleEvent.bind(this))) + .then((result) => res.json(result)); + }); + httpsServer.listen(port, () => { + let datetime: string = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss"); + console.log(`${datetime} listening on ${port}`); + console.log(`${datetime} [BOT已準備就緒]`); + }); + } + + //#endregion + + //#region Custom + + private async handleEvent(event: any): Promise { + switch (event.type) { + case "message": { + this.message.Message(event); + break; + } + + case "postback": { + // self.Postback.Postback(event); + return Promise.resolve(null); + } + + case "join": + case "leave": + case "follow": + case "unfollow": + + case "memberJoin": { + this.memberJoined.MemberJoined(event); + break; + } + + case "memberLeave": + case "accountLink": + case "fallback": + default: { + return Promise.resolve(null); + } + } + // if (event.type !== "message" || event.message.type !== "text") { + // return Promise.resolve(null); + // } + + // return this.bot.replyMessage(event.replyToken, { + // type: "text", + // text: event.message.text + // }); + } + + //#endregion +} diff --git a/src/LineNotify.ts b/src/LineNotify.ts new file mode 100644 index 0000000..fb4ffe4 --- /dev/null +++ b/src/LineNotify.ts @@ -0,0 +1,37 @@ +/** + * LineNotify + */ +export default class LineNotify { + + //#region Custom + public static Send(message: string): void { + const data: string = `message=\n${message}`; + + const xhr: XMLHttpRequest = new XMLHttpRequest(); + + xhr.addEventListener("readystatechange", function (): void { + if (this.readyState === 4) { + // console.log(this.responseText); + } + }); + + xhr.open("POST", "https://notify-api.line.me/api/notify"); + xhr.setRequestHeader("Authorization", "Bearer Dkv8Yh1Li3XsKFqZkmFMNP5o0JDSvan7qfcDmSv9GJr"); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + + xhr.send(data); + } + + public static SendBadmintonNotify(): void { + const xhr: XMLHttpRequest = new XMLHttpRequest(); + xhr.addEventListener("readystatechange", function (): void { + if (this.readyState === 4) { + // console.log(this.responseText); + } + }); + xhr.open("POST", "http://jianmiau.tk:1880/BadmintonNotify"); + xhr.send(); + } + + //#endregion +} diff --git a/src/MemberJoinedClass.ts b/src/MemberJoinedClass.ts new file mode 100644 index 0000000..c6428ac --- /dev/null +++ b/src/MemberJoinedClass.ts @@ -0,0 +1,55 @@ +import * as line from "@line/bot-sdk"; +import LineNotify from "./LineNotify"; + +/** + * MemberJoined + */ +export default class MemberJoinedClass { + + //#region private + + private bot: line.Client = null; + + //#endregion + + //#region Lifecycle + + constructor(bot: line.Client) { + this.bot = bot; + } + + //#endregion + + //#region Custom + + public async MemberJoined(event: any): Promise { + switch (event.source.groupId) { + case process.env.toBadminton: { + this.badminton_MemberJoin(event); + break; + } + + default: + break; + } + } + + private async badminton_MemberJoin(event: any): Promise { + const members: any[] = event.joined.members; + for (let i: number = 0; i < members.length; i++) { + const userId: string = members[i].userId; + this.sendBadminton_Welcome(event, event.source.groupId, userId); + } + } + + private async sendBadminton_Welcome(event: any, groupId: string, userId: string): Promise { + const userdata: line.Profile = await this.bot.getGroupMemberProfile(groupId, userId); + if (userdata && userdata.displayName) { + const message: string = `歡迎尊貴的 ${userdata.displayName} 降臨羽球團`; + event.reply(message); + LineNotify.SendBadmintonNotify(); + } + } + + //#endregion +} diff --git a/src/MessageClass.ts b/src/MessageClass.ts new file mode 100644 index 0000000..1213eba --- /dev/null +++ b/src/MessageClass.ts @@ -0,0 +1,162 @@ +import * as line from "@line/bot-sdk"; +import OpenAI from "./OpenAI"; + +/** + * Message + */ +export default class MessageClass { + + //#region private + + private bot: line.Client = null; + + //#endregion + + //#region Lifecycle + + constructor(bot: line.Client) { + this.bot = bot; + } + + //#endregion + + //#region Custom + + public async Message(event: any): Promise { + switch (event.message.type) { + case "text": { + return await this.Text(event); + } + + case "sticker": { + return await this.Sticker(event); + } + + default: + break; + } + } + + public async Text(event: any): Promise { + switch (event.source.type) { + case "user": { + return await this.User(event); + } + + case "group": + default: + break; + } + } + + public async Sticker(event: any): Promise { + switch (event.source.type) { + case "user": { + let replyMsg: string = `https://liff.line.me/1657715144-m4W6lyjL?type=sticker&stk=noanim&sid=${event.message.stickerId}&pkg=${event.message.packageId}`; + const resp: line.Message = { + type: "text", + text: replyMsg + }; + return this.bot.replyMessage(event.replyToken, resp); + } + + case "group": + default: { + return Promise.resolve(null); + } + } + } + + public async User(event: any): Promise { + let userId = event.source.userId; + let replyMsg = event.message.text; + // let displayName = ""; + // let profile = await this.bot.getProfile(userId); + // if (profile) { + // displayName = profile.displayName; + // } + // 豬喵 特別功能 + if ([process.env.toJianMiau, process.env.toZhuHan].includes(userId)) { + const response: string = await OpenAI.RunOpenAI(replyMsg); + replyMsg = response; + } + // /** 訊息 */ + // let Msg = event.message.text.split(" "); + + // /** 指令 */ + // let Instruction = Msg[0]; + // switch (Instruction) { + // case "msg": + // case "Msg": + // case "MSG": { + // if (userId == process.env.toJianMiau) { + // replyMsg = ""; + // if (Msg[1] === "豬涵") { + // if (Msg[2] === "豬涵") { + // Msg[2] = process.env.toZhuHantoZhuHan; + // } else if (Msg[2] === "建喵") { + // Msg[2] = process.env.toZhuHantoJianMiau; + // } + // for (let i = 3; i < Msg.length; i++) { + // replyMsg += Msg[i] + (i === Msg.length - 1 ? "" : " "); + // } + // let res_Msg = this.ZhuHanbot.push(Msg[2], replyMsg); + // } else { + // for (let i = 3; i < Msg.length; i++) { + // replyMsg += Msg[i] + (i === Msg.length - 1 ? "" : " "); + // } + // let res_Msg = this.bot.push(Msg[2], replyMsg); + // } + + // let ToJM_message = "已發送訊息:"; + // ToJM_message += `\nMyId: ${Msg[1]}`; + // ToJM_message += `\nuserId: ${Msg[2]}`; + // ToJM_message += `\nmessage: ${replyMsg}`; + // let res_reply = event.reply(ToJM_message).then(function (data) { + // // 當訊息成功回傳後的處理 + // }).catch(function (error) { + // // 當訊息回傳失敗後的處理 + // }); + // } + // break; + // } + + // default: { + // // 使用event.reply(要回傳的訊息)方法可將訊息回傳給使用者 + // event.reply(replyMsg).then(function (data) { + // // 當訊息成功回傳後的處理 + // }).catch(function (error) { + // // 當訊息回傳失敗後的處理 + // }); + // break; + // } + // } + // } + return this.bot.replyMessage(event.replyToken, { + type: "text", + text: replyMsg + }); + // return this.bot.replyMessage(event.replyToken, { + // type: "text", + // text: event.message.text + // }); + } + + // Group(event) { + // switch (event.source.groupId) { + // case process.env.toYoutube: { + // let messagereplace = event.message.text; + // messagereplace.replace("【IFTTT】 \n", ""); + // let replyMsg = messagereplace; + // let res_toUniversity = this.bot.pushMessage(process.env.toUniversity, replyMsg); + // let res_toApex = this.bot.pushMessage(process.env.toApex, replyMsg); + // break; + // } + + // default: + // break; + // } + // } + + //#endregion +} diff --git a/src/OpenAI.ts b/src/OpenAI.ts new file mode 100644 index 0000000..1449a1e --- /dev/null +++ b/src/OpenAI.ts @@ -0,0 +1,34 @@ +import { Configuration, CreateCompletionResponseChoicesInner, OpenAIApi } from "openai"; + +/** + * OpenAI + */ +export default class OpenAI { + + //#region Custom + + public static async RunOpenAI(msg: string): Promise { + const configuration = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, + }); + const openai = new OpenAIApi(configuration); + const response = await openai.createCompletion({ + model: "text-davinci-003", + prompt: msg, + temperature: 0.7, + max_tokens: 256, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0, + }); + let resptext: string = msg; + const choices = response.data.choices; + for (let i = 0; i < choices.length; i++) { + const choice: CreateCompletionResponseChoicesInner = choices[i]; + resptext += choice.text; + } + return resptext; + } + + //#endregion +} diff --git a/src/app.ts b/src/app.ts new file mode 100644 index 0000000..37fe0c5 --- /dev/null +++ b/src/app.ts @@ -0,0 +1,10 @@ +// 背景執行 forever start -c ts-node -a -l line-bot-ts.log src/app.ts +// 重新背景執行 forever restart -a -l line-bot-ts.log src/app.ts +// 監聽檔案變化 "npm start" +// 連線Debug "npm run dev" + +import dotenv from "dotenv"; +import LineBotClass from "./LineBotClass"; + +dotenv.config(); +new LineBotClass(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4a4ae41 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "es5", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "module": "commonjs", /* Specify what module code is generated. */ + "lib": [ + "es2015", + "es2017", + "dom" + ], + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": false, /* Enable all strict type-checking options. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "outDir": "dist" // 將編譯過後的js檔放到dist資料夾中 + } +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..b1671c0 --- /dev/null +++ b/tslint.json @@ -0,0 +1,107 @@ +{ + "defaultSeverity": "warning", + "rules": { + "ban": [ + true, + [ + "_", + "extend" + ], + [ + "_", + "isNull" + ], + [ + "_", + "isDefined" + ] + ], + "class-name": false, + "comment-format": [ + false, + "check-space" + ], + "curly": true, + "eofline": false, + "forin": false, + "indent": [ + true, + 4 + ], + "interface-name": [ + false, + "never-prefix" + ], + "jsdoc-format": false, + "label-position": true, + "max-line-length": [ + false, + 140 + ], + "no-arg": true, + "no-bitwise": false, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": true, + // "no-eval": true, + "no-string-literal": false, + "no-trailing-whitespace": true, + "no-unused-expression": false, + "no-unused-variable": true, + "no-use-before-declare": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [ + true, + "double" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef": [ + true, + "call-signature", + "parameter", + "property-declaration", + "variable-declaration" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace" + }, + { + "index-signature": "space" + } + ], + "variable-name": false, + "whitespace": [ + false, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} \ No newline at end of file