nonebot-plugin-receipts 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/PKG-INFO +29 -5
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/README.md +28 -4
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/__init__.py +3 -2
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/command_handlers.py +37 -1
- nonebot_plugin_receipts-0.2.4/nonebot_plugin_receipts/config.py +66 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts.egg-info/PKG-INFO +29 -5
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts.egg-info/SOURCES.txt +1 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/pyproject.toml +1 -1
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/tests/test_command_handlers.py +130 -6
- nonebot_plugin_receipts-0.2.4/tests/test_config.py +69 -0
- nonebot_plugin_receipts-0.2.2/nonebot_plugin_receipts/config.py +0 -20
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/escpos_backend.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/raster_backend.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/render_blocks.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/render_types.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/render_utils.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/renderer.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/spooler.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/template.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/text_markup.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts.egg-info/dependency_links.txt +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts.egg-info/requires.txt +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts.egg-info/top_level.txt +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/setup.cfg +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/tests/test_render_blocks.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/tests/test_renderer.py +0 -0
- {nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/tests/test_text_markup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot_plugin_receipts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: NoneBot2 plugin for sending text and image messages to receipts-spooler
|
|
5
5
|
Project-URL: homepage, https://github.com/Effect-Wei/nonebot_plugin_receipts
|
|
6
6
|
Project-URL: repository, https://github.com/Effect-Wei/nonebot_plugin_receipts
|
|
@@ -23,7 +23,7 @@ Requires-Dist: ruff; extra == "dev"
|
|
|
23
23
|
|
|
24
24
|
## 功能
|
|
25
25
|
|
|
26
|
-
- 提供 `ticket
|
|
26
|
+
- 提供 `ticket`、`receipt` 指令,二者互为别名
|
|
27
27
|
- 支持命令后直接附带文本和图片
|
|
28
28
|
- 如果命令后没有内容,会继续追问下一条消息
|
|
29
29
|
- 将文本和图片合成为一张热敏小票图,再编码为 ESC/POS 栅格打印命令
|
|
@@ -62,6 +62,8 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
62
62
|
| `RECEIPT_SESSION_TIMEOUT_SECONDS` | `120` | 等待用户补发打印内容的超时时间(秒) |
|
|
63
63
|
| `RECEIPT_FEED_LINES` | `4` | 打印后走纸行数 |
|
|
64
64
|
| `RECEIPT_ENABLE_CUT` | `true` | 是否附加切纸命令 |
|
|
65
|
+
| `RECEIPT_ALLOWED_USER_IDS` | 空 | 允许提交打印任务的 QQ 号白名单 |
|
|
66
|
+
| `RECEIPT_ALLOWED_GROUP_IDS` | 空 | 允许提交打印任务的 QQ 群号白名单 |
|
|
65
67
|
|
|
66
68
|
### 渲染模式
|
|
67
69
|
|
|
@@ -70,6 +72,28 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
70
72
|
|
|
71
73
|
`hybrid` 模式下,中文是否正常输出取决于打印机对中英文字符集的支持情况。
|
|
72
74
|
|
|
75
|
+
### 白名单
|
|
76
|
+
|
|
77
|
+
可以通过以下配置限制谁能提交打印任务:
|
|
78
|
+
|
|
79
|
+
- `RECEIPT_ALLOWED_USER_IDS`:允许提交的 QQ 号白名单
|
|
80
|
+
- `RECEIPT_ALLOWED_GROUP_IDS`:允许提交的 QQ 群号白名单
|
|
81
|
+
|
|
82
|
+
两个配置都为空时,默认允许所有会话使用。
|
|
83
|
+
只要命中任意一个条件就会放行:
|
|
84
|
+
|
|
85
|
+
- 发送者 QQ 号在 `RECEIPT_ALLOWED_USER_IDS` 中
|
|
86
|
+
- 当前群号在 `RECEIPT_ALLOWED_GROUP_IDS` 中
|
|
87
|
+
|
|
88
|
+
未命中白名单的消息会被直接忽略,不会回复任何提示。
|
|
89
|
+
|
|
90
|
+
环境变量可以写成逗号分隔,例如:
|
|
91
|
+
|
|
92
|
+
```env
|
|
93
|
+
RECEIPT_ALLOWED_USER_IDS=123456,234567
|
|
94
|
+
RECEIPT_ALLOWED_GROUP_IDS=345678,456789
|
|
95
|
+
```
|
|
96
|
+
|
|
73
97
|
### 渲染模板
|
|
74
98
|
|
|
75
99
|
如果你想单独调整小票布局,可以配置 `RECEIPT_TEMPLATE_PATH` 指向一个 JSON 文件。项目根目录里附带了一个示例文件 [`receipt_template.example.json`](receipt_template.example.json)。
|
|
@@ -113,18 +137,18 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
113
137
|
直接在命令后附带内容:
|
|
114
138
|
|
|
115
139
|
```text
|
|
116
|
-
|
|
140
|
+
/ticket 今晚睡大觉
|
|
117
141
|
```
|
|
118
142
|
|
|
119
143
|
或者:
|
|
120
144
|
|
|
121
|
-
1. 发送
|
|
145
|
+
1. 发送 `/ticket` 或 `/receipt`
|
|
122
146
|
2. 按提示继续发送一条包含文本、图片或两者混合的消息
|
|
123
147
|
|
|
124
148
|
插件会把纯文本和图片消息段一起打印,并在提交成功后回复当前队列长度。
|
|
125
149
|
|
|
126
150
|
如果命令后没有直接附带内容,插件会提示用户继续发送,并明确说明多久后超时。
|
|
127
|
-
如果用户在该时间窗口后才继续回复,插件会提示“已超时”,并直接结束本次等待;此时需要重新发送
|
|
151
|
+
如果用户在该时间窗口后才继续回复,插件会提示“已超时”,并直接结束本次等待;此时需要重新发送 `/ticket` 或 `/receipt` 开始新的打印流程。
|
|
128
152
|
|
|
129
153
|
`RECEIPT_SESSION_TIMEOUT_SECONDS` 的实际生效时间不会超过 NoneBot 全局 `SESSION_EXPIRE_TIMEOUT`,建议两者保持一致或让前者更小。
|
|
130
154
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
|
-
- 提供 `ticket
|
|
9
|
+
- 提供 `ticket`、`receipt` 指令,二者互为别名
|
|
10
10
|
- 支持命令后直接附带文本和图片
|
|
11
11
|
- 如果命令后没有内容,会继续追问下一条消息
|
|
12
12
|
- 将文本和图片合成为一张热敏小票图,再编码为 ESC/POS 栅格打印命令
|
|
@@ -45,6 +45,8 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
45
45
|
| `RECEIPT_SESSION_TIMEOUT_SECONDS` | `120` | 等待用户补发打印内容的超时时间(秒) |
|
|
46
46
|
| `RECEIPT_FEED_LINES` | `4` | 打印后走纸行数 |
|
|
47
47
|
| `RECEIPT_ENABLE_CUT` | `true` | 是否附加切纸命令 |
|
|
48
|
+
| `RECEIPT_ALLOWED_USER_IDS` | 空 | 允许提交打印任务的 QQ 号白名单 |
|
|
49
|
+
| `RECEIPT_ALLOWED_GROUP_IDS` | 空 | 允许提交打印任务的 QQ 群号白名单 |
|
|
48
50
|
|
|
49
51
|
### 渲染模式
|
|
50
52
|
|
|
@@ -53,6 +55,28 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
53
55
|
|
|
54
56
|
`hybrid` 模式下,中文是否正常输出取决于打印机对中英文字符集的支持情况。
|
|
55
57
|
|
|
58
|
+
### 白名单
|
|
59
|
+
|
|
60
|
+
可以通过以下配置限制谁能提交打印任务:
|
|
61
|
+
|
|
62
|
+
- `RECEIPT_ALLOWED_USER_IDS`:允许提交的 QQ 号白名单
|
|
63
|
+
- `RECEIPT_ALLOWED_GROUP_IDS`:允许提交的 QQ 群号白名单
|
|
64
|
+
|
|
65
|
+
两个配置都为空时,默认允许所有会话使用。
|
|
66
|
+
只要命中任意一个条件就会放行:
|
|
67
|
+
|
|
68
|
+
- 发送者 QQ 号在 `RECEIPT_ALLOWED_USER_IDS` 中
|
|
69
|
+
- 当前群号在 `RECEIPT_ALLOWED_GROUP_IDS` 中
|
|
70
|
+
|
|
71
|
+
未命中白名单的消息会被直接忽略,不会回复任何提示。
|
|
72
|
+
|
|
73
|
+
环境变量可以写成逗号分隔,例如:
|
|
74
|
+
|
|
75
|
+
```env
|
|
76
|
+
RECEIPT_ALLOWED_USER_IDS=123456,234567
|
|
77
|
+
RECEIPT_ALLOWED_GROUP_IDS=345678,456789
|
|
78
|
+
```
|
|
79
|
+
|
|
56
80
|
### 渲染模板
|
|
57
81
|
|
|
58
82
|
如果你想单独调整小票布局,可以配置 `RECEIPT_TEMPLATE_PATH` 指向一个 JSON 文件。项目根目录里附带了一个示例文件 [`receipt_template.example.json`](receipt_template.example.json)。
|
|
@@ -96,18 +120,18 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
96
120
|
直接在命令后附带内容:
|
|
97
121
|
|
|
98
122
|
```text
|
|
99
|
-
|
|
123
|
+
/ticket 今晚睡大觉
|
|
100
124
|
```
|
|
101
125
|
|
|
102
126
|
或者:
|
|
103
127
|
|
|
104
|
-
1. 发送
|
|
128
|
+
1. 发送 `/ticket` 或 `/receipt`
|
|
105
129
|
2. 按提示继续发送一条包含文本、图片或两者混合的消息
|
|
106
130
|
|
|
107
131
|
插件会把纯文本和图片消息段一起打印,并在提交成功后回复当前队列长度。
|
|
108
132
|
|
|
109
133
|
如果命令后没有直接附带内容,插件会提示用户继续发送,并明确说明多久后超时。
|
|
110
|
-
如果用户在该时间窗口后才继续回复,插件会提示“已超时”,并直接结束本次等待;此时需要重新发送
|
|
134
|
+
如果用户在该时间窗口后才继续回复,插件会提示“已超时”,并直接结束本次等待;此时需要重新发送 `/ticket` 或 `/receipt` 开始新的打印流程。
|
|
111
135
|
|
|
112
136
|
`RECEIPT_SESSION_TIMEOUT_SECONDS` 的实际生效时间不会超过 NoneBot 全局 `SESSION_EXPIRE_TIMEOUT`,建议两者保持一致或让前者更小。
|
|
113
137
|
|
{nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/__init__.py
RENAMED
|
@@ -8,7 +8,8 @@ __plugin_meta__ = PluginMetadata(
|
|
|
8
8
|
name="小票打印",
|
|
9
9
|
description="将发送给 Bot 的文本和图片消息通过 receipts-spooler 打印",
|
|
10
10
|
usage=(
|
|
11
|
-
"发送
|
|
11
|
+
"发送 /ticket 或 /receipt 后附带文本/图片,"
|
|
12
|
+
"或单独发送命令后按提示再发送一条要打印的消息。"
|
|
12
13
|
),
|
|
13
14
|
type="application",
|
|
14
15
|
config=Config,
|
|
@@ -19,7 +20,7 @@ __plugin_meta__ = PluginMetadata(
|
|
|
19
20
|
try:
|
|
20
21
|
ReceiptPrint = on_command(
|
|
21
22
|
"ticket",
|
|
22
|
-
aliases={"
|
|
23
|
+
aliases={"ticket", "receipt"},
|
|
23
24
|
block=True,
|
|
24
25
|
priority=10,
|
|
25
26
|
)
|
|
@@ -4,6 +4,7 @@ from time import monotonic
|
|
|
4
4
|
|
|
5
5
|
from nonebot import get_driver, get_plugin_config
|
|
6
6
|
from nonebot.adapters.onebot.v11 import Message, MessageEvent # noqa: TC002
|
|
7
|
+
from nonebot.exception import FinishedException
|
|
7
8
|
from nonebot.matcher import Matcher # noqa: TC002
|
|
8
9
|
from nonebot.params import Arg, CommandArg
|
|
9
10
|
|
|
@@ -20,8 +21,11 @@ def register_receipt_handlers(receipt_print: type[Matcher]) -> None:
|
|
|
20
21
|
|
|
21
22
|
@receipt_print.handle()
|
|
22
23
|
async def handle_print_command(
|
|
23
|
-
matcher: Matcher, message: Message = CommandArg()
|
|
24
|
+
matcher: Matcher, event: MessageEvent, message: Message = CommandArg()
|
|
24
25
|
) -> None:
|
|
26
|
+
if not is_receipt_submission_allowed(event):
|
|
27
|
+
raise FinishedException
|
|
28
|
+
|
|
25
29
|
if has_printable_content(message):
|
|
26
30
|
matcher.set_arg("receipt_content", message)
|
|
27
31
|
return
|
|
@@ -38,6 +42,9 @@ def register_receipt_handlers(receipt_print: type[Matcher]) -> None:
|
|
|
38
42
|
event: MessageEvent,
|
|
39
43
|
receipt_content: Message = Arg("receipt_content"),
|
|
40
44
|
) -> None:
|
|
45
|
+
if not is_receipt_submission_allowed(event):
|
|
46
|
+
raise FinishedException
|
|
47
|
+
|
|
41
48
|
timeout_seconds = get_effective_session_timeout_seconds()
|
|
42
49
|
if is_receipt_timeout_expired(matcher):
|
|
43
50
|
await receipt_print.finish(build_timeout_receipt_prompt(timeout_seconds))
|
|
@@ -102,6 +109,35 @@ def get_runtime_config() -> Config:
|
|
|
102
109
|
return get_plugin_config(Config)
|
|
103
110
|
|
|
104
111
|
|
|
112
|
+
def is_receipt_submission_allowed(event: MessageEvent) -> bool:
|
|
113
|
+
"""Return whether the incoming event is allowed to submit a print job."""
|
|
114
|
+
config = get_runtime_config()
|
|
115
|
+
allowed_user_ids = set(config.allowed_user_ids)
|
|
116
|
+
allowed_group_ids = set(config.allowed_group_ids)
|
|
117
|
+
|
|
118
|
+
if not allowed_user_ids and not allowed_group_ids:
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
user_id = get_event_user_id(event)
|
|
122
|
+
if user_id and user_id in allowed_user_ids:
|
|
123
|
+
return True
|
|
124
|
+
|
|
125
|
+
group_id = get_event_group_id(event)
|
|
126
|
+
return bool(group_id and group_id in allowed_group_ids)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_event_user_id(event: MessageEvent) -> str:
|
|
130
|
+
"""Extract the normalized sender QQ id from an event."""
|
|
131
|
+
user_id = getattr(event, "user_id", None)
|
|
132
|
+
return str(user_id).strip() if user_id is not None else ""
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_event_group_id(event: MessageEvent) -> str:
|
|
136
|
+
"""Extract the normalized group QQ id from an event, if any."""
|
|
137
|
+
group_id = getattr(event, "group_id", None)
|
|
138
|
+
return str(group_id).strip() if group_id is not None else ""
|
|
139
|
+
|
|
140
|
+
|
|
105
141
|
def get_effective_session_timeout_seconds() -> int:
|
|
106
142
|
"""Return the effective wait timeout bounded by NoneBot's session timeout."""
|
|
107
143
|
plugin_timeout = get_runtime_config().receipt_session_timeout_seconds
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from typing import Any, Literal
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def normalize_whitelist_ids(value: Any) -> list[str]:
|
|
9
|
+
if value is None:
|
|
10
|
+
return []
|
|
11
|
+
|
|
12
|
+
if isinstance(value, str):
|
|
13
|
+
stripped = value.strip()
|
|
14
|
+
if not stripped:
|
|
15
|
+
return []
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
value = json.loads(stripped)
|
|
19
|
+
except json.JSONDecodeError:
|
|
20
|
+
return [
|
|
21
|
+
item
|
|
22
|
+
for part in re.split(r"[\s,,]+", stripped)
|
|
23
|
+
if (item := part.strip())
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
if isinstance(value, list | tuple | set | frozenset):
|
|
27
|
+
return [item for entry in value if (item := str(entry).strip())]
|
|
28
|
+
|
|
29
|
+
item = str(value).strip()
|
|
30
|
+
return [item] if item else []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Config(BaseModel):
|
|
34
|
+
receipts_spooler_url: str = "http://127.0.0.1:8000"
|
|
35
|
+
receipts_spooler_token: str | None = None
|
|
36
|
+
receipts_spooler_timeout: float = 5.0
|
|
37
|
+
receipt_image_fetch_timeout: float = 5.0
|
|
38
|
+
receipt_render_mode: Literal["raster", "hybrid"] = "raster"
|
|
39
|
+
receipt_printer_width: int = Field(default=576, ge=128, le=576)
|
|
40
|
+
receipt_template_path: str | None = None
|
|
41
|
+
receipt_font_path: str | None = None
|
|
42
|
+
receipt_font_size: int = Field(default=24, ge=12, le=96)
|
|
43
|
+
receipt_line_spacing: int = Field(default=6, ge=0, le=40)
|
|
44
|
+
receipt_section_gap: int = Field(default=6, ge=0, le=100)
|
|
45
|
+
receipt_session_timeout_seconds: int = Field(default=120, ge=10, le=3600)
|
|
46
|
+
receipt_feed_lines: int = Field(default=4, ge=0, le=10)
|
|
47
|
+
receipt_enable_cut: bool = True
|
|
48
|
+
receipt_allowed_user_ids: str | list[str] | None = Field(default_factory=list)
|
|
49
|
+
receipt_allowed_group_ids: str | list[str] | None = Field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
@field_validator(
|
|
52
|
+
"receipt_allowed_user_ids",
|
|
53
|
+
"receipt_allowed_group_ids",
|
|
54
|
+
mode="before",
|
|
55
|
+
)
|
|
56
|
+
@classmethod
|
|
57
|
+
def normalize_whitelist_ids(cls, value: Any) -> list[str]:
|
|
58
|
+
return normalize_whitelist_ids(value)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def allowed_user_ids(self) -> list[str]:
|
|
62
|
+
return normalize_whitelist_ids(self.receipt_allowed_user_ids)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def allowed_group_ids(self) -> list[str]:
|
|
66
|
+
return normalize_whitelist_ids(self.receipt_allowed_group_ids)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot_plugin_receipts
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: NoneBot2 plugin for sending text and image messages to receipts-spooler
|
|
5
5
|
Project-URL: homepage, https://github.com/Effect-Wei/nonebot_plugin_receipts
|
|
6
6
|
Project-URL: repository, https://github.com/Effect-Wei/nonebot_plugin_receipts
|
|
@@ -23,7 +23,7 @@ Requires-Dist: ruff; extra == "dev"
|
|
|
23
23
|
|
|
24
24
|
## 功能
|
|
25
25
|
|
|
26
|
-
- 提供 `ticket
|
|
26
|
+
- 提供 `ticket`、`receipt` 指令,二者互为别名
|
|
27
27
|
- 支持命令后直接附带文本和图片
|
|
28
28
|
- 如果命令后没有内容,会继续追问下一条消息
|
|
29
29
|
- 将文本和图片合成为一张热敏小票图,再编码为 ESC/POS 栅格打印命令
|
|
@@ -62,6 +62,8 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
62
62
|
| `RECEIPT_SESSION_TIMEOUT_SECONDS` | `120` | 等待用户补发打印内容的超时时间(秒) |
|
|
63
63
|
| `RECEIPT_FEED_LINES` | `4` | 打印后走纸行数 |
|
|
64
64
|
| `RECEIPT_ENABLE_CUT` | `true` | 是否附加切纸命令 |
|
|
65
|
+
| `RECEIPT_ALLOWED_USER_IDS` | 空 | 允许提交打印任务的 QQ 号白名单 |
|
|
66
|
+
| `RECEIPT_ALLOWED_GROUP_IDS` | 空 | 允许提交打印任务的 QQ 群号白名单 |
|
|
65
67
|
|
|
66
68
|
### 渲染模式
|
|
67
69
|
|
|
@@ -70,6 +72,28 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
70
72
|
|
|
71
73
|
`hybrid` 模式下,中文是否正常输出取决于打印机对中英文字符集的支持情况。
|
|
72
74
|
|
|
75
|
+
### 白名单
|
|
76
|
+
|
|
77
|
+
可以通过以下配置限制谁能提交打印任务:
|
|
78
|
+
|
|
79
|
+
- `RECEIPT_ALLOWED_USER_IDS`:允许提交的 QQ 号白名单
|
|
80
|
+
- `RECEIPT_ALLOWED_GROUP_IDS`:允许提交的 QQ 群号白名单
|
|
81
|
+
|
|
82
|
+
两个配置都为空时,默认允许所有会话使用。
|
|
83
|
+
只要命中任意一个条件就会放行:
|
|
84
|
+
|
|
85
|
+
- 发送者 QQ 号在 `RECEIPT_ALLOWED_USER_IDS` 中
|
|
86
|
+
- 当前群号在 `RECEIPT_ALLOWED_GROUP_IDS` 中
|
|
87
|
+
|
|
88
|
+
未命中白名单的消息会被直接忽略,不会回复任何提示。
|
|
89
|
+
|
|
90
|
+
环境变量可以写成逗号分隔,例如:
|
|
91
|
+
|
|
92
|
+
```env
|
|
93
|
+
RECEIPT_ALLOWED_USER_IDS=123456,234567
|
|
94
|
+
RECEIPT_ALLOWED_GROUP_IDS=345678,456789
|
|
95
|
+
```
|
|
96
|
+
|
|
73
97
|
### 渲染模板
|
|
74
98
|
|
|
75
99
|
如果你想单独调整小票布局,可以配置 `RECEIPT_TEMPLATE_PATH` 指向一个 JSON 文件。项目根目录里附带了一个示例文件 [`receipt_template.example.json`](receipt_template.example.json)。
|
|
@@ -113,18 +137,18 @@ nonebot.load_plugin("nonebot_plugin_receipts")
|
|
|
113
137
|
直接在命令后附带内容:
|
|
114
138
|
|
|
115
139
|
```text
|
|
116
|
-
|
|
140
|
+
/ticket 今晚睡大觉
|
|
117
141
|
```
|
|
118
142
|
|
|
119
143
|
或者:
|
|
120
144
|
|
|
121
|
-
1. 发送
|
|
145
|
+
1. 发送 `/ticket` 或 `/receipt`
|
|
122
146
|
2. 按提示继续发送一条包含文本、图片或两者混合的消息
|
|
123
147
|
|
|
124
148
|
插件会把纯文本和图片消息段一起打印,并在提交成功后回复当前队列长度。
|
|
125
149
|
|
|
126
150
|
如果命令后没有直接附带内容,插件会提示用户继续发送,并明确说明多久后超时。
|
|
127
|
-
如果用户在该时间窗口后才继续回复,插件会提示“已超时”,并直接结束本次等待;此时需要重新发送
|
|
151
|
+
如果用户在该时间窗口后才继续回复,插件会提示“已超时”,并直接结束本次等待;此时需要重新发送 `/ticket` 或 `/receipt` 开始新的打印流程。
|
|
128
152
|
|
|
129
153
|
`RECEIPT_SESSION_TIMEOUT_SECONDS` 的实际生效时间不会超过 NoneBot 全局 `SESSION_EXPIRE_TIMEOUT`,建议两者保持一致或让前者更小。
|
|
130
154
|
|
|
@@ -18,6 +18,7 @@ nonebot_plugin_receipts.egg-info/dependency_links.txt
|
|
|
18
18
|
nonebot_plugin_receipts.egg-info/requires.txt
|
|
19
19
|
nonebot_plugin_receipts.egg-info/top_level.txt
|
|
20
20
|
tests/test_command_handlers.py
|
|
21
|
+
tests/test_config.py
|
|
21
22
|
tests/test_render_blocks.py
|
|
22
23
|
tests/test_renderer.py
|
|
23
24
|
tests/test_text_markup.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nonebot_plugin_receipts"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.4"
|
|
8
8
|
description = "NoneBot2 plugin for sending text and image messages to receipts-spooler"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12, <4.0"
|
{nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/tests/test_command_handlers.py
RENAMED
|
@@ -5,12 +5,17 @@ from types import SimpleNamespace
|
|
|
5
5
|
from typing import TYPE_CHECKING, Any, cast
|
|
6
6
|
from unittest.mock import AsyncMock, patch
|
|
7
7
|
|
|
8
|
+
from nonebot.exception import FinishedException
|
|
9
|
+
|
|
8
10
|
from nonebot_plugin_receipts.command_handlers import (
|
|
9
11
|
RECEIPT_TIMEOUT_DEADLINE_KEY,
|
|
10
12
|
build_initial_receipt_prompt,
|
|
11
13
|
build_timeout_receipt_prompt,
|
|
12
14
|
format_timeout_duration,
|
|
13
15
|
get_effective_session_timeout_seconds,
|
|
16
|
+
get_event_group_id,
|
|
17
|
+
get_event_user_id,
|
|
18
|
+
is_receipt_submission_allowed,
|
|
14
19
|
is_receipt_timeout_expired,
|
|
15
20
|
register_receipt_handlers,
|
|
16
21
|
reset_receipt_timeout_deadline,
|
|
@@ -99,8 +104,23 @@ class DummyDriver:
|
|
|
99
104
|
|
|
100
105
|
|
|
101
106
|
class DummyPluginConfig:
|
|
102
|
-
def __init__(
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
seconds: int,
|
|
110
|
+
allowed_user_ids: list[str] | None = None,
|
|
111
|
+
allowed_group_ids: list[str] | None = None,
|
|
112
|
+
) -> None:
|
|
103
113
|
self.receipt_session_timeout_seconds = seconds
|
|
114
|
+
self.receipt_allowed_user_ids = allowed_user_ids or []
|
|
115
|
+
self.receipt_allowed_group_ids = allowed_group_ids or []
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def allowed_user_ids(self) -> list[str]:
|
|
119
|
+
return self.receipt_allowed_user_ids
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def allowed_group_ids(self) -> list[str]:
|
|
123
|
+
return self.receipt_allowed_group_ids
|
|
104
124
|
|
|
105
125
|
|
|
106
126
|
class CommandHandlersTestCase(unittest.TestCase):
|
|
@@ -142,6 +162,54 @@ class CommandHandlersTestCase(unittest.TestCase):
|
|
|
142
162
|
matcher.state[RECEIPT_TIMEOUT_DEADLINE_KEY] = 0
|
|
143
163
|
self.assertTrue(is_receipt_timeout_expired(cast("Any", matcher)))
|
|
144
164
|
|
|
165
|
+
def test_get_event_ids(self) -> None:
|
|
166
|
+
private_event = SimpleNamespace(user_id=123456)
|
|
167
|
+
group_event = SimpleNamespace(user_id=123456, group_id=654321)
|
|
168
|
+
|
|
169
|
+
self.assertEqual(get_event_user_id(cast("Any", private_event)), "123456")
|
|
170
|
+
self.assertEqual(get_event_group_id(cast("Any", private_event)), "")
|
|
171
|
+
self.assertEqual(get_event_group_id(cast("Any", group_event)), "654321")
|
|
172
|
+
|
|
173
|
+
def test_submission_allowed_when_whitelist_empty(self) -> None:
|
|
174
|
+
event = SimpleNamespace(user_id=123456, group_id=654321)
|
|
175
|
+
|
|
176
|
+
with patch(
|
|
177
|
+
"nonebot_plugin_receipts.command_handlers.get_runtime_config",
|
|
178
|
+
return_value=DummyPluginConfig(120),
|
|
179
|
+
):
|
|
180
|
+
self.assertTrue(is_receipt_submission_allowed(cast("Any", event)))
|
|
181
|
+
|
|
182
|
+
def test_submission_allowed_for_whitelisted_user(self) -> None:
|
|
183
|
+
event = SimpleNamespace(user_id=123456)
|
|
184
|
+
|
|
185
|
+
with patch(
|
|
186
|
+
"nonebot_plugin_receipts.command_handlers.get_runtime_config",
|
|
187
|
+
return_value=DummyPluginConfig(120, allowed_user_ids=["123456"]),
|
|
188
|
+
):
|
|
189
|
+
self.assertTrue(is_receipt_submission_allowed(cast("Any", event)))
|
|
190
|
+
|
|
191
|
+
def test_submission_allowed_for_whitelisted_group(self) -> None:
|
|
192
|
+
event = SimpleNamespace(user_id=111111, group_id=654321)
|
|
193
|
+
|
|
194
|
+
with patch(
|
|
195
|
+
"nonebot_plugin_receipts.command_handlers.get_runtime_config",
|
|
196
|
+
return_value=DummyPluginConfig(120, allowed_group_ids=["654321"]),
|
|
197
|
+
):
|
|
198
|
+
self.assertTrue(is_receipt_submission_allowed(cast("Any", event)))
|
|
199
|
+
|
|
200
|
+
def test_submission_denied_for_non_whitelisted_sender(self) -> None:
|
|
201
|
+
event = SimpleNamespace(user_id=111111, group_id=222222)
|
|
202
|
+
|
|
203
|
+
with patch(
|
|
204
|
+
"nonebot_plugin_receipts.command_handlers.get_runtime_config",
|
|
205
|
+
return_value=DummyPluginConfig(
|
|
206
|
+
120,
|
|
207
|
+
allowed_user_ids=["123456"],
|
|
208
|
+
allowed_group_ids=["654321"],
|
|
209
|
+
),
|
|
210
|
+
):
|
|
211
|
+
self.assertFalse(is_receipt_submission_allowed(cast("Any", event)))
|
|
212
|
+
|
|
145
213
|
|
|
146
214
|
class CommandHandlerFlowTestCase(unittest.IsolatedAsyncioTestCase):
|
|
147
215
|
def setUp(self) -> None:
|
|
@@ -153,23 +221,55 @@ class CommandHandlerFlowTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
153
221
|
self,
|
|
154
222
|
) -> None:
|
|
155
223
|
matcher = SimpleNamespace(state={}, send=AsyncMock(), set_arg=AsyncMock())
|
|
224
|
+
event = SimpleNamespace(user_id=123456)
|
|
156
225
|
message = DummyMessage()
|
|
157
226
|
|
|
158
|
-
with
|
|
159
|
-
|
|
160
|
-
|
|
227
|
+
with (
|
|
228
|
+
patch(
|
|
229
|
+
"nonebot_plugin_receipts.command_handlers.get_effective_session_timeout_seconds",
|
|
230
|
+
return_value=119,
|
|
231
|
+
),
|
|
232
|
+
patch(
|
|
233
|
+
"nonebot_plugin_receipts.command_handlers.get_runtime_config",
|
|
234
|
+
return_value=DummyPluginConfig(120),
|
|
235
|
+
),
|
|
161
236
|
):
|
|
162
|
-
await call_registered_handler(
|
|
237
|
+
await call_registered_handler(
|
|
238
|
+
require_handle_handler(), matcher, event, message
|
|
239
|
+
)
|
|
163
240
|
|
|
164
241
|
matcher.send.assert_awaited_once()
|
|
165
242
|
prompt = matcher.send.await_args.args[0]
|
|
166
243
|
self.assertIn("1 分 59 秒", prompt)
|
|
167
244
|
self.assertIn(RECEIPT_TIMEOUT_DEADLINE_KEY, matcher.state)
|
|
168
245
|
|
|
246
|
+
async def test_handle_print_command_ignores_non_whitelisted_sender(self) -> None:
|
|
247
|
+
matcher = SimpleNamespace(state={}, send=AsyncMock(), set_arg=AsyncMock())
|
|
248
|
+
event = SimpleNamespace(user_id=111111, group_id=222222)
|
|
249
|
+
message = DummyMessage(text="hello")
|
|
250
|
+
|
|
251
|
+
with (
|
|
252
|
+
self.assertRaises(FinishedException),
|
|
253
|
+
patch(
|
|
254
|
+
"nonebot_plugin_receipts.command_handlers.get_runtime_config",
|
|
255
|
+
return_value=DummyPluginConfig(
|
|
256
|
+
120,
|
|
257
|
+
allowed_user_ids=["123456"],
|
|
258
|
+
allowed_group_ids=["654321"],
|
|
259
|
+
),
|
|
260
|
+
),
|
|
261
|
+
):
|
|
262
|
+
await call_registered_handler(
|
|
263
|
+
require_handle_handler(), matcher, event, message
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
matcher.send.assert_not_awaited()
|
|
267
|
+
matcher.set_arg.assert_not_awaited()
|
|
268
|
+
|
|
169
269
|
async def test_handle_receipt_content_finishes_when_timeout_expired(self) -> None:
|
|
170
270
|
matcher = DummyMatcher()
|
|
171
271
|
matcher.state[RECEIPT_TIMEOUT_DEADLINE_KEY] = 0
|
|
172
|
-
event = SimpleNamespace()
|
|
272
|
+
event = SimpleNamespace(user_id=123456)
|
|
173
273
|
receipt_content = DummyMessage(text="late")
|
|
174
274
|
|
|
175
275
|
with (
|
|
@@ -178,6 +278,10 @@ class CommandHandlerFlowTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
178
278
|
"nonebot_plugin_receipts.command_handlers.get_effective_session_timeout_seconds",
|
|
179
279
|
return_value=119,
|
|
180
280
|
),
|
|
281
|
+
patch(
|
|
282
|
+
"nonebot_plugin_receipts.command_handlers.get_runtime_config",
|
|
283
|
+
return_value=DummyPluginConfig(120),
|
|
284
|
+
),
|
|
181
285
|
):
|
|
182
286
|
await call_registered_handler(
|
|
183
287
|
require_got_handler(), matcher, event, receipt_content
|
|
@@ -186,6 +290,26 @@ class CommandHandlerFlowTestCase(unittest.IsolatedAsyncioTestCase):
|
|
|
186
290
|
self.assertIn("已超时", str(exc_info.exception))
|
|
187
291
|
self.assertIn("/小票", str(exc_info.exception))
|
|
188
292
|
|
|
293
|
+
async def test_handle_receipt_content_ignores_non_whitelisted_sender(self) -> None:
|
|
294
|
+
matcher = DummyMatcher()
|
|
295
|
+
event = SimpleNamespace(user_id=111111, group_id=222222)
|
|
296
|
+
receipt_content = DummyMessage(text="late")
|
|
297
|
+
|
|
298
|
+
with (
|
|
299
|
+
self.assertRaises(FinishedException),
|
|
300
|
+
patch(
|
|
301
|
+
"nonebot_plugin_receipts.command_handlers.get_runtime_config",
|
|
302
|
+
return_value=DummyPluginConfig(
|
|
303
|
+
120,
|
|
304
|
+
allowed_user_ids=["123456"],
|
|
305
|
+
allowed_group_ids=["654321"],
|
|
306
|
+
),
|
|
307
|
+
),
|
|
308
|
+
):
|
|
309
|
+
await call_registered_handler(
|
|
310
|
+
require_got_handler(), matcher, event, receipt_content
|
|
311
|
+
)
|
|
312
|
+
|
|
189
313
|
|
|
190
314
|
if __name__ == "__main__":
|
|
191
315
|
unittest.main()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import unittest
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from nonebot.config import BaseSettings
|
|
8
|
+
from nonebot_plugin_receipts.config import Config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigTestCase(unittest.TestCase):
|
|
12
|
+
def test_whitelist_defaults_to_empty_lists(self) -> None:
|
|
13
|
+
config = Config()
|
|
14
|
+
|
|
15
|
+
self.assertEqual(config.allowed_user_ids, [])
|
|
16
|
+
self.assertEqual(config.allowed_group_ids, [])
|
|
17
|
+
|
|
18
|
+
def test_whitelist_accepts_comma_separated_strings(self) -> None:
|
|
19
|
+
config = Config.model_validate(
|
|
20
|
+
{
|
|
21
|
+
"receipt_allowed_user_ids": "123456, 234567,345678",
|
|
22
|
+
"receipt_allowed_group_ids": "456789 567890",
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
self.assertEqual(
|
|
27
|
+
config.allowed_user_ids,
|
|
28
|
+
["123456", "234567", "345678"],
|
|
29
|
+
)
|
|
30
|
+
self.assertEqual(
|
|
31
|
+
config.allowed_group_ids,
|
|
32
|
+
["456789", "567890"],
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def test_whitelist_accepts_json_arrays(self) -> None:
|
|
36
|
+
config = Config.model_validate(
|
|
37
|
+
{
|
|
38
|
+
"receipt_allowed_user_ids": '["123456", 234567, ""]',
|
|
39
|
+
"receipt_allowed_group_ids": [456789, "567890", ""],
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.assertEqual(config.allowed_user_ids, ["123456", "234567"])
|
|
44
|
+
self.assertEqual(config.allowed_group_ids, ["456789", "567890"])
|
|
45
|
+
|
|
46
|
+
def test_whitelist_accepts_nonebot_env_style_values(self) -> None:
|
|
47
|
+
with patch.dict(
|
|
48
|
+
os.environ,
|
|
49
|
+
{
|
|
50
|
+
"RECEIPT_ALLOWED_USER_IDS": "123456,234567",
|
|
51
|
+
"RECEIPT_ALLOWED_GROUP_IDS": "[\"345678\", \"456789\"]",
|
|
52
|
+
},
|
|
53
|
+
clear=False,
|
|
54
|
+
):
|
|
55
|
+
values = BaseSettings._settings_build_values(
|
|
56
|
+
Config,
|
|
57
|
+
{},
|
|
58
|
+
env_file=None,
|
|
59
|
+
env_file_encoding="utf-8",
|
|
60
|
+
env_nested_delimiter="__",
|
|
61
|
+
)
|
|
62
|
+
config = Config.model_validate(values)
|
|
63
|
+
|
|
64
|
+
self.assertEqual(config.allowed_user_ids, ["123456", "234567"])
|
|
65
|
+
self.assertEqual(config.allowed_group_ids, ["345678", "456789"])
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
unittest.main()
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
from typing import Literal
|
|
2
|
-
|
|
3
|
-
from pydantic import BaseModel, Field
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Config(BaseModel):
|
|
7
|
-
receipts_spooler_url: str = "http://127.0.0.1:8000"
|
|
8
|
-
receipts_spooler_token: str | None = None
|
|
9
|
-
receipts_spooler_timeout: float = 5.0
|
|
10
|
-
receipt_image_fetch_timeout: float = 5.0
|
|
11
|
-
receipt_render_mode: Literal["raster", "hybrid"] = "raster"
|
|
12
|
-
receipt_printer_width: int = Field(default=576, ge=128, le=576)
|
|
13
|
-
receipt_template_path: str | None = None
|
|
14
|
-
receipt_font_path: str | None = None
|
|
15
|
-
receipt_font_size: int = Field(default=24, ge=12, le=96)
|
|
16
|
-
receipt_line_spacing: int = Field(default=6, ge=0, le=40)
|
|
17
|
-
receipt_section_gap: int = Field(default=6, ge=0, le=100)
|
|
18
|
-
receipt_session_timeout_seconds: int = Field(default=120, ge=10, le=3600)
|
|
19
|
-
receipt_feed_lines: int = Field(default=4, ge=0, le=10)
|
|
20
|
-
receipt_enable_cut: bool = True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/renderer.py
RENAMED
|
File without changes
|
{nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/spooler.py
RENAMED
|
File without changes
|
{nonebot_plugin_receipts-0.2.2 → nonebot_plugin_receipts-0.2.4}/nonebot_plugin_receipts/template.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|