Add Node.js client
This commit is contained in:
Executable
+20
@@ -0,0 +1,20 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export class Context {}
|
||||
|
||||
export class Interaction {
|
||||
constructor(input: string) {
|
||||
this.input = input;
|
||||
this.options = {};
|
||||
}
|
||||
input: string;
|
||||
output?: string;
|
||||
options: any;
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
name: string;
|
||||
follow: (interaction: Interaction) => number;
|
||||
run: (ctx: Context, interaction: Interaction) => string | Promise<string>;
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import { log } from '../utils/log';
|
||||
import { Interaction, Command, Context } from './base-command';
|
||||
import rsp from './rock-scissors-paper';
|
||||
import stock from './stock';
|
||||
|
||||
let commands: Command[] = [rsp, stock];
|
||||
|
||||
export const handleCommand = async (ctx: Context, input: string): Promise<string> => {
|
||||
const interaction = new Interaction(input);
|
||||
const choice = commands
|
||||
.map((c) => {
|
||||
return { score: c.follow(interaction), command: c };
|
||||
})
|
||||
?.sort((a, b) => {
|
||||
return a.score - b.score;
|
||||
})
|
||||
?.filter((x) => x.score > 0)
|
||||
?.pop();
|
||||
|
||||
if (!choice) {
|
||||
log.debug('no command found');
|
||||
return '';
|
||||
}
|
||||
const reply = await choice.command.run(ctx, interaction);
|
||||
return reply;
|
||||
};
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import { log } from '../utils/log';
|
||||
import { Interaction, Command, Context } from './base-command';
|
||||
|
||||
function RandomNum(max: number): number {
|
||||
return Math.floor(Math.random() * (max + 1));
|
||||
}
|
||||
|
||||
const keywords = ['rock', 'paper', 'scissors'];
|
||||
|
||||
export default {
|
||||
name: 'rock-scissors-paper',
|
||||
follow: (interaction: Interaction): number => {
|
||||
const i = keywords.find((k) => {
|
||||
return interaction.input.includes(k);
|
||||
});
|
||||
log.debug(`rock-scissors-paper follow-up: ${i}`);
|
||||
interaction.options.pick = i ?? undefined;
|
||||
return i ? 1 : 0;
|
||||
},
|
||||
run: (ctx: Context, interaction: Interaction): string => {
|
||||
const pick: string = interaction.options.pick as string;
|
||||
|
||||
const playerPick = keywords.findIndex((x) => x == pick);
|
||||
const botPick = RandomNum(2);
|
||||
|
||||
let result = 'tied';
|
||||
|
||||
if (playerPick == botPick) result = 'Tied';
|
||||
else if (playerPick == 0 && botPick == 2) result = 'Win';
|
||||
else if ((playerPick - botPick) % 3 == 1) result = 'Win';
|
||||
else result = 'Lose';
|
||||
|
||||
const content = `You picked **${pick}** and I picked **${keywords[botPick]}**, You **${result}**`;
|
||||
return content;
|
||||
}
|
||||
} as Command;
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import { stocks } from 'stock-api';
|
||||
import { Interaction, Command, Context } from './base-command';
|
||||
|
||||
function getPrice(code: string): Promise<string> {
|
||||
return stocks.tencent.getStocks([code]).then((res) => {
|
||||
return JSON.stringify(res[0]);
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'stock',
|
||||
follow: (interaction: Interaction): number => {
|
||||
let match = /(S[HZ]\d{6})/.exec(interaction.input);
|
||||
if (match) {
|
||||
let stock = match && match[0];
|
||||
interaction.options.stock = stock;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
run: async (ctx: Context, interaction: Interaction): Promise<string> => {
|
||||
const pick: string = interaction.options.stock as string;
|
||||
const content = await getPrice(pick);
|
||||
return content;
|
||||
}
|
||||
} as Command;
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import * as readline from 'readline';
|
||||
import { stdin as input } from 'node:process';
|
||||
import { WxMsg } from '../proto/wcf_pb';
|
||||
import { log } from './utils/log';
|
||||
import { handleCommand } from './commands';
|
||||
import { Context } from './commands/base-command';
|
||||
import { SDK } from './wcf/win32';
|
||||
import { WCF } from './wcf';
|
||||
import { portAvailable, sleep } from './utils/misic';
|
||||
|
||||
let ctx = new Context();
|
||||
|
||||
(async () => {
|
||||
if (await portAvailable(10086)) {
|
||||
log.debug(`Call SDK to inject.`);
|
||||
const sdk = new SDK();
|
||||
sdk.WxInitSDK();
|
||||
}
|
||||
|
||||
// wait util WCF is ready
|
||||
while (!(await portAvailable(10086))) {
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
log.debug(`Connect to gRPC and wait for login.`);
|
||||
const wcf = new WCF();
|
||||
while (!(await wcf.IsLogin())) {
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
wcf.on('message', async (msg: WxMsg) => {
|
||||
const txt = msg.getContent();
|
||||
log.debug(txt, msg.getIsSelf());
|
||||
if (!msg.getIsSelf()) {
|
||||
const reply = await handleCommand(ctx, txt);
|
||||
if (msg.getIsGroup()) {
|
||||
wcf.SendTextMsg(msg.getRoomid(), reply);
|
||||
} else {
|
||||
wcf.SendTextMsg(msg.getSender(), reply);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
wcf.EnableReceiveMsg();
|
||||
})();
|
||||
|
||||
const std = readline.createInterface({ input });
|
||||
std.on('line', async (line) => {
|
||||
const reply = await handleCommand(ctx, line);
|
||||
log.debug(`reply: ${reply}`);
|
||||
if (line === 'exit') {
|
||||
std.close();
|
||||
}
|
||||
});
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import { Logger } from 'tslog';
|
||||
|
||||
export const log: Logger = new Logger({
|
||||
name: 'main',
|
||||
displayLoggerName: false,
|
||||
overwriteConsole: true,
|
||||
dateTimePattern: 'monthday hour:minute:second'
|
||||
});
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export function portAvailable(port: number): Promise<boolean> {
|
||||
const net = require('net');
|
||||
return new Promise((resolve, _reject) => {
|
||||
let server = net.createServer().listen(port);
|
||||
server.on('listening', function () {
|
||||
server.close();
|
||||
resolve(true);
|
||||
});
|
||||
server.on('error', function (err: any) {
|
||||
if (err.code == 'EADDRINUSE') {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
import * as grpc from 'grpc';
|
||||
import { WcfClient, IWcfClient } from '../../proto/wcf_grpc_pb';
|
||||
import { Empty, TextMsg, WxMsg } from '../../proto/wcf_pb';
|
||||
|
||||
type MessageEvents = {
|
||||
error: (error: Error) => void;
|
||||
message: (msg: WxMsg) => void;
|
||||
};
|
||||
|
||||
export class WCF extends (EventEmitter as new () => TypedEmitter<MessageEvents>) {
|
||||
client: IWcfClient;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.client = new WcfClient(`localhost:10086`, grpc.credentials.createInsecure());
|
||||
}
|
||||
|
||||
async IsLogin(): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const request = new Empty();
|
||||
this.client.rpcIsLogin(request, (err, ret) => {
|
||||
if (err) reject(err);
|
||||
else resolve(ret.getStatus() !== 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async EnableReceiveMsg() {
|
||||
let stream = this.client.rpcEnableRecvMsg(new Empty());
|
||||
stream.on('data', (d) => {
|
||||
this.emit('message', d);
|
||||
});
|
||||
stream.on('error', (err) => {
|
||||
this.emit('error', err);
|
||||
});
|
||||
}
|
||||
|
||||
async SendTextMsg(to: string, msg: string): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const request = new TextMsg();
|
||||
request.setReceiver(to);
|
||||
request.setMsg(msg);
|
||||
|
||||
this.client.rpcSendTextMsg(request, (err, ret) => {
|
||||
if (err) reject(err);
|
||||
else resolve(ret.getStatus() !== 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import { Library, Method, never } from 'ffi-decorators';
|
||||
|
||||
@Library({ libPath: './sdk.dll' })
|
||||
export class SDK {
|
||||
@Method({ types: ['int', []] })
|
||||
WxInitSDK(): number {
|
||||
return never();
|
||||
}
|
||||
|
||||
@Method({ types: ['int', []] })
|
||||
WxDestroySDK(): number {
|
||||
return never();
|
||||
}
|
||||
}
|
||||
|
||||
// Todo: inject spy.dll directly in ts code.
|
||||
Reference in New Issue
Block a user