mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-02 19:52:35 +00:00
mcp: add plain QR code text
This commit is contained in:
1 parent
a9d5ba3be1
commit
87f280c3a3
4 files changed
+76
-63
No files matched your search
@@ -1,9 +1,9 @@
|
||||
import {ChatEvent, T} from "@simplex-chat/types"
|
||||
import * as qrcode from "qrcode"
|
||||
import {api} from "simplex-chat"
|
||||
import {Action, ActionHooks, Actions, ConnectAction, Subject, ToolError, leaveVerbs, subjectText, summary} from "./actions"
|
||||
import {ExitError, chatErrorOf, log, logError, oneLine, shellWord} from "./log"
|
||||
import {chatNumber, chatRef, isChatRef, utcMinutes, validName, viewName} from "./messages"
|
||||
import {qrCodeText} from "./qr"
|
||||
import {AgentTools, chatSubject, isActive, isConnected, memberCurrent} from "./tools"
|
||||
|
||||
export type QrCodeSize = "small" | "large"
|
||||
@@ -27,15 +27,6 @@ const truncationMark = "…"
|
||||
const longestError = "€".repeat(600)
|
||||
const greeting = "I'm your agent. I ask here before I connect or send."
|
||||
const noOwners = "No owners in the agent control chat."
|
||||
const qrCodeHint = "If the QR code does not scan, restart with --qr-code large, or paste the link into the app."
|
||||
const qrMargin = 2
|
||||
const darkCell = "\x1b[40m "
|
||||
const lightCell = "\x1b[47m "
|
||||
const darkHalfCell = "\x1b[40m "
|
||||
const lightHalfCell = "\x1b[47m "
|
||||
const darkTopCell = "\x1b[37;40m▄"
|
||||
const darkBottomCell = "\x1b[30;47m▄"
|
||||
const resetColors = "\x1b[0m"
|
||||
|
||||
export interface ControlData {
|
||||
control: true
|
||||
@@ -318,9 +309,8 @@ export class Control implements Pick<ActionHooks, "refusal" | "saveCounter" | "p
|
||||
await this.deleteLink()
|
||||
const link = await this.chat.apiCreateGroupLink(this.groupId, role)
|
||||
log("Join the agent control chat in the SimpleX app. The link works once:")
|
||||
log(qrCodeText(link, this.options.qrCode))
|
||||
log(qrCodeText(link, "ansi"))
|
||||
log(link)
|
||||
if (this.options.qrCode === "small") log(qrCodeHint)
|
||||
log(role === T.GroupMemberRole.Observer
|
||||
? "The link adds an observer. Its ID and name are printed when it joins."
|
||||
: `Whoever joins by this link becomes an owner of "${this.options.displayName}" and approves its actions. To approve owners yourself, start with --owners.`)
|
||||
@@ -573,22 +563,6 @@ function ownerRequest({keyword, params}: Command): OwnerRequest | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function qrCodeText(link: string, size: QrCodeSize): string {
|
||||
const {modules} = qrcode.create(link, {errorCorrectionLevel: "L"})
|
||||
const positions = Array.from({length: modules.size + 2 * qrMargin}, (_, i) => i - qrMargin)
|
||||
const inside = (i: number): boolean => i >= 0 && i < modules.size
|
||||
const dark = (row: number, col: number): boolean => inside(row) && inside(col) && modules.get(row, col) !== 0
|
||||
const cell = (row: number, col: number): string => (dark(row, col) ? darkCell : lightCell)
|
||||
const halfCell = (top: boolean, bottom: boolean): string => {
|
||||
if (top === bottom) return top ? darkHalfCell : lightHalfCell
|
||||
return top ? darkTopCell : darkBottomCell
|
||||
}
|
||||
const lines = size === "large"
|
||||
? positions.map(row => positions.map(col => cell(row, col)).join(""))
|
||||
: positions.filter((_, i) => i % 2 === 0).map(row => positions.map(col => halfCell(dark(row, col), dark(row + 1, col))).join(""))
|
||||
return lines.map(line => line + resetColors).join("\n")
|
||||
}
|
||||
|
||||
function losesControl(event: ChatEvent): boolean {
|
||||
switch (event.type) {
|
||||
case "deletedMemberUser":
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as qrcode from "qrcode"
|
||||
|
||||
export type QrStyle = "ansi" | "plain"
|
||||
|
||||
const qrMargin = 2
|
||||
const darkHalfCell = "\x1b[40m "
|
||||
const lightHalfCell = "\x1b[47m "
|
||||
const darkTopCell = "\x1b[37;40m▄"
|
||||
const darkBottomCell = "\x1b[30;47m▄"
|
||||
const resetColors = "\x1b[0m"
|
||||
|
||||
export function qrCodeText(link: string, style: QrStyle): string {
|
||||
const {modules} = qrcode.create(link, {errorCorrectionLevel: "L"})
|
||||
const positions = Array.from({length: modules.size + 2 * qrMargin}, (_, i) => i - qrMargin)
|
||||
const inside = (i: number): boolean => i >= 0 && i < modules.size
|
||||
const dark = (row: number, col: number): boolean => inside(row) && inside(col) && modules.get(row, col) !== 0
|
||||
const cell = style === "ansi" ? ansiCell : plainCell
|
||||
const lines = positions.filter((_, i) => i % 2 === 0).map(row => positions.map(col => cell(dark(row, col), dark(row + 1, col))).join(""))
|
||||
return lines.map(line => (style === "ansi" ? line + resetColors : line)).join("\n")
|
||||
}
|
||||
|
||||
function ansiCell(top: boolean, bottom: boolean): string {
|
||||
if (top === bottom) return top ? darkHalfCell : lightHalfCell
|
||||
return top ? darkTopCell : darkBottomCell
|
||||
}
|
||||
|
||||
function plainCell(topDark: boolean, bottomDark: boolean): string {
|
||||
if (topDark && bottomDark) return " "
|
||||
if (topDark) return "▄"
|
||||
return bottomDark ? "▀" : "█"
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as qrcode from "qrcode"
|
||||
import {Action, Mode, NewAction, State} from "../../src/actions"
|
||||
import {cardText, qrCodeText} from "../../src/control"
|
||||
import {cardText} from "../../src/control"
|
||||
import {Changes, Collected} from "../../src/messages"
|
||||
|
||||
const expires = Date.UTC(2026, 9, 18, 9, 2)
|
||||
@@ -199,36 +198,3 @@ describe("leave cards", () => {
|
||||
expect(lines(action(group, "pending", {interrupted: true}))).toEqual(["I want to leave business ep (#2).", "Cancelled: the server stopped."])
|
||||
})
|
||||
})
|
||||
|
||||
describe("QR codes", () => {
|
||||
const link = "https://smp4.simplex.im/g#1ai9d8WS140_IxvPDYWLigJNZU-z7GsDUhp0JoJ7kbU"
|
||||
const cells = (line: string): string[] => [...line.matchAll(/\x1b\[[0-9;]+m[^\x1b]+/g)].map(([cell]) => cell)
|
||||
const visible = (line: string): string => line.replace(/\x1b\[[0-9;]*m/g, "")
|
||||
|
||||
it("draws each module as two cells in large", () => {
|
||||
const lines = qrCodeText(link, "large").split("\n")
|
||||
expect(lines).toHaveLength(37)
|
||||
for (const line of lines) {
|
||||
expect(visible(line)).toHaveLength(74)
|
||||
expect(line.endsWith("\x1b[0m")).toBe(true)
|
||||
}
|
||||
for (const line of [...lines.slice(0, 2), ...lines.slice(-2)]) expect(cells(line)).toEqual(Array(37).fill("\x1b[47m "))
|
||||
expect(cells(lines[2]).slice(0, 10)).toEqual([...Array(2).fill("\x1b[47m "), ...Array(7).fill("\x1b[40m "), "\x1b[47m "])
|
||||
})
|
||||
|
||||
it("draws two module rows per line with lower half blocks in small", () => {
|
||||
const lines = qrCodeText(link, "small").split("\n")
|
||||
expect(lines).toHaveLength(19)
|
||||
for (const line of lines) {
|
||||
expect(visible(line)).toHaveLength(37)
|
||||
expect(line.endsWith("\x1b[0m")).toBe(true)
|
||||
expect([...cells(line).slice(0, 2), ...cells(line).slice(-2)]).toEqual(Array(4).fill("\x1b[47m "))
|
||||
}
|
||||
for (const line of [lines[0], lines.at(-1)!]) expect(cells(line)).toEqual(Array(37).fill("\x1b[47m "))
|
||||
expect(cells(lines[1]).slice(2, 4)).toEqual(["\x1b[40m ", "\x1b[37;40m▄"])
|
||||
const {modules} = qrcode.create(link, {errorCorrectionLevel: "L"})
|
||||
const row = Array.from({length: 16}, (_, i) => 2 * i).find(r => Array.from({length: modules.size}, (_, c) => c).some(c => !modules.get(r, c) && modules.get(r + 1, c)))!
|
||||
const col = Array.from({length: modules.size}, (_, c) => c).find(c => !modules.get(row, c) && modules.get(row + 1, c))!
|
||||
expect(cells(lines[row / 2 + 1])[col + 2]).toBe("\x1b[30;47m▄")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as qrcode from "qrcode"
|
||||
import {qrCodeText} from "../../src/qr"
|
||||
|
||||
describe("QR codes", () => {
|
||||
const link = "https://smp4.simplex.im/g#1ai9d8WS140_IxvPDYWLigJNZU-z7GsDUhp0JoJ7kbU"
|
||||
const cells = (line: string): string[] => [...line.matchAll(/\x1b\[[0-9;]+m[^\x1b]+/g)].map(([cell]) => cell)
|
||||
const visible = (line: string): string => line.replace(/\x1b\[[0-9;]*m/g, "")
|
||||
|
||||
it("draws two module rows per line with lower half blocks in ansi", () => {
|
||||
const lines = qrCodeText(link, "ansi").split("\n")
|
||||
expect(lines).toHaveLength(19)
|
||||
for (const line of lines) {
|
||||
expect(visible(line)).toHaveLength(37)
|
||||
expect(line.endsWith("\x1b[0m")).toBe(true)
|
||||
expect([...cells(line).slice(0, 2), ...cells(line).slice(-2)]).toEqual(Array(4).fill("\x1b[47m "))
|
||||
}
|
||||
for (const line of [lines[0], lines.at(-1)!]) expect(cells(line)).toEqual(Array(37).fill("\x1b[47m "))
|
||||
expect(cells(lines[1]).slice(2, 4)).toEqual(["\x1b[40m ", "\x1b[37;40m▄"])
|
||||
const {modules} = qrcode.create(link, {errorCorrectionLevel: "L"})
|
||||
const row = Array.from({length: 16}, (_, i) => 2 * i).find(r => Array.from({length: modules.size}, (_, c) => c).some(c => !modules.get(r, c) && modules.get(r + 1, c)))!
|
||||
const col = Array.from({length: modules.size}, (_, c) => c).find(c => !modules.get(row, c) && modules.get(row + 1, c))!
|
||||
expect(cells(lines[row / 2 + 1])[col + 2]).toBe("\x1b[30;47m▄")
|
||||
})
|
||||
|
||||
it("draws light modules filled with plain half blocks, two module rows per line", () => {
|
||||
const link = "https://smp4.simplex.im/g#test"
|
||||
const {modules} = qrcode.create(link, {errorCorrectionLevel: "L"})
|
||||
const lines = qrCodeText(link, "plain").split("\n")
|
||||
const width = modules.size + 4
|
||||
expect(lines).toHaveLength(Math.ceil(width / 2))
|
||||
for (const line of lines) expect([...line]).toHaveLength(width)
|
||||
expect(qrCodeText(link, "plain")).toMatch(/^[█▀▄ \n]+$/)
|
||||
expect(lines[0]).toBe("█".repeat(width))
|
||||
const light = (row: number, col: number): boolean => row < 0 || col < 0 || row >= modules.size || col >= modules.size || modules.get(row, col) === 0
|
||||
const expected = [...lines[1]].map((_, i) => {
|
||||
const top = light(0, i - 2)
|
||||
const bottom = light(1, i - 2)
|
||||
return top && bottom ? "█" : top ? "▀" : bottom ? "▄" : " "
|
||||
})
|
||||
expect([...lines[1]]).toEqual(expected)
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user