[add] first
This commit is contained in:
parent
d808934e8f
commit
25fb0de4c4
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
package-lock.json
|
||||||
|
*.pem
|
||||||
|
.foreverignore
|
||||||
|
.vscode
|
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# sudo docker build -t line_catan_groupbuy_ts .
|
||||||
|
# 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" ]
|
35
package.json
Normal file
35
package.json
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "line_catan_groupbuy_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/mysql": "^2.15.21",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"typescript": "^4.9.4"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@line/bot-sdk": "^7.5.2",
|
||||||
|
"dateformat": "^4.5.1",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
|
"dotenv": "^16.0.3",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"fs": "^0.0.1-security",
|
||||||
|
"linebot": "^1.6.1",
|
||||||
|
"mysql": "^2.18.1",
|
||||||
|
"nodemon": "^2.0.20",
|
||||||
|
"openai": "^3.1.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"xmlhttprequest": "^1.8.0"
|
||||||
|
}
|
||||||
|
}
|
44
src/DBTools.ts
Normal file
44
src/DBTools.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import mysql from "mysql";
|
||||||
|
import Tools from "./Tools";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DBTools
|
||||||
|
*/
|
||||||
|
export default class DBTools {
|
||||||
|
|
||||||
|
//#region Custom
|
||||||
|
|
||||||
|
public static async Query(query: string): Promise<any> {
|
||||||
|
const conn: mysql.Connection = this.connect();
|
||||||
|
|
||||||
|
let resp: any = null;
|
||||||
|
let run: boolean = true;
|
||||||
|
conn.query(query, function (err: mysql.MysqlError, rows: any, fields: mysql.FieldInfo[]): void {
|
||||||
|
if (err) {
|
||||||
|
console.error(`${query} Error: \n${err.message}`);
|
||||||
|
run = false;
|
||||||
|
}
|
||||||
|
resp = rows;
|
||||||
|
run = false;
|
||||||
|
});
|
||||||
|
while (run) {
|
||||||
|
await Tools.Sleep(100);
|
||||||
|
}
|
||||||
|
conn.end();
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static connect(): mysql.Connection {
|
||||||
|
const conn: mysql.Connection = mysql.createConnection({
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: +process.env.DB_PORT,
|
||||||
|
user: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASSWORD,
|
||||||
|
database: process.env.DB_DATABASE
|
||||||
|
});
|
||||||
|
conn.connect();
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
116
src/Engine/CCExtensions/ArrayExtension.ts
Normal file
116
src/Engine/CCExtensions/ArrayExtension.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
declare interface Array<T> {
|
||||||
|
/**
|
||||||
|
* 移除一個值並且回傳
|
||||||
|
* @param index
|
||||||
|
*/
|
||||||
|
ExRemoveAt(index: number): T;
|
||||||
|
/**
|
||||||
|
* 移除全部值(注意. 參考的也會被清空)
|
||||||
|
* @example
|
||||||
|
*
|
||||||
|
* let bar: number[] = [1, 2, 3];
|
||||||
|
* let bar2: number[] = bar;
|
||||||
|
* bar.Clear();
|
||||||
|
* console.log(bar, bar2);
|
||||||
|
*
|
||||||
|
* // {
|
||||||
|
* // "bar": [],
|
||||||
|
* // "bar2": []
|
||||||
|
* // }
|
||||||
|
*/
|
||||||
|
Clear(): void;
|
||||||
|
/**
|
||||||
|
* 物件陣列排序,asc&key陣列長度請一樣
|
||||||
|
* PS. boolean 帶false是先true在false
|
||||||
|
* @link JavaScript Object 排序 http://www.eion.com.tw/Blogger/?Pid=1170#:~:text=JavaScript%20Object%20排序
|
||||||
|
* @param asc 是否升序排列(小到大)
|
||||||
|
* @param key 需排序的key(優先順序左到右)(沒有就放空)
|
||||||
|
*/
|
||||||
|
ObjectSort(asc?: boolean[], key?: string[]): any[];
|
||||||
|
/**
|
||||||
|
* 設計給Array<cc.Component.EventHandler>forHoldButton使用
|
||||||
|
* Add a non persistent listener to the UnityEvent.
|
||||||
|
* @param call Callback function.
|
||||||
|
*/
|
||||||
|
AddListener(call: Function): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.prototype.ExRemoveAt || Object.defineProperty(Array.prototype, "ExRemoveAt", {
|
||||||
|
enumerable: false,
|
||||||
|
value: function (index: number): any {
|
||||||
|
let item: any = this.splice(index, 1);
|
||||||
|
return item[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Array.prototype.Clear || Object.defineProperty(Array.prototype, "Clear", {
|
||||||
|
enumerable: false,
|
||||||
|
value: function (): void {
|
||||||
|
this.length = 0;
|
||||||
|
|
||||||
|
// let foo: number[] = [1, 2, 3];
|
||||||
|
// let bar: number[] = [1, 2, 3];
|
||||||
|
// let foo2: number[] = foo;
|
||||||
|
// let bar2: number[] = bar;
|
||||||
|
// foo = [];
|
||||||
|
// bar.length = 0;
|
||||||
|
// console.log(foo, bar, foo2, bar2);
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "foo": [],
|
||||||
|
// "bar": [],
|
||||||
|
// "foo2": [
|
||||||
|
// 1,
|
||||||
|
// 2,
|
||||||
|
// 3
|
||||||
|
// ],
|
||||||
|
// "bar2": []
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Array.prototype.ObjectSort || Object.defineProperty(Array.prototype, "ObjectSort", {
|
||||||
|
enumerable: false,
|
||||||
|
/**
|
||||||
|
* @param asc 是否升序排列(小到大)
|
||||||
|
* @param key 需排序的key(優先順序左到右)(沒有就放空)
|
||||||
|
*/
|
||||||
|
value: function (asc: boolean[] = [true], key?: string[]): any[] {
|
||||||
|
if (this.length === 0) {
|
||||||
|
return this;
|
||||||
|
} else if (!key || key.length === 0) {
|
||||||
|
console.error(`ObjectSort key error`);
|
||||||
|
return this;
|
||||||
|
} else if (asc.length !== key.length) {
|
||||||
|
console.error(`ObjectSort key asc error asc.length: ${asc.length}, key.length: ${key.length}`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
for (let i: number = 0; i < key.length; i++) {
|
||||||
|
const keyname: string = key[i];
|
||||||
|
if (this[0][keyname] === undefined) {
|
||||||
|
console.error(`ObjectSort has not key[${i}]: ${keyname}`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let count: number = key ? key.length : 1;
|
||||||
|
let arr: any[];
|
||||||
|
for (let i: number = count - 1; i >= 0; i--) {
|
||||||
|
arr = this.sort(function (a: any, b: any): 1 | -1 {
|
||||||
|
let mya: any = a;
|
||||||
|
let myb: any = b;
|
||||||
|
if (key) {
|
||||||
|
mya = a[key[i]];
|
||||||
|
myb = b[key[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加個等於數字相同不要再去排序到
|
||||||
|
if (asc[i]) {
|
||||||
|
return mya >= myb ? 1 : -1;
|
||||||
|
} else {
|
||||||
|
return mya <= myb ? 1 : -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
});
|
10
src/Engine/CCExtensions/CCExtension.ts.meta
Normal file
10
src/Engine/CCExtensions/CCExtension.ts.meta
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"ver": "1.1.0",
|
||||||
|
"uuid": "b373f805-9297-4af5-8ea6-0a250649b5b0",
|
||||||
|
"importer": "typescript",
|
||||||
|
"isPlugin": false,
|
||||||
|
"loadPluginInWeb": true,
|
||||||
|
"loadPluginInNative": true,
|
||||||
|
"loadPluginInEditor": false,
|
||||||
|
"subMetas": {}
|
||||||
|
}
|
189
src/Engine/CCExtensions/NumberExtension.ts
Normal file
189
src/Engine/CCExtensions/NumberExtension.ts
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
|
||||||
|
declare interface Number {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 金額每三位數(千)加逗號, 並且補到小數點第2位
|
||||||
|
* 輸出 41,038,560.00
|
||||||
|
* @param precision 補到小數點第幾位
|
||||||
|
* @param isPadZero 是否要補零
|
||||||
|
* */
|
||||||
|
ExFormatNumberWithComma(precision?: number, isPadZero?: boolean): string;
|
||||||
|
/**
|
||||||
|
* 基本4位數(9,999-999B-T)
|
||||||
|
* */
|
||||||
|
ExTransferToBMK(precision?: number,offset?: number): string;
|
||||||
|
/**
|
||||||
|
* 數字轉字串, 頭補0
|
||||||
|
* @param size
|
||||||
|
*/
|
||||||
|
Pad(size: number): string;
|
||||||
|
/**
|
||||||
|
* 四捨五入到小數點第X位 (同server計算規則)
|
||||||
|
* @param precision
|
||||||
|
*/
|
||||||
|
ExToNumRoundDecimal(precision: number): number;
|
||||||
|
/**
|
||||||
|
* 無條件捨去到小數點第X位
|
||||||
|
* @param precision
|
||||||
|
*/
|
||||||
|
ExToNumFloorDecimal(precision: number): number;
|
||||||
|
/**
|
||||||
|
* 無條件捨去強制保留X位小數,如:2,會在2後面補上00.即2.00
|
||||||
|
* @param precision 補到小數點第幾位
|
||||||
|
* @param isPadZero 是否要補零
|
||||||
|
*/
|
||||||
|
ExToStringFloorDecimal(precision: number, isPadZero?: boolean): string;
|
||||||
|
/**
|
||||||
|
* 取整數)
|
||||||
|
*/
|
||||||
|
ExToInt():number;
|
||||||
|
/**
|
||||||
|
* 小數轉整數(支援科學符號)
|
||||||
|
*/
|
||||||
|
Float2Fixed():number;
|
||||||
|
/**
|
||||||
|
* 數字長度(支援科學符號)
|
||||||
|
*/
|
||||||
|
DigitLength():number;
|
||||||
|
|
||||||
|
target: number;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Number.prototype.ExFormatNumberWithComma || Object.defineProperty(Number.prototype, 'ExFormatNumberWithComma', {
|
||||||
|
enumerable: false,
|
||||||
|
value: function (precision: number = 2, isPadZero: boolean = true) {
|
||||||
|
|
||||||
|
// let arr = String(this).split('.');
|
||||||
|
let arr = this.ExToStringFloorDecimal(precision, isPadZero).split('.');
|
||||||
|
let num = arr[0], result = '';
|
||||||
|
while (num.length > 3) {
|
||||||
|
result = ',' + num.slice(-3) + result;
|
||||||
|
num = num.slice(0, num.length - 3);
|
||||||
|
}
|
||||||
|
if (num.length > 0) result = num + result;
|
||||||
|
return arr[1] ? result + '.' + arr[1] : result;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
Number.prototype.ExTransferToBMK || Object.defineProperty(Number.prototype, 'ExTransferToBMK', {
|
||||||
|
enumerable: false,
|
||||||
|
value: function (precision: number=2,offset: number = 0) {
|
||||||
|
/**千 */
|
||||||
|
let MONEY_1K: number = 1000;
|
||||||
|
/**萬 */
|
||||||
|
// let MONEY_10K: number = 10000;
|
||||||
|
/**十萬 */
|
||||||
|
// let MONEY_100K: number = 100000;
|
||||||
|
/**百萬 */
|
||||||
|
let MONEY_1M: number = 1000000;
|
||||||
|
/**千萬 */
|
||||||
|
// let MONEY_10M: number = 10000000;
|
||||||
|
/**億 */
|
||||||
|
// let MONEY_100M: number = 100000000;
|
||||||
|
/**十億 */
|
||||||
|
let MONEY_1B: number = 1000000000;
|
||||||
|
/**百億 */
|
||||||
|
// let MONEY_10B: number = 10000000000;
|
||||||
|
/**千億 */
|
||||||
|
// let MONEY_100B: number = 100000000000;
|
||||||
|
/**兆 */
|
||||||
|
// let MONEY_1T: number = 1000000000000;
|
||||||
|
offset = Math.pow(10, offset);
|
||||||
|
// if (this >= MONEY_1T * offset) {
|
||||||
|
// //(3)1,000T
|
||||||
|
// //1T~
|
||||||
|
// return (~~(this / MONEY_1T)).ExFormatNumberWithComma(0) + "T";
|
||||||
|
// }
|
||||||
|
if (this >= MONEY_1B * offset) {
|
||||||
|
//1,000B~900,000B
|
||||||
|
//1B~900B
|
||||||
|
return (this / MONEY_1B).ExFormatNumberWithComma(3, false) + "B";
|
||||||
|
}
|
||||||
|
else if (this >= MONEY_1M * offset) {
|
||||||
|
//1,000M~900,000M
|
||||||
|
//1M~900M
|
||||||
|
return (this / MONEY_1M).ExFormatNumberWithComma(3, false) + "M";
|
||||||
|
}
|
||||||
|
else if (this >= MONEY_1K * offset) {
|
||||||
|
//1,000K~900,000K
|
||||||
|
//1K~90K
|
||||||
|
return (this / MONEY_1K).ExFormatNumberWithComma(3, false) + "K";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
//0~9,000,000
|
||||||
|
//0~9,000
|
||||||
|
return this.ExFormatNumberWithComma(precision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Number.prototype.Pad || Object.defineProperty(Number.prototype, 'Pad', {
|
||||||
|
enumerable: false,
|
||||||
|
value: function (size: number) {
|
||||||
|
let s = this + "";
|
||||||
|
while (s.length < size) s = "0" + s;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Number.prototype.ExToNumRoundDecimal || Object.defineProperty(Number.prototype, 'ExToNumRoundDecimal', {
|
||||||
|
enumerable: false,
|
||||||
|
value: function (precision: number) {
|
||||||
|
return Math.round(Math.round(this * Math.pow(10, (precision || 0) + 1)) / 10) / Math.pow(10, (precision || 0));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Number.prototype.ExToInt || Object.defineProperty(Number.prototype, 'ExToInt',{
|
||||||
|
enumerable: false,
|
||||||
|
value: function (){
|
||||||
|
return ~~this;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Number.prototype.ExToNumFloorDecimal || Object.defineProperty(Number.prototype, 'ExToNumFloorDecimal', {
|
||||||
|
enumerable: false,
|
||||||
|
value: function (precision: number) {
|
||||||
|
let str = this.toPrecision(12);
|
||||||
|
let dotPos = str.indexOf('.');
|
||||||
|
return dotPos == -1 ? this : +`${str.substr(0, dotPos + 1 + precision)}`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Number.prototype.ExToStringFloorDecimal || Object.defineProperty(Number.prototype, 'ExToStringFloorDecimal', {
|
||||||
|
enumerable: false,
|
||||||
|
value: function (precision: number, isPadZero: boolean = true) {
|
||||||
|
// 取小數點第X位
|
||||||
|
let f = this.ExToNumFloorDecimal(precision);
|
||||||
|
let s = f.toString();
|
||||||
|
// 補0
|
||||||
|
if (isPadZero) {
|
||||||
|
let rs = s.indexOf('.');
|
||||||
|
if (rs < 0) {
|
||||||
|
rs = s.length;
|
||||||
|
s += '.';
|
||||||
|
}
|
||||||
|
while (s.length <= rs + precision) {
|
||||||
|
s += '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Number.prototype.Float2Fixed || Object.defineProperty(Number.prototype, 'Float2Fixed', {
|
||||||
|
enumerable: false,
|
||||||
|
value: function () {
|
||||||
|
if (this.toString().indexOf('e') === -1) {
|
||||||
|
return Number(this.toString().replace('.', ''));
|
||||||
|
}
|
||||||
|
const dLen = this.DigitLength();
|
||||||
|
return dLen > 0 ? +parseFloat((this * Math.pow(10, dLen)).toPrecision(12)) : this;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
Number.prototype.DigitLength || Object.defineProperty(Number.prototype, 'DigitLength', {
|
||||||
|
enumerable: false,
|
||||||
|
value: function () {
|
||||||
|
const eSplit = this.toString().split(/[eE]/);
|
||||||
|
const len = (eSplit[0].split('.')[1] || '').length - (+(eSplit[1] || 0));
|
||||||
|
return len > 0 ? len : 0;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
84
src/Engine/Number/NumberEx.ts
Normal file
84
src/Engine/Number/NumberEx.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
export module NumberEx {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测数字是否越界,如果越界给出提示
|
||||||
|
* @param {*number} num 输入数
|
||||||
|
*/
|
||||||
|
function checkBoundary(num: number) {
|
||||||
|
if (_boundaryCheckingState) {
|
||||||
|
if (num > Number.MAX_SAFE_INTEGER || num < Number.MIN_SAFE_INTEGER) {
|
||||||
|
console.warn(`${num} is beyond boundary when transfer to integer, the results may not be accurate`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精确乘法
|
||||||
|
*/
|
||||||
|
export function times(num1: number, num2: number, ...others: number[]): number {
|
||||||
|
if (others.length > 0) {
|
||||||
|
return times(times(num1, num2), others[0], ...others.slice(1));
|
||||||
|
}
|
||||||
|
const num1Changed = num1.Float2Fixed();
|
||||||
|
const num2Changed = num2.Float2Fixed();
|
||||||
|
const baseNum = num1.DigitLength() + num2.DigitLength();
|
||||||
|
const leftValue = num1Changed * num2Changed;
|
||||||
|
|
||||||
|
checkBoundary(leftValue);
|
||||||
|
|
||||||
|
return leftValue / Math.pow(10, baseNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精确加法
|
||||||
|
*/
|
||||||
|
export function plus(num1: number, num2: number, ...others: number[]): number {
|
||||||
|
if (others.length > 0) {
|
||||||
|
return plus(plus(num1, num2), others[0], ...others.slice(1));
|
||||||
|
}
|
||||||
|
const baseNum = Math.pow(10, Math.max(num1.DigitLength(), num2.DigitLength()));
|
||||||
|
return (times(num1, baseNum) + times(num2, baseNum)) / baseNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精确减法
|
||||||
|
*/
|
||||||
|
export function minus(num1: number, num2: number, ...others: number[]): number {
|
||||||
|
if (others.length > 0) {
|
||||||
|
return minus(minus(num1, num2), others[0], ...others.slice(1));
|
||||||
|
}
|
||||||
|
const baseNum = Math.pow(10, Math.max(num1.DigitLength(), num2.DigitLength()));
|
||||||
|
return (times(num1, baseNum) - times(num2, baseNum)) / baseNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 精确除法
|
||||||
|
*/
|
||||||
|
export function divide(num1: number, num2: number, ...others: number[]): number {
|
||||||
|
if (others.length > 0) {
|
||||||
|
return divide(divide(num1, num2), others[0], ...others.slice(1));
|
||||||
|
}
|
||||||
|
const num1Changed = num1.Float2Fixed();
|
||||||
|
const num2Changed = num2.Float2Fixed();
|
||||||
|
checkBoundary(num1Changed);
|
||||||
|
checkBoundary(num2Changed);
|
||||||
|
return times((num1Changed / num2Changed), Math.pow(10, num2.DigitLength() - num1.DigitLength()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 四舍五入
|
||||||
|
*/
|
||||||
|
export function round(num: number, ratio: number): number {
|
||||||
|
const base = Math.pow(10, ratio);
|
||||||
|
return divide(Math.round(times(num, base)), base);
|
||||||
|
}
|
||||||
|
|
||||||
|
let _boundaryCheckingState = false;
|
||||||
|
/**
|
||||||
|
* 是否进行边界检查
|
||||||
|
* @param flag 标记开关,true 为开启,false 为关闭
|
||||||
|
*/
|
||||||
|
function enableBoundaryChecking(flag = true) {
|
||||||
|
_boundaryCheckingState = flag;
|
||||||
|
}
|
||||||
|
}
|
90
src/Engine/Number/RandomEx.ts
Normal file
90
src/Engine/Number/RandomEx.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
export module RandomEx {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得隨機布林值
|
||||||
|
*/
|
||||||
|
export function GetBool() {
|
||||||
|
return GetInt() >= 0;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 取得隨機整數(回傳min ~ max - 1)
|
||||||
|
* @param min
|
||||||
|
* @param max
|
||||||
|
*/
|
||||||
|
export function GetInt(min: number = Number.MIN_VALUE, max: number = Number.MAX_VALUE): number {
|
||||||
|
return Math.floor(Math.random() * (max - min)) + min;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 取得隨機小數
|
||||||
|
* @param min
|
||||||
|
* @param max
|
||||||
|
*/
|
||||||
|
export function GetFloat(min: number = Number.MIN_VALUE, max: number = Number.MAX_VALUE): number {
|
||||||
|
return Math.random() * (max - min) + min;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 隨機取得複數個不重複回傳
|
||||||
|
* @param num 取得數量
|
||||||
|
* @param items 陣列
|
||||||
|
*/
|
||||||
|
export function GetMultiNoRepeat(num: number, items: any[]): any[] {
|
||||||
|
let result: any[] = [];
|
||||||
|
for (let i: number = 0; i < num; i++) {
|
||||||
|
let ran: number = Math.floor(Math.random() * items.length);
|
||||||
|
let item = items.splice(ran, 1)[0];
|
||||||
|
if (result.indexOf(item) == -1) {
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根據權重取得複數個不重複回傳
|
||||||
|
* @param prize 獎項
|
||||||
|
* @param weights 機率
|
||||||
|
* @param count 數量
|
||||||
|
*/
|
||||||
|
export function GetMultiNoRepeatByWeight(prize: any[], weights: number[] = null, count: number = 1): any[] {
|
||||||
|
if (weights === null) {
|
||||||
|
weights = [];
|
||||||
|
for (let i: number = 0; i < prize.length; i++) {
|
||||||
|
weights.push(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let target: any[] = [];
|
||||||
|
for (let i: number = 0; i < count; i++) {
|
||||||
|
let results: number[] = RandomEx.GetPrizeByWeight(prize, weights);
|
||||||
|
prize.splice(results[0], 1);
|
||||||
|
weights.splice(results[0], 1);
|
||||||
|
target.push(results[1]);
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根據權重隨機取值
|
||||||
|
* @param prize 獎項
|
||||||
|
* @param weights 機率
|
||||||
|
*/
|
||||||
|
export function GetPrizeByWeight(prize: any[], weights: number[]): any[] {
|
||||||
|
if (prize.length !== weights.length) {
|
||||||
|
console.error(`GetWeight error -> prize.length:${prize.length} !== weights.length:${weights.length}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let totalWeight: number = 0;
|
||||||
|
for (let i: number = 0; i < weights.length; i++) {
|
||||||
|
totalWeight += weights[i];
|
||||||
|
}
|
||||||
|
let random: number = RandomEx.GetInt(0, totalWeight) + 1;
|
||||||
|
let nowWeight: number = weights[0];
|
||||||
|
for (let i: number = 0; i < weights.length; i++) {
|
||||||
|
if (nowWeight >= random) {
|
||||||
|
return [i, prize[i]];
|
||||||
|
}
|
||||||
|
nowWeight += weights[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/Engine/String.ts
Normal file
16
src/Engine/String.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
interface StringConstructor {
|
||||||
|
IsNullOrEmpty: (value: string) => boolean;
|
||||||
|
Format: (format: string, ...args: any[]) => string;
|
||||||
|
}
|
||||||
|
|
||||||
|
String.IsNullOrEmpty = function (value: string): boolean {
|
||||||
|
return value === undefined || value === null || value.trim() === "";
|
||||||
|
};
|
||||||
|
|
||||||
|
String.Format = function (format: string, ...args: any[]): string {
|
||||||
|
return format.replace(/{(\d+)}/g, (match, index) => {
|
||||||
|
let value: any = args[index];
|
||||||
|
if (value === null || value === undefined) { return ""; }
|
||||||
|
return "" + value;
|
||||||
|
});
|
||||||
|
};
|
212
src/GroupBuy.ts
Normal file
212
src/GroupBuy.ts
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import * as line from "@line/bot-sdk";
|
||||||
|
import DBTools from "./DBTools";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GroupBuy
|
||||||
|
*/
|
||||||
|
export default class GroupBuy {
|
||||||
|
|
||||||
|
//#region Custom
|
||||||
|
|
||||||
|
public static async Run(event: any, msg: string[], bot: line.Client): Promise<line.Message | line.Message[]> {
|
||||||
|
let message: line.Message | line.Message[] = null;
|
||||||
|
const instruction: string = msg[1];
|
||||||
|
switch (instruction) {
|
||||||
|
case "開團": {
|
||||||
|
message = await this.startBuying(event, msg, bot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "購買": {
|
||||||
|
message = await this.buying(event, msg, bot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "查詢": {
|
||||||
|
message = await this.searchBuying(event, msg[2], bot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "收單": {
|
||||||
|
message = await this.stopBuying(event, msg[2], bot);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 開始團購
|
||||||
|
* @param msg
|
||||||
|
*/
|
||||||
|
private static async startBuying(event: any, msg: any[], bot: line.Client): Promise<line.Message | line.Message[]> {
|
||||||
|
const storename: string = msg[2];
|
||||||
|
const query: string = String.Format("INSERT INTO `line_catan_groupbuy`.`groupbuydata` (`storename`) VALUES ('{0}');"
|
||||||
|
, storename);
|
||||||
|
const queryresp: any = await DBTools.Query(query);
|
||||||
|
|
||||||
|
const message: line.Message = {
|
||||||
|
type: "text",
|
||||||
|
text: storename + " 已開團成功"
|
||||||
|
};
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 購物
|
||||||
|
* @param msg
|
||||||
|
*/
|
||||||
|
private static async buying(event: any, msg: any[], bot: line.Client): Promise<line.Message | line.Message[]> {
|
||||||
|
const storename: string = msg[2];
|
||||||
|
const food: string = msg[3];
|
||||||
|
const count: number = +msg[4];
|
||||||
|
let note: string = msg[5] ?? "";
|
||||||
|
const userId: string = event.source.userId;
|
||||||
|
const userdata: line.Profile = await bot.getGroupMemberProfile(event.source.groupId, userId);
|
||||||
|
|
||||||
|
const query: string = String.Format("SELECT * FROM `line_catan_groupbuy`.`groupbuydata` WHERE `storename` = '{0}' ORDER BY `storename` DESC LIMIT 1;", storename);
|
||||||
|
const queryresp: any[] = await DBTools.Query(query);
|
||||||
|
if (queryresp.length === 0) {
|
||||||
|
const message: line.Message = {
|
||||||
|
type: "text",
|
||||||
|
text: `錯誤`
|
||||||
|
};
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
const querydata: any = queryresp[0];
|
||||||
|
if (!querydata.isopen) {
|
||||||
|
const message: line.Message = {
|
||||||
|
type: "text",
|
||||||
|
text: `團購未開放`
|
||||||
|
};
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalCount: number = count;
|
||||||
|
let data: any = querydata.data ? JSON.parse(querydata.data) : {};
|
||||||
|
if (data[food]) {
|
||||||
|
const userbuydata: any = data[food][userId];
|
||||||
|
if (userbuydata) {
|
||||||
|
totalCount = userbuydata.count = count;
|
||||||
|
if (note) {
|
||||||
|
userbuydata.note = note;
|
||||||
|
} else {
|
||||||
|
note = userbuydata.note;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const userbuydata: any = {
|
||||||
|
count: count,
|
||||||
|
note: note
|
||||||
|
};
|
||||||
|
data[food][userId] = userbuydata;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const userbuydata: any = {
|
||||||
|
count: count,
|
||||||
|
note: note
|
||||||
|
};
|
||||||
|
const adddata: Object = {};
|
||||||
|
adddata[userId] = userbuydata;
|
||||||
|
data[food] = adddata;
|
||||||
|
}
|
||||||
|
const data_str: string = JSON.stringify(data);
|
||||||
|
const queryAddData: string = String.Format("UPDATE `line_catan_groupbuy`.`groupbuydata` SET `data` = '{0}' WHERE `id` = {1};"
|
||||||
|
, data_str, querydata.id);
|
||||||
|
await DBTools.Query(queryAddData);
|
||||||
|
let test: string = `${userdata.displayName} 已${count > 0 ? "增加" : "減少"}成功 ${querydata.storename}的${food} ${count}個, 總共${totalCount}個`;
|
||||||
|
if (note) {
|
||||||
|
test += ` 備註: ${note}`;
|
||||||
|
}
|
||||||
|
const message: line.Message = {
|
||||||
|
type: "text",
|
||||||
|
text: test
|
||||||
|
};
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async searchBuying(event: any, storename: string, bot: line.Client): Promise<line.Message | line.Message[]> {
|
||||||
|
let resp: line.Message | line.Message[] = {
|
||||||
|
type: "text",
|
||||||
|
text: "沒有資料"
|
||||||
|
};
|
||||||
|
const query: string = String.Format("SELECT * FROM `line_catan_groupbuy`.`groupbuydata` WHERE `storename` = '{0}' ORDER BY `id` DESC LIMIT 1;", storename);
|
||||||
|
const queryresp: any[] = await DBTools.Query(query);
|
||||||
|
if (queryresp && queryresp.length > 0) {
|
||||||
|
resp = await this.getData_LineFlex(event, queryresp[0], bot);
|
||||||
|
} else {
|
||||||
|
const message: line.Message = {
|
||||||
|
type: "text",
|
||||||
|
text: `沒有符合的選項`
|
||||||
|
};
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async stopBuying(event: any, storename: string, bot: line.Client): Promise<line.Message | line.Message[]> {
|
||||||
|
let resp: line.Message | line.Message[] = {
|
||||||
|
type: "text",
|
||||||
|
text: "沒有資料"
|
||||||
|
};
|
||||||
|
const query: string = String.Format("SELECT * FROM `line_catan_groupbuy`.`groupbuydata` WHERE `storename` = '{0}' AND `isopen` = '1' ORDER BY `id` DESC LIMIT 1;", storename);
|
||||||
|
const queryresp: any[] = await DBTools.Query(query);
|
||||||
|
if (queryresp.length > 0) {
|
||||||
|
const query1: string = String.Format("UPDATE `line_catan_groupbuy`.`groupbuydata` SET `isopen` = 0 WHERE `id` = {0};", queryresp[0].id);
|
||||||
|
const queryresp1: any[] = await DBTools.Query(query1);
|
||||||
|
resp = await this.searchBuying(event, storename, bot);
|
||||||
|
} else {
|
||||||
|
const message: line.Message = {
|
||||||
|
type: "text",
|
||||||
|
text: `沒有符合的選項`
|
||||||
|
};
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取得名單Line模板
|
||||||
|
* @param storename
|
||||||
|
* @param matchs
|
||||||
|
*/
|
||||||
|
private static async getData_LineFlex(event: any, queryresp: any, bot: line.Client): Promise<line.TextMessage | line.Message[]> {
|
||||||
|
let data_LineFlex: string = queryresp.storename;
|
||||||
|
const data_Str: string = queryresp.data;
|
||||||
|
const names: string[] = [];
|
||||||
|
if (data_Str) {
|
||||||
|
const data: any = JSON.parse(data_Str);
|
||||||
|
for (let i: number = 0, data_key: string[] = Object.keys(data); i < data_key.length; i++) {
|
||||||
|
const food: string = data_key[i];
|
||||||
|
const shopping: Object = data ? data[food] : {};
|
||||||
|
data_LineFlex += `\n\n品項: ${food}`;
|
||||||
|
for (let j: number = 0, shopping_key: string[] = Object.keys(shopping); j < shopping_key.length; j++) {
|
||||||
|
const userId: string = shopping_key[j];
|
||||||
|
const userbuydata: any = shopping[userId];
|
||||||
|
const count: number = userbuydata.count;
|
||||||
|
const note: string = userbuydata.note;
|
||||||
|
if (count) {
|
||||||
|
let name: string = names[userId];
|
||||||
|
if (!name) {
|
||||||
|
const userdata: line.Profile = await bot.getGroupMemberProfile(event.source.groupId, userId);
|
||||||
|
name = names[userId] = userdata?.displayName ?? userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
data_LineFlex += `\n${name} ${count}個` + (note ? `, 備註: ${note}` : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data_LineFlex += `\nㄏ 沒人買`;
|
||||||
|
}
|
||||||
|
const messages: line.Message = {
|
||||||
|
type: "text",
|
||||||
|
text: data_LineFlex
|
||||||
|
};
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
122
src/LineBotClass.ts
Normal file
122
src/LineBotClass.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
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";
|
||||||
|
import PostbackClass from "./PostbackClass";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LineBot
|
||||||
|
*/
|
||||||
|
export default class LineBotClass {
|
||||||
|
|
||||||
|
//#region private
|
||||||
|
|
||||||
|
private bot: line.Client = null;
|
||||||
|
|
||||||
|
private message: MessageClass;
|
||||||
|
private postback: PostbackClass;
|
||||||
|
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.postback = new PostbackClass(this.bot);
|
||||||
|
this.memberJoined = new MemberJoinedClass(this.bot);
|
||||||
|
|
||||||
|
|
||||||
|
// 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<typeof IncomingMessage, typeof ServerResponse> = 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<any> {
|
||||||
|
try {
|
||||||
|
switch (event.type) {
|
||||||
|
case "message": {
|
||||||
|
this.message.Message(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "postback": {
|
||||||
|
this.postback.Postback(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "join":
|
||||||
|
case "leave":
|
||||||
|
case "follow":
|
||||||
|
case "unfollow":
|
||||||
|
case "memberJoin":
|
||||||
|
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
|
||||||
|
// });
|
||||||
|
} catch (error) {
|
||||||
|
let datetime: string = dateFormat(new Date(), "yyyy-mm-dd HH:MM:ss");
|
||||||
|
const messages: line.Message = {
|
||||||
|
type: "text",
|
||||||
|
text: `錯誤`
|
||||||
|
};
|
||||||
|
console.error(`${datetime} 錯誤:\n${JSON.stringify(event)}\n\n${error}`);
|
||||||
|
this.bot.replyMessage(event.replyToken, messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
37
src/LineNotify.ts
Normal file
37
src/LineNotify.ts
Normal file
@ -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
|
||||||
|
}
|
55
src/MemberJoinedClass.ts
Normal file
55
src/MemberJoinedClass.ts
Normal file
@ -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<void> {
|
||||||
|
switch (event.source.groupId) {
|
||||||
|
case process.env.toBadminton: {
|
||||||
|
this.badminton_MemberJoin(event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async badminton_MemberJoin(event: any): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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
|
||||||
|
}
|
87
src/MessageClass.ts
Normal file
87
src/MessageClass.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import * as line from "@line/bot-sdk";
|
||||||
|
import GroupBuy from "./GroupBuy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<void> {
|
||||||
|
switch (event.message.type) {
|
||||||
|
case "text": {
|
||||||
|
return await this.Text(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Text(event: any): Promise<void> {
|
||||||
|
switch (event.source.type) {
|
||||||
|
case "group": {
|
||||||
|
return await this.Group(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Group(event: any): Promise<any> {
|
||||||
|
let groupId: string = event.source.groupId;
|
||||||
|
// 團購 特別功能
|
||||||
|
if ([process.env.toGroupBuy].includes(groupId)) {
|
||||||
|
/** 訊息 */
|
||||||
|
let msg: string[] = event.message.text.split(" ");
|
||||||
|
/** 指令 */
|
||||||
|
let instruction: string = msg[0];
|
||||||
|
switch (instruction) {
|
||||||
|
case "團購": {
|
||||||
|
const messages: line.Message | line.Message[] = await GroupBuy.Run(event, msg, this.bot);
|
||||||
|
if (messages) {
|
||||||
|
return this.bot.replyMessage(event.replyToken, messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
35
src/OpenAI.ts
Normal file
35
src/OpenAI.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { Configuration, CreateCompletionResponseChoicesInner, OpenAIApi } from "openai";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenAI
|
||||||
|
*/
|
||||||
|
export default class OpenAI {
|
||||||
|
|
||||||
|
//#region Custom
|
||||||
|
|
||||||
|
public static async RunOpenAI(msg: string): Promise<string> {
|
||||||
|
const configuration: Configuration = new Configuration({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY,
|
||||||
|
});
|
||||||
|
const openai: OpenAIApi = new OpenAIApi(configuration);
|
||||||
|
// tslint:disable-next-line:typedef
|
||||||
|
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: CreateCompletionResponseChoicesInner[] = response.data.choices;
|
||||||
|
for (let i: number = 0; i < choices.length; i++) {
|
||||||
|
const choice: CreateCompletionResponseChoicesInner = choices[i];
|
||||||
|
resptext += choice.text;
|
||||||
|
}
|
||||||
|
return resptext;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
76
src/PostbackClass.ts
Normal file
76
src/PostbackClass.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import * as line from "@line/bot-sdk";
|
||||||
|
import GroupBuy from "./GroupBuy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Postback
|
||||||
|
*/
|
||||||
|
export default class PostbackClass {
|
||||||
|
|
||||||
|
//#region private
|
||||||
|
|
||||||
|
private bot: line.Client = null;
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Lifecycle
|
||||||
|
|
||||||
|
constructor(bot: line.Client) {
|
||||||
|
this.bot = bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Custom
|
||||||
|
|
||||||
|
public async Postback(event: any): Promise<void> {
|
||||||
|
switch (event.source.type) {
|
||||||
|
case "group": {
|
||||||
|
return await this.Group(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Group(event: any): Promise<any> {
|
||||||
|
let groupId: string = event.source.groupId;
|
||||||
|
// 團購 特別功能
|
||||||
|
if ([process.env.toGroupBuy].includes(groupId)) {
|
||||||
|
/** 訊息 */
|
||||||
|
let msg: string[] = event.postback.data.split(" ");
|
||||||
|
/** 指令 */
|
||||||
|
let instruction: string = msg[0];
|
||||||
|
switch (instruction) {
|
||||||
|
case "團購": {
|
||||||
|
const messages: line.Message | line.Message[] = await GroupBuy.Run(event, msg, this.bot);
|
||||||
|
if (messages) {
|
||||||
|
return this.bot.replyMessage(event.replyToken, messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
13
src/Tools.ts
Normal file
13
src/Tools.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Tools
|
||||||
|
*/
|
||||||
|
export default class Tools {
|
||||||
|
|
||||||
|
//#region Custom
|
||||||
|
|
||||||
|
public static Sleep(ms: number): Promise<any> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
}
|
16
src/app.ts
Normal file
16
src/app.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// 背景執行 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 dayjs from "dayjs";
|
||||||
|
import "dayjs/locale/zh-tw";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
import "./Engine/CCExtensions/ArrayExtension";
|
||||||
|
import "./Engine/CCExtensions/NumberExtension";
|
||||||
|
import "./Engine/String";
|
||||||
|
import LineBotClass from "./LineBotClass";
|
||||||
|
|
||||||
|
dayjs.locale("zh-tw");
|
||||||
|
dotenv.config();
|
||||||
|
new LineBotClass();
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -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資料夾中
|
||||||
|
}
|
||||||
|
}
|
107
tslint.json
Normal file
107
tslint.json
Normal file
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user