mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 20:08:34 +00:00
refactor
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import {test, expect} from "vitest"
|
||||
import {mkdtempSync, rmSync} from "fs"
|
||||
import {tmpdir} from "os"
|
||||
import {join} from "path"
|
||||
import {CEvt, T} from "@simplex-chat/types"
|
||||
import {api, util} from "simplex-chat"
|
||||
import {runCalculatorBot} from "./src/calculatorBot.js"
|
||||
|
||||
const isCalculator = (display: string) => (ci: T.AChatItem) => ci.chatItem.meta.itemText.startsWith(`*${display}*\n`)
|
||||
|
||||
const calculatorShows = (display: string) => ({chatItems}: CEvt.NewChatItems) => chatItems.some(isCalculator(display))
|
||||
|
||||
const hasText = (text: string) => ({chatItems}: CEvt.NewChatItems) =>
|
||||
chatItems.some(ci => ci.chatItem.meta.itemText === text)
|
||||
|
||||
test("calculator in business chat (uses preset servers)", async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "calculator-bot-"))
|
||||
const [calculator, _botUser, address] = await runCalculatorBot({type: "sqlite", filePrefix: join(dir, "bot")})
|
||||
const alice = await api.ChatApi.init({type: "sqlite", filePrefix: join(dir, "alice")})
|
||||
const aliceUser = await alice.apiCreateActiveUser({displayName: "alice", fullName: ""})
|
||||
await alice.startChat()
|
||||
try {
|
||||
const [_plan, link] = await alice.apiConnectPlan(aliceUser.userId, util.contactAddressStr(address!.connLinkContact))
|
||||
const firstCalculator = alice.wait("newChatItems", calculatorShows("0"), 30000)
|
||||
await alice.apiConnect(aliceUser.userId, false, link)
|
||||
const calculatorItem = (await firstCalculator)?.chatItems.find(isCalculator("0"))
|
||||
expect(calculatorItem?.chatInfo.type).toBe(T.ChatType.Group)
|
||||
const groupId = (calculatorItem!.chatInfo as T.ChatInfo.Group).groupInfo.groupId
|
||||
|
||||
const logLine = alice.wait("newChatItems", hasText("2 + 2 = 4"), 30000)
|
||||
const four = alice.wait("newChatItems", calculatorShows("4"), 30000)
|
||||
for (const key of ["/2", "/+", "/2", "/="]) await alice.apiSendTextMessage([T.ChatType.Group, groupId], key)
|
||||
expect(await logLine).toBeDefined()
|
||||
expect(await four).toBeDefined()
|
||||
|
||||
const forty = alice.wait("newChatItems", calculatorShows("40"), 30000)
|
||||
await alice.apiSendTextMessage([T.ChatType.Group, groupId], "12 × 3 + 4")
|
||||
expect(await forty).toBeDefined()
|
||||
} finally {
|
||||
await alice.close()
|
||||
await calculator.close()
|
||||
rmSync(dir, {recursive: true, force: true})
|
||||
}
|
||||
}, 120000)
|
||||
@@ -0,0 +1,89 @@
|
||||
import {T} from "@simplex-chat/types"
|
||||
import {api, bot, util} from "simplex-chat"
|
||||
import {Calc, calculatorText, evaluate, initialCalc, keyWord, keypad, press, textKeys} from "./calculator.js"
|
||||
|
||||
const anyTextCommandsVersion = 21
|
||||
const idleMinutes = 10
|
||||
|
||||
const welcomeMessage = `Tap the keys, or send an expression like 12 × 3 + 4.\nKeys are applied left to right, as on a pocket calculator.\nThe calculator turns off after ${idleMinutes} minutes.`
|
||||
const offText = "*Off*\nTap /calc or send an expression."
|
||||
const hint = "Send a number or an expression like 12 × 3 + 4."
|
||||
|
||||
interface Session {
|
||||
calc: Calc
|
||||
itemId: number
|
||||
timer: NodeJS.Timeout
|
||||
}
|
||||
|
||||
const sessions = new Map<number, Session>()
|
||||
|
||||
function groupSender({chatInfo, chatItem}: T.AChatItem): {groupId: number, member: T.GroupMember} | undefined {
|
||||
return chatInfo.type === "group" && chatItem.chatDir.type === "groupRcv"
|
||||
? {groupId: chatInfo.groupInfo.groupId, member: chatItem.chatDir.groupMember}
|
||||
: undefined
|
||||
}
|
||||
|
||||
function currentCalc(groupId: number): Calc {
|
||||
return sessions.get(groupId)?.calc ?? initialCalc
|
||||
}
|
||||
|
||||
async function showCalculator(chat: api.ChatApi, groupId: number, member: T.GroupMember, calc: Calc): Promise<void> {
|
||||
const symbolKeys = member.memberChatVRange.maxVersion >= anyTextCommandsVersion
|
||||
const [sent] = await chat.apiSendTextMessage([T.ChatType.Group, groupId], calculatorText(calc, symbolKeys))
|
||||
const itemId = sent.chatItem.meta.itemId
|
||||
const previous = sessions.get(groupId)
|
||||
sessions.set(groupId, {calc, itemId, timer: setTimeout(() => turnOff(chat, groupId, itemId), idleMinutes * 60_000)})
|
||||
if (previous) {
|
||||
clearTimeout(previous.timer)
|
||||
await chat.apiDeleteChatItems(T.ChatType.Group, groupId, [previous.itemId], T.CIDeleteMode.Broadcast)
|
||||
}
|
||||
}
|
||||
|
||||
function turnOff(chat: api.ChatApi, groupId: number, itemId: number): void {
|
||||
sessions.delete(groupId)
|
||||
chat.apiUpdateChatItem(T.ChatType.Group, groupId, itemId, {type: "text", text: offText}, false)
|
||||
.catch(e => console.log("error turning calculator off", e))
|
||||
}
|
||||
|
||||
function tapCommand(update: (calc: Calc) => [Calc, string?]) {
|
||||
return async (ci: T.AChatItem, _command: util.BotCommand, chat: api.ChatApi): Promise<void> => {
|
||||
const sender = groupSender(ci)
|
||||
if (!sender) return
|
||||
await chat.apiDeleteMemberChatItem(sender.groupId, [ci.chatItem.meta.itemId])
|
||||
const [calc, logLine] = update(currentCalc(sender.groupId))
|
||||
if (logLine) await chat.apiSendTextMessage([T.ChatType.Group, sender.groupId], logLine)
|
||||
await showCalculator(chat, sender.groupId, sender.member, calc)
|
||||
}
|
||||
}
|
||||
|
||||
const keyCommands = Object.fromEntries(
|
||||
keypad.flat().flatMap(key => [key, keyWord(key)].map(keyword => [keyword, tapCommand(calc => press(calc, key))]))
|
||||
)
|
||||
|
||||
async function onMessage(ci: T.AChatItem, content: T.MsgContent, chat: api.ChatApi): Promise<void> {
|
||||
const sender = groupSender(ci)
|
||||
if (!sender || content.type !== "text") return
|
||||
const keys = textKeys(content.text)
|
||||
if (keys) await showCalculator(chat, sender.groupId, sender.member, evaluate(keys))
|
||||
else await chat.apiSendTextReply(ci, hint)
|
||||
}
|
||||
|
||||
export function runCalculatorBot(dbOpts: bot.BotDbOpts): Promise<[api.ChatApi, T.User, T.UserContactLink | undefined]> {
|
||||
return bot.run({
|
||||
profile: {displayName: "SimpleX Calculator", fullName: "", preferences: {fullDelete: {allow: T.FeatureAllowed.Yes}}},
|
||||
dbOpts,
|
||||
options: {
|
||||
addressSettings: {businessAddress: true, welcomeMessage},
|
||||
commands: [{type: "command", keyword: "calc", label: "Show calculator"}],
|
||||
},
|
||||
onMessage,
|
||||
onCommands: {
|
||||
...keyCommands,
|
||||
calc: tapCommand(calc => [calc]),
|
||||
"": async (ci, _command, chat) => { await chat.apiSendTextReply(ci, hint) },
|
||||
},
|
||||
events: {
|
||||
joinedGroupMember: ({groupInfo, member}, chat) => showCalculator(chat, groupInfo.groupId, member, initialCalc),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,93 +1,9 @@
|
||||
import {mkdirSync} from "fs"
|
||||
import {T} from "@simplex-chat/types"
|
||||
import {api, bot, util} from "simplex-chat"
|
||||
import {Calc, calculatorText, evaluate, initialCalc, keyWord, keypad, press, textKeys} from "./calculator.js"
|
||||
|
||||
const anyTextCommandsVersion = 21
|
||||
const idleMinutes = 10
|
||||
|
||||
const welcomeMessage = `Tap the keys, or send an expression like 12 × 3 + 4.\nKeys are applied left to right, as on a pocket calculator.\nThe calculator turns off after ${idleMinutes} minutes.`
|
||||
const offText = "*Off*\nTap /calc or send an expression."
|
||||
const hint = "Send a number or an expression like 12 × 3 + 4."
|
||||
|
||||
interface Session {
|
||||
calc: Calc
|
||||
itemId: number
|
||||
timer: NodeJS.Timeout
|
||||
}
|
||||
|
||||
const sessions = new Map<number, Session>()
|
||||
|
||||
function groupSender({chatInfo, chatItem}: T.AChatItem): {groupId: number, member: T.GroupMember} | undefined {
|
||||
return chatInfo.type === "group" && chatItem.chatDir.type === "groupRcv"
|
||||
? {groupId: chatInfo.groupInfo.groupId, member: chatItem.chatDir.groupMember}
|
||||
: undefined
|
||||
}
|
||||
|
||||
function currentCalc(groupId: number): Calc {
|
||||
return sessions.get(groupId)?.calc ?? initialCalc
|
||||
}
|
||||
|
||||
async function showCalculator(chat: api.ChatApi, groupId: number, member: T.GroupMember, calc: Calc): Promise<void> {
|
||||
const symbolKeys = member.memberChatVRange.maxVersion >= anyTextCommandsVersion
|
||||
const [sent] = await chat.apiSendTextMessage([T.ChatType.Group, groupId], calculatorText(calc, symbolKeys))
|
||||
const itemId = sent.chatItem.meta.itemId
|
||||
const previous = sessions.get(groupId)
|
||||
sessions.set(groupId, {calc, itemId, timer: setTimeout(() => turnOff(chat, groupId, itemId), idleMinutes * 60_000)})
|
||||
if (previous) {
|
||||
clearTimeout(previous.timer)
|
||||
await chat.apiDeleteChatItems(T.ChatType.Group, groupId, [previous.itemId], T.CIDeleteMode.Broadcast)
|
||||
}
|
||||
}
|
||||
|
||||
function turnOff(chat: api.ChatApi, groupId: number, itemId: number): void {
|
||||
sessions.delete(groupId)
|
||||
chat.apiUpdateChatItem(T.ChatType.Group, groupId, itemId, {type: "text", text: offText}, false)
|
||||
.catch(e => console.log("error turning calculator off", e))
|
||||
}
|
||||
|
||||
function tapCommand(update: (calc: Calc) => [Calc, string?]) {
|
||||
return async (ci: T.AChatItem, _command: util.BotCommand, chat: api.ChatApi): Promise<void> => {
|
||||
const sender = groupSender(ci)
|
||||
if (!sender) return
|
||||
await chat.apiDeleteMemberChatItem(sender.groupId, [ci.chatItem.meta.itemId])
|
||||
const [calc, logLine] = update(currentCalc(sender.groupId))
|
||||
if (logLine) await chat.apiSendTextMessage([T.ChatType.Group, sender.groupId], logLine)
|
||||
await showCalculator(chat, sender.groupId, sender.member, calc)
|
||||
}
|
||||
}
|
||||
|
||||
const keyCommands = Object.fromEntries(
|
||||
keypad.flat().flatMap(key => [key, keyWord(key)].map(keyword => [keyword, tapCommand(calc => press(calc, key))]))
|
||||
)
|
||||
|
||||
async function onMessage(ci: T.AChatItem, content: T.MsgContent, chat: api.ChatApi): Promise<void> {
|
||||
const sender = groupSender(ci)
|
||||
if (!sender || content.type !== "text") return
|
||||
const keys = textKeys(content.text)
|
||||
if (keys) await showCalculator(chat, sender.groupId, sender.member, evaluate(keys))
|
||||
else await chat.apiSendTextReply(ci, hint)
|
||||
}
|
||||
import {runCalculatorBot} from "./calculatorBot.js"
|
||||
|
||||
mkdirSync("data", {recursive: true})
|
||||
|
||||
bot.run({
|
||||
profile: {displayName: "SimpleX Calculator", fullName: "", preferences: {fullDelete: {allow: T.FeatureAllowed.Yes}}},
|
||||
dbOpts: {type: "sqlite", filePrefix: "./data/calculator"},
|
||||
options: {
|
||||
addressSettings: {businessAddress: true, welcomeMessage},
|
||||
commands: [{type: "command", keyword: "calc", label: "Show calculator"}],
|
||||
},
|
||||
onMessage,
|
||||
onCommands: {
|
||||
...keyCommands,
|
||||
calc: tapCommand(calc => [calc]),
|
||||
"": async (ci, _command, chat) => { await chat.apiSendTextReply(ci, hint) },
|
||||
},
|
||||
events: {
|
||||
joinedGroupMember: ({groupInfo, member}, chat) => showCalculator(chat, groupInfo.groupId, member, initialCalc),
|
||||
},
|
||||
}).catch(e => {
|
||||
runCalculatorBot({type: "sqlite", filePrefix: "./data/calculator"}).catch(e => {
|
||||
console.log("fatal error", e)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
@@ -413,6 +413,10 @@ textWithCommands = describe "text with commands" do
|
||||
"/√ /÷" <==> command "√" "/√" <> " " <> command "÷" "/÷"
|
||||
"send /+." <==> "send " <> command "+" "/+" <> "."
|
||||
"/'+'" <==> command "+" "/'+'"
|
||||
it "calculator keys" do
|
||||
"/C /± /% /÷" <==> command "C" "/C" <> " " <> command "±" "/±" <> " " <> command "%" "/%" <> " " <> command "÷" "/÷"
|
||||
"/√ /0 /. /=" <==> command "√" "/√" <> " " <> command "0" "/0" <> " " <> command "." "/." <> " " <> command "=" "/="
|
||||
"/C `\160\160\160\160`/neg `\160\160`" <==> command "C" "/C" <> " " <> markdown Snippet "\160\160\160\160" <> command "neg" "/neg" <> " " <> markdown Snippet "\160\160"
|
||||
it "ignored as markdown" $ do
|
||||
"send /'filter 1" <==> "send /'filter 1"
|
||||
"send /help /'filter 1" <==> "send " <> command "help" "/help" <> " /'filter 1"
|
||||
@@ -477,6 +481,7 @@ multilineMarkdownList = describe "multiline markdown" do
|
||||
it "command markdown" do
|
||||
"/link 1" <<==>> [command' "link 1" "/link 1"]
|
||||
" /link 1" <<==>> [command' "link 1" " /link 1"]
|
||||
"*0*\n/7 /+" <<==>> [FormattedText (Just Bold) "0", "\n", command' "7" "/7", " ", command' "+" "/+"]
|
||||
|
||||
testSanitizeUri :: Spec
|
||||
testSanitizeUri = describe "sanitizeUri" $ do
|
||||
|
||||
Reference in New Issue
Block a user