nonebot-plugin-mail 0.7.0__py3-none-any.whl
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_mail/__init__.py +30 -0
- nonebot_plugin_mail/config.py +29 -0
- nonebot_plugin_mail/constants.py +18 -0
- nonebot_plugin_mail/data/__init__.py +4 -0
- nonebot_plugin_mail/data/fallback_contacts.py +48 -0
- nonebot_plugin_mail/data/fallback_contacts_privacy.py +25 -0
- nonebot_plugin_mail/handlers/__init__.py +4 -0
- nonebot_plugin_mail/handlers/mail.py +69 -0
- nonebot_plugin_mail/handlers/query.py +48 -0
- nonebot_plugin_mail/handlers/receive.py +69 -0
- nonebot_plugin_mail/handlers/recognize.py +134 -0
- nonebot_plugin_mail/handlers/send.py +243 -0
- nonebot_plugin_mail/handlers/utils.py +44 -0
- nonebot_plugin_mail/models.py +53 -0
- nonebot_plugin_mail/services/__init__.py +4 -0
- nonebot_plugin_mail/services/contacts.py +157 -0
- nonebot_plugin_mail/services/images.py +79 -0
- nonebot_plugin_mail/services/matcher.py +57 -0
- nonebot_plugin_mail/services/notion.py +229 -0
- nonebot_plugin_mail/services/recognizer.py +191 -0
- nonebot_plugin_mail/services/render.py +115 -0
- nonebot_plugin_mail/services/rules.py +165 -0
- nonebot_plugin_mail-0.7.0.dist-info/METADATA +126 -0
- nonebot_plugin_mail-0.7.0.dist-info/RECORD +27 -0
- nonebot_plugin_mail-0.7.0.dist-info/WHEEL +5 -0
- nonebot_plugin_mail-0.7.0.dist-info/licenses/LICENSE +674 -0
- nonebot_plugin_mail-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# @Version : 0.7.0
|
|
4
|
+
# 规划备注:原 mail_v7 的“寄信/寄件/寄出”人工登记流程
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import datetime
|
|
8
|
+
import random
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
from nonebot import on_command
|
|
12
|
+
from nonebot.adapters.onebot.v11 import Bot, Event, GroupMessageEvent, MessageSegment
|
|
13
|
+
from nonebot.params import ArgStr
|
|
14
|
+
from nonebot.typing import T_State
|
|
15
|
+
|
|
16
|
+
from .utils import At
|
|
17
|
+
from ..config import config
|
|
18
|
+
from ..constants import (
|
|
19
|
+
SPECIAL_BIRDGREEN_ID,
|
|
20
|
+
SPECIAL_CAKE_ID,
|
|
21
|
+
SPECIAL_DANDAN_ID,
|
|
22
|
+
SPECIAL_HK_ID,
|
|
23
|
+
SPECIAL_SCHOOL_ID,
|
|
24
|
+
SPECIAL_YING_ID,
|
|
25
|
+
SPECIAL_YUN_ID,
|
|
26
|
+
)
|
|
27
|
+
from ..services.contacts import get_contacts, get_key_by_qq, get_name_by_uuid, qq_map, qqmap
|
|
28
|
+
from ..services.notion import mail_record
|
|
29
|
+
from ..services.rules import normalize_mail_type, normalize_tracking_token
|
|
30
|
+
|
|
31
|
+
sendletter = on_command("寄信", priority=5, block=True, aliases={"寄件", "寄出", "send a mail"})
|
|
32
|
+
attempt = 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@sendletter.handle()
|
|
36
|
+
async def _(state: T_State, bot: Bot, event: GroupMessageEvent):
|
|
37
|
+
global attempt
|
|
38
|
+
attempt = 0
|
|
39
|
+
contacts = await get_contacts()
|
|
40
|
+
qq_str = event.get_user_id()
|
|
41
|
+
nowhour = datetime.datetime.now().hour
|
|
42
|
+
qqmap(contacts)
|
|
43
|
+
state["lang"] = "zh-cn"
|
|
44
|
+
if qq_str in qq_map.get(SPECIAL_CAKE_ID, []):
|
|
45
|
+
s = random.choice(["是可恶的蛋糕又在寄信,这次又会寄信去诅咒谁呢?", "可恶,蛋糕又要去诅咒人了,这次会诅咒谁?", "真坏,蛋糕又在偷偷写信诅咒了,这次又打算害谁呢?", "糟糕,蛋糕又开始寄信了,这回会盯上谁倒霉?", "烦人的蛋糕又动笔写信诅咒了,这次不知道谁要遭殃了", "哎,蛋糕又寄出诅咒信了,这次轮到谁了?", "可恶的蛋糕又在寄出那封信了,这次又会坑谁呢?", "不好,蛋糕又开始诅咒人了,这回是谁中招?", "蛋糕这个家伙又写信诅咒去了,这次又准备害谁啊?", "糟了,蛋糕又寄信诅咒了,这次谁要倒霉?", "这个蛋糕又在搞事情写信诅咒了,这次又会针对谁呢?", "唉,蛋糕又寄出诅咒信了,这回是谁被盯上?"])
|
|
46
|
+
s += "\n不过话说回来,蛋糕还是不愿意透露学校的收件地址诶,给蛋糕回信的时候得等多久才能收到呢?"
|
|
47
|
+
elif qq_str in qq_map.get(SPECIAL_YUN_ID, []):
|
|
48
|
+
s = "早安" if nowhour < 12 else ("午安" if nowhour < 18 else "晚安") + "捏," + random.choice(["是本✌又在寄信,这次又在想谁呢?","是可爱的云云又在寄信,这次又会寄信去诱惑谁呢?","云云又要去诱惑人了,这次会诱惑谁?"])
|
|
49
|
+
elif qq_str in qq_map.get(SPECIAL_HK_ID, []):
|
|
50
|
+
state["lang"] = "zh-hk"
|
|
51
|
+
s = f"{'早晨' if nowhour < 12 else ('午安' if nowhour < 18 else '晚安')} 諾寶寶,而家你又要寄信畀邊個呀?"
|
|
52
|
+
elif qq_str in qq_map.get(SPECIAL_BIRDGREEN_ID, []):
|
|
53
|
+
s = random.choice([f"原来是勤奋的鸟绿哥哥要去寄信了诶?是要给谁寄呢👀","每日一问:鸟绿哥哥又会在什么时候抽奖呢?\n今天你打算给谁寄信"])
|
|
54
|
+
elif qq_str in qq_map.get(SPECIAL_YING_ID, []):
|
|
55
|
+
s = f"英✌,难得寄一次信呢。\n打算寄给谁呢?"
|
|
56
|
+
elif qq_str in qq_map.get(SPECIAL_DANDAN_ID, []):
|
|
57
|
+
s = f"蛋蛋今天难得有空寄信呀?打算寄给谁呢?"
|
|
58
|
+
elif qq_str in qq_map.get(SPECIAL_SCHOOL_ID, []):
|
|
59
|
+
s = f"从学校到寄件点得走好远吧?这么珍贵的一封信打算寄给谁呢?"
|
|
60
|
+
else:
|
|
61
|
+
s = "今天要给谁寄信呢?"
|
|
62
|
+
|
|
63
|
+
state["sender"] = get_key_by_qq(event.get_user_id())
|
|
64
|
+
state["contacts"] = contacts
|
|
65
|
+
if state["lang"] == "zh-cn":
|
|
66
|
+
s += "\n你需要直接@出收件人哦,我这边看得到哒!\n中途不要输入其他消息"
|
|
67
|
+
elif state["lang"] == "zh-hk":
|
|
68
|
+
s += "\n你要直接@返收件人呀,我呢邊睇得到㗎!\n中途唔好輸入其他嘅訊息"
|
|
69
|
+
await sendletter.send(s)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@sendletter.got("a1")
|
|
73
|
+
async def _(state: T_State, bot: Bot, event: Event, addressee: str = ArgStr("a1")):
|
|
74
|
+
contacts = state["contacts"]
|
|
75
|
+
at = At(event.json())
|
|
76
|
+
if not at:
|
|
77
|
+
if state["lang"] == "zh-cn":
|
|
78
|
+
await sendletter.finish("我不太明白你的输入,下次需要登记时候再叫我吧!")
|
|
79
|
+
elif state["lang"] == "zh-hk":
|
|
80
|
+
await sendletter.finish("我都係仲未係好明,下次要登記嗰陣再叫我啦!")
|
|
81
|
+
|
|
82
|
+
if len(at) > 1:
|
|
83
|
+
addressee_list = []
|
|
84
|
+
name_list = []
|
|
85
|
+
for qq in at:
|
|
86
|
+
uuid = get_key_by_qq(str(qq))
|
|
87
|
+
if uuid:
|
|
88
|
+
addressee_list.append(uuid)
|
|
89
|
+
name_list.append(await get_name_by_uuid(uuid, contacts))
|
|
90
|
+
state["multi"] = True
|
|
91
|
+
state["addressee_list"] = addressee_list
|
|
92
|
+
state["name_list"] = name_list
|
|
93
|
+
state["name_str"] = ",".join(name_list)
|
|
94
|
+
if state["lang"] == "zh-cn":
|
|
95
|
+
await sendletter.send(f"那么现在要给{state['name_str']}寄什么种类呢?\n如果大家都一样,直接输入一个类型就行。\n如果要区分,请按顺序用空格分开,比如:平信 挂号信 明信片")
|
|
96
|
+
elif state["lang"] == "zh-hk":
|
|
97
|
+
await sendletter.send(f"咁而家要寄畀{state['name_str']}嘅係咩種類呢?\n如果全部一樣,直接輸入一個類型就得。\n如果要分開,請按順序用空格隔開,例如:平信 掛號信 明信片")
|
|
98
|
+
else:
|
|
99
|
+
state["multi"] = False
|
|
100
|
+
addressee = get_key_by_qq(str(at[0]))
|
|
101
|
+
state["addressee"] = addressee
|
|
102
|
+
if state["lang"] == "zh-cn":
|
|
103
|
+
await sendletter.send("那么,寄出哪种类型呢?\n平信、挂号信、明信片还是印刷品小包呢?")
|
|
104
|
+
elif state["lang"] == "zh-hk":
|
|
105
|
+
await sendletter.send("咁,寄邊種類型好呢?\n平郵、掛號信、明信片定係印刷品小包呢?")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@sendletter.got("a2")
|
|
109
|
+
async def _(state: T_State, bot: Bot, event: Event, type: str = ArgStr("a2")):
|
|
110
|
+
global attempt
|
|
111
|
+
if state.get("multi") == False:
|
|
112
|
+
type = normalize_mail_type(type)
|
|
113
|
+
if state["lang"] == "zh-cn":
|
|
114
|
+
await sendletter.send(f"是要寄 {type} 吗?这边先记下来了")
|
|
115
|
+
elif state["lang"] == "zh-hk":
|
|
116
|
+
await sendletter.send(f"係要寄 {type} 嗎?呢邊先記低咗")
|
|
117
|
+
await asyncio.sleep(1 + random.randint(1, 2) + random.randint(0, 10) / 10)
|
|
118
|
+
state["type"] = type
|
|
119
|
+
if type in ["平信", "邮简", "明信片", "平常印刷品"]:
|
|
120
|
+
if state["lang"] == "zh-cn":
|
|
121
|
+
await sendletter.send(f"如果是{type}的话?能拿到{type}编号/条码吗?如果有的话那就直接打出来吧!")
|
|
122
|
+
elif state["lang"] == "zh-hk":
|
|
123
|
+
await sendletter.send(f"如果係{type}嘅話?可唔可以攞到{type}嘅編號/條碼?如果有嘅話就直接打出嚟啦!")
|
|
124
|
+
else:
|
|
125
|
+
if state["lang"] == "zh-cn":
|
|
126
|
+
await sendletter.send(MessageSegment.text(f"如果是{type},想必一定有邮件编号吧?快快发在聊天框给我看看吧") + MessageSegment.face(2) + MessageSegment.face(2) + MessageSegment.face(2) + MessageSegment.text("邮件编号内请不要输入空格"))
|
|
127
|
+
elif state["lang"] == "zh-hk":
|
|
128
|
+
await sendletter.send(MessageSegment.text(f"如果係{type}嘅話?可唔可以攞到{type}嘅編號/條碼?如果有嘅話就直接打出嚟啦!") + MessageSegment.face(2) + MessageSegment.face(2) + MessageSegment.face(2) + MessageSegment.text("郵件編號內唔要輸入空格"))
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
name_list = state["name_list"]
|
|
132
|
+
addressee_list = state["addressee_list"]
|
|
133
|
+
parts = [x.strip() for x in type.strip().split() if x.strip()]
|
|
134
|
+
if not parts:
|
|
135
|
+
if state["lang"] == "zh-cn":
|
|
136
|
+
await sendletter.reject("输入不能为空哦,请重新输入邮件类型。")
|
|
137
|
+
elif state["lang"] == "zh-hk":
|
|
138
|
+
await sendletter.reject("輸入唔可以為空哦,請重新輸入郵件嘅類型。")
|
|
139
|
+
if len(parts) == 1:
|
|
140
|
+
common_type = normalize_mail_type(parts[0])
|
|
141
|
+
state["type_list"] = [common_type for _ in addressee_list]
|
|
142
|
+
if state["lang"] == "zh-cn":
|
|
143
|
+
await sendletter.send("我明白啦,这次大家都是同一种:\n" + "\n".join(f"{name}:{common_type}" for name in name_list) + "\n那么有对应的邮件编号吗?如果有的话请按顺序以空格输入吧!如果部分缺少编号请以 none 占位。如果都沒有只需要输入一个none。")
|
|
144
|
+
elif state["lang"] == "zh-hk":
|
|
145
|
+
await sendletter.send("我明白啦,今次大家都係同一種:\n" + "\n".join(f"{name}:{common_type}" for name in name_list) + "\n咁有冇對應嘅郵件編號?如果有嘅話,請按順序用空格輸入啦!如果有部分冇編號,請用 none 佔位。如果都冇輸入一個 none 就行喇。")
|
|
146
|
+
return
|
|
147
|
+
if len(parts) != len(addressee_list):
|
|
148
|
+
if attempt <= 1:
|
|
149
|
+
attempt += 1
|
|
150
|
+
if state["lang"] == "zh-cn":
|
|
151
|
+
await sendletter.reject(f"输入不正确哦。\n" f"你这次要寄给 {len(addressee_list)} 个人:{','.join(name_list)}\n" f"如果不区分,直接输入一个类型就可以;\n" f"如果要区分,请按顺序输入 {len(addressee_list)} 个类型,并用空格隔开。")
|
|
152
|
+
elif state["lang"] == "zh-hk":
|
|
153
|
+
await sendletter.reject(f"輸入唔正確喎。\n" f"你今次要寄畀 {len(addressee_list)} 個人:{','.join(name_list)}\n" f"如果唔使分,直接輸入一個類型就得;\n" f"如果要分,請按順序輸入 {len(addressee_list)} 個類型,仲要用空格隔開。")
|
|
154
|
+
else:
|
|
155
|
+
if state["lang"] == "zh-cn":
|
|
156
|
+
await sendletter.finish("我还是不太明白你的意思,稍后再重试吧!")
|
|
157
|
+
elif state["lang"] == "zh-hk":
|
|
158
|
+
await sendletter.finish("我仲係唔太明你嘅意思,等陣再試過啦!")
|
|
159
|
+
normalized_types = [normalize_mail_type(x) for x in parts]
|
|
160
|
+
state["type_list"] = normalized_types
|
|
161
|
+
if state["lang"] == "zh-cn":
|
|
162
|
+
await sendletter.send("好的,我按顺序记下来了:\n" + "\n".join(f"{name}:{mail_type}" for name, mail_type in zip(name_list, normalized_types)) + "\n那么有对应的邮件编号吗?如果有的话请按顺序以空格输入吧!如果部分缺少编号请以 none 占位。")
|
|
163
|
+
elif state["lang"] == "zh-hk":
|
|
164
|
+
await sendletter.send("好嘅,我按順序記低咗:\n" + "\n".join(f"{name}:{mail_type}" for name, mail_type in zip(name_list, normalized_types)) + "\n咁有對應嘅電郵編號嗎?如果有嘅話請按順序用空格輸入啦!如果部分缺少編號請用 none 佔位。")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@sendletter.got("a3")
|
|
168
|
+
async def _(bot: Bot, event: Event, state: T_State, tracking_no: str = ArgStr("a3")):
|
|
169
|
+
contacts = state["contacts"]
|
|
170
|
+
sender = state["sender"]
|
|
171
|
+
today = datetime.date.today().isoformat()
|
|
172
|
+
if state.get("multi") == False:
|
|
173
|
+
addressee = state["addressee"]
|
|
174
|
+
type_ = state["type"]
|
|
175
|
+
tracking_no = normalize_tracking_token(tracking_no)
|
|
176
|
+
if state["lang"] == "zh-cn":
|
|
177
|
+
if datetime.datetime.now().hour <= 17:
|
|
178
|
+
await sendletter.send(f"{'唔,这样啊,那就只能老老实实当最纯正的平信寄咯!' if not tracking_no else ''}" f"那么这封邮件就是由{await get_name_by_uuid(sender, contacts)}寄给{await get_name_by_uuid(addressee, contacts)}的{type_}吧\n" f"现在是{datetime.date.today().strftime('%y-%m-%d')},应该是今天寄出的吧?\n" f"那我就先帮你登记下来了哦")
|
|
179
|
+
else:
|
|
180
|
+
today = (datetime.date.today() + datetime.timedelta(days=1)).isoformat()
|
|
181
|
+
await sendletter.send(f"{'唔,这样啊,那就只能老老实实当最纯正的平信寄咯!' if not tracking_no else ''}" f"那么这封邮件就是由{await get_name_by_uuid(sender, contacts)}寄给{await get_name_by_uuid(addressee, contacts)}的{type_}吧\n" f"现在是{datetime.date.today().strftime('%y-%m-%d')},可是邮局现在下班了,那就帮你登记第二天寄出咯")
|
|
182
|
+
elif state["lang"] == "zh-hk":
|
|
183
|
+
if datetime.datetime.now().hour <= 17:
|
|
184
|
+
await sendletter.send(f"{'唔,咁樣啊,咁就唯有老老實實當最純正嘅平信寄啦!' if not tracking_no else ''}" f"咁呢封郵件就係由{await get_name_by_uuid(sender, contacts)}寄俾{await get_name_by_uuid(addressee, contacts)}嘅{type_}啦\n" f"而家係{datetime.date.today().strftime('%y-%m-%d')},應該係今日寄出嘅吧?\n" f"咁我就先幫你登記咗先啦")
|
|
185
|
+
else:
|
|
186
|
+
today = (datetime.date.today() + datetime.timedelta(days=1)).isoformat()
|
|
187
|
+
await sendletter.send(f"{'唔,咁樣啊,咁就唯有老老實實當最純正嘅平信寄啦!' if not tracking_no else ''}" f"咁呢封郵件就係由{await get_name_by_uuid(sender, contacts)}寄俾{await get_name_by_uuid(addressee, contacts)}嘅{type_}啦\n" f"而家係{datetime.date.today().strftime('%y-%m-%d')},郵局而家已經收工咗,我會幫你登記,喺第二日寄出。")
|
|
188
|
+
try:
|
|
189
|
+
sendmail = mail_record(DATABASE_ID=config.ras_database_id, SENDER_ID=sender, ADDRESSEE_ID=addressee, SEND_DATE=today, TRACKING_NO=tracking_no, TYPE=type_)
|
|
190
|
+
except httpx.ConnectError as e:
|
|
191
|
+
await sendletter.finish(f"Notion 请求异常,请重试: {e}")
|
|
192
|
+
return
|
|
193
|
+
await asyncio.sleep(1 + random.randint(1, 2) + random.randint(0, 10) / 10)
|
|
194
|
+
if state["lang"] == "zh-cn":
|
|
195
|
+
s = f"登记成功!\n\n登记的编号是:“{sendmail['id']}”,查询就靠这个啦!\n\n链接是\n{sendmail['url']}\n现在就可以打开看到哦!"
|
|
196
|
+
elif state["lang"] == "zh-hk":
|
|
197
|
+
s = f"登記成功!\n\n登記嘅編號係:「{sendmail['id']}」,之後查詢就靠呢個啦!\n\n連結係\n{sendmail['url']}\n而家就可以打開睇到喇!"
|
|
198
|
+
if addressee == SPECIAL_CAKE_ID:
|
|
199
|
+
if state["lang"] == "zh-cn":
|
|
200
|
+
s += "\n诶等等!蛋糕的地址好像不是学校诶?\n他真的能及时收到你寄出的信吗..."
|
|
201
|
+
elif state["lang"] == "zh-hk":
|
|
202
|
+
s += "\n欸等等!蛋糕嘅地址好似唔係學校喎?\n佢真係可以準時收到你寄出嘅信嗎..."
|
|
203
|
+
await sendletter.finish(s)
|
|
204
|
+
|
|
205
|
+
else:
|
|
206
|
+
addressee_list = state["addressee_list"]
|
|
207
|
+
name_list = state["name_list"]
|
|
208
|
+
type_list = state["type_list"]
|
|
209
|
+
parts = [x.strip() for x in tracking_no.strip().split() if x.strip()]
|
|
210
|
+
if len(parts) == 1 and normalize_tracking_token(parts[0]) is None:
|
|
211
|
+
tracking_list = [None for _ in addressee_list]
|
|
212
|
+
else:
|
|
213
|
+
if len(parts) != len(addressee_list):
|
|
214
|
+
if state["lang"] == "zh-cn":
|
|
215
|
+
await sendletter.reject(f"输入不正确哦。\n" f"你这次要寄给 {len(addressee_list)} 个人:{','.join(name_list)}\n" f"如果大家都没有编号,直接输入一个 none 就可以;\n" f"如果要区分,请按顺序输入 {len(addressee_list)} 个编号,并用空格隔开。\n")
|
|
216
|
+
elif state["lang"] == "zh-hk":
|
|
217
|
+
await sendletter.reject(f"輸入唔正確喎。\n" f"你今次要寄俾 {len(addressee_list)} 個人:{','.join(name_list)}\n" f"如果大家都冇編號,直接輸入一個 none 就可以;\n" f"如果要分開,請按順序輸入 {len(addressee_list)} 個編號,並用空格隔開。\n")
|
|
218
|
+
tracking_list = [normalize_tracking_token(x) for x in parts]
|
|
219
|
+
confirm_lines = []
|
|
220
|
+
for uuid, name, mail_type, trk in zip(addressee_list, name_list, type_list, tracking_list):
|
|
221
|
+
trk = trk if trk else "无"
|
|
222
|
+
confirm_lines.append(f"{name}:{mail_type},编号:{trk}")
|
|
223
|
+
if state["lang"] == "zh-cn":
|
|
224
|
+
await sendletter.send(f"好哦,这次寄件信息如下:\n" + "\n".join(confirm_lines) + f"\n现在是{datetime.date.today().strftime('%y-%m-%d')},我这就帮你登记。")
|
|
225
|
+
elif state["lang"] == "zh-hk":
|
|
226
|
+
await sendletter.send(f"好呀,今次寄件資料如下:\n" + "\n".join(confirm_lines) + f"\n而家係{datetime.date.today().strftime('%y-%m-%d')},我即刻幫你登記。")
|
|
227
|
+
results = []
|
|
228
|
+
try:
|
|
229
|
+
for uuid, mail_type, trk in zip(addressee_list, type_list, tracking_list):
|
|
230
|
+
sendmail = mail_record(DATABASE_ID=config.ras_database_id, SENDER_ID=sender, ADDRESSEE_ID=uuid, SEND_DATE=today, TRACKING_NO=trk, TYPE=mail_type)
|
|
231
|
+
results.append(sendmail)
|
|
232
|
+
except httpx.ConnectError as e:
|
|
233
|
+
await sendletter.finish(f"Notion 请求异常,请重试: {e}")
|
|
234
|
+
return
|
|
235
|
+
await asyncio.sleep(1 + random.randint(1, 2) + random.randint(0, 10) / 10)
|
|
236
|
+
success_lines = []
|
|
237
|
+
for uuid, res in zip(addressee_list, results):
|
|
238
|
+
name = await get_name_by_uuid(uuid, contacts)
|
|
239
|
+
success_lines.append(f"{name}:{res['url']}")
|
|
240
|
+
if state["lang"] == "zh-cn":
|
|
241
|
+
await sendletter.finish("多人寄件登记成功!\n" + "\n".join(success_lines))
|
|
242
|
+
elif state["lang"] == "zh-hk":
|
|
243
|
+
await sendletter.finish("多人寄件登記成功!\n" + "\n".join(success_lines))
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# @Version : 0.7.0
|
|
4
|
+
# 规划备注:群聊命令通用消息解析工具
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Union
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def At(data: str) -> Union[list[str], list[int], list]:
|
|
11
|
+
"""
|
|
12
|
+
检测at了谁,返回[qq, qq, qq,...]
|
|
13
|
+
包含全体成员直接返回['all']
|
|
14
|
+
如果没有at任何人,返回[]
|
|
15
|
+
:param data: event.json() event: GroupMessageEvent
|
|
16
|
+
:return: list
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
qq_list = []
|
|
20
|
+
data = json.loads(data)
|
|
21
|
+
for msg in data["message"]:
|
|
22
|
+
if msg["type"] == "at":
|
|
23
|
+
if "all" not in str(msg):
|
|
24
|
+
qq_list.append(int(msg["data"]["qq"]))
|
|
25
|
+
else:
|
|
26
|
+
return ["all"]
|
|
27
|
+
return qq_list
|
|
28
|
+
except KeyError:
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def MsgText(data: str):
|
|
33
|
+
"""
|
|
34
|
+
返回消息文本段内容(即去除 cq 码后的内容)
|
|
35
|
+
:param data: event.json()
|
|
36
|
+
:return: str
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
data = json.loads(data)
|
|
40
|
+
msg_text_list = filter(lambda x: x["type"] == "text" and x["data"]["text"].replace(" ", "") != "", data["message"])
|
|
41
|
+
msg_text = " ".join(map(lambda x: x["data"]["text"].strip(), msg_text_list)).strip()
|
|
42
|
+
return msg_text
|
|
43
|
+
except Exception:
|
|
44
|
+
return ""
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# @Version : 0.7.0
|
|
4
|
+
# 规划备注:Contact、RecognizeResult、MailRecordDraft 等数据结构
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Contact(BaseModel):
|
|
11
|
+
id: str
|
|
12
|
+
name: str = ""
|
|
13
|
+
phone: str = ""
|
|
14
|
+
email: str = ""
|
|
15
|
+
postcode1: str = ""
|
|
16
|
+
address1: str = ""
|
|
17
|
+
postcode2: str = ""
|
|
18
|
+
address2: str = ""
|
|
19
|
+
qq: str = ""
|
|
20
|
+
url: str = ""
|
|
21
|
+
source: str = "notion"
|
|
22
|
+
aliases: list[str] = Field(default_factory=list)
|
|
23
|
+
macau_recipient: bool = False
|
|
24
|
+
|
|
25
|
+
def to_legacy(self) -> dict[str, Any]:
|
|
26
|
+
return {
|
|
27
|
+
"id": self.id,
|
|
28
|
+
"姓名": self.name,
|
|
29
|
+
"电话": self.phone,
|
|
30
|
+
"邮箱": self.email,
|
|
31
|
+
"地址1": self.address1,
|
|
32
|
+
"邮编1": self.postcode1,
|
|
33
|
+
"地址2": self.address2,
|
|
34
|
+
"邮编2": self.postcode2,
|
|
35
|
+
"QQ": self.qq,
|
|
36
|
+
"url": self.url,
|
|
37
|
+
"source": self.source,
|
|
38
|
+
"aliases": self.aliases,
|
|
39
|
+
"macauRecipient": self.macau_recipient,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MailRecordDraft(BaseModel):
|
|
44
|
+
sender_id: str = ""
|
|
45
|
+
recipient_id: str = ""
|
|
46
|
+
send_date: str = ""
|
|
47
|
+
tracking_no: str = ""
|
|
48
|
+
mail_type: str = ""
|
|
49
|
+
image_name: str = ""
|
|
50
|
+
note: str = ""
|
|
51
|
+
errors: list[str] = Field(default_factory=list)
|
|
52
|
+
evidence: dict[str, Any] = Field(default_factory=dict)
|
|
53
|
+
confidence: dict[str, float] = Field(default_factory=dict)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# @Version : 0.7.0
|
|
4
|
+
# 规划备注:联系人缓存、fallback 合并、QQ 映射、联系人公开字段
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from nonebot.log import logger
|
|
11
|
+
|
|
12
|
+
from ..config import config
|
|
13
|
+
from ..data.fallback_contacts import FALLBACK_CONTACTS
|
|
14
|
+
from .notion import query_all_rows, row_to_contact
|
|
15
|
+
|
|
16
|
+
contacts_cache: dict[str, Any] = {"at": 0.0, "contacts": []}
|
|
17
|
+
qq_map: dict[str, list[str]] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def normalize_text(value: Any) -> str:
|
|
21
|
+
return str(value or "").replace(" ", "").replace("\n", "").replace("\t", "").lower()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def unique(items):
|
|
25
|
+
return list(dict.fromkeys([x for x in items if x]))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_macau_contact(contact_item: dict[str, Any]) -> bool:
|
|
29
|
+
return bool(re.search(
|
|
30
|
+
r"陳國政|陈国政|诺斯|chan\s+k[uw]ok\s+cheng",
|
|
31
|
+
f"{contact_item.get('name') or contact_item.get('姓名') or ''} {' '.join(contact_item.get('aliases') or [])}",
|
|
32
|
+
re.I,
|
|
33
|
+
))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def contact_key(contact_item: dict[str, Any]) -> str:
|
|
37
|
+
qq = str(contact_item.get("qq") or contact_item.get("QQ") or "").split(",")[0].strip()
|
|
38
|
+
return qq or normalize_text(contact_item.get("name") or contact_item.get("姓名"))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def merge_contacts(primary: list[dict[str, Any]], fallback: list[dict[str, Any]]):
|
|
42
|
+
by_key = {}
|
|
43
|
+
for item in fallback:
|
|
44
|
+
by_key[contact_key(item)] = item
|
|
45
|
+
for item in primary:
|
|
46
|
+
existing = by_key.get(contact_key(item), {})
|
|
47
|
+
by_key[contact_key(item)] = {**existing, **item, "source": item.get("source") or "notion"}
|
|
48
|
+
merged = []
|
|
49
|
+
for c in by_key.values():
|
|
50
|
+
if is_macau_contact(c):
|
|
51
|
+
c["aliases"] = unique([*(c.get("aliases") or []), "陈国政", "陳國政", "诺斯", "Chan Kuok Cheng", "Chan Kwok Cheng"])
|
|
52
|
+
c["macauRecipient"] = True
|
|
53
|
+
merged.append(c)
|
|
54
|
+
return merged
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def to_legacy_contact(c: dict[str, Any]) -> dict[str, Any]:
|
|
58
|
+
return {
|
|
59
|
+
"id": c.get("id", ""),
|
|
60
|
+
"姓名": c.get("姓名") or c.get("name", ""),
|
|
61
|
+
"电话": c.get("电话") or c.get("phone", ""),
|
|
62
|
+
"邮箱": c.get("邮箱") or c.get("email", ""),
|
|
63
|
+
"地址1": c.get("地址1") or c.get("address1", ""),
|
|
64
|
+
"邮编1": c.get("邮编1") or c.get("postcode1", ""),
|
|
65
|
+
"地址2": c.get("地址2") or c.get("address2", ""),
|
|
66
|
+
"邮编2": c.get("邮编2") or c.get("postcode2", ""),
|
|
67
|
+
"QQ": c.get("QQ") or c.get("qq", ""),
|
|
68
|
+
"url": c.get("url", ""),
|
|
69
|
+
"source": c.get("source", ""),
|
|
70
|
+
"aliases": c.get("aliases", []),
|
|
71
|
+
"macauRecipient": bool(c.get("macauRecipient")),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def to_ai_contact(c: dict[str, Any]) -> dict[str, Any]:
|
|
76
|
+
legacy = to_legacy_contact(c)
|
|
77
|
+
return {
|
|
78
|
+
"id": legacy["id"],
|
|
79
|
+
"name": legacy["姓名"],
|
|
80
|
+
"aliases": legacy.get("aliases") or [],
|
|
81
|
+
"phone": legacy["电话"],
|
|
82
|
+
"qq": legacy["QQ"],
|
|
83
|
+
"postcode1": legacy["邮编1"],
|
|
84
|
+
"address1": legacy["地址1"],
|
|
85
|
+
"postcode2": legacy["邮编2"],
|
|
86
|
+
"address2": legacy["地址2"],
|
|
87
|
+
"source": legacy.get("source", ""),
|
|
88
|
+
"macauRecipient": bool(legacy.get("macauRecipient")),
|
|
89
|
+
"url": legacy.get("url", ""),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def qqmap(data):
|
|
94
|
+
qq_map.clear()
|
|
95
|
+
for item in data:
|
|
96
|
+
legacy = to_legacy_contact(item)
|
|
97
|
+
id_ = legacy.get("id")
|
|
98
|
+
qq_str = legacy.get("QQ", "")
|
|
99
|
+
if qq_str:
|
|
100
|
+
qq_map[id_] = [qq.strip() for qq in re.split(r"[,,;\s]+", qq_str) if qq.strip()]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def get_contacts(force: bool = False):
|
|
104
|
+
if not force and contacts_cache["contacts"] and time.time() - contacts_cache["at"] < 10 * 60:
|
|
105
|
+
return contacts_cache["contacts"]
|
|
106
|
+
notion_contacts = []
|
|
107
|
+
if config.notion_token and config.contact_data_source_id:
|
|
108
|
+
try:
|
|
109
|
+
rows = await query_all_rows(config.contact_data_source_id)
|
|
110
|
+
notion_contacts = [row_to_contact(row) for row in rows]
|
|
111
|
+
notion_contacts = [c for c in notion_contacts if c.get("name") or c.get("qq") or c.get("phone")]
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning(f"Notion contacts unavailable, using local fallback: {e}")
|
|
114
|
+
merged = merge_contacts(notion_contacts, FALLBACK_CONTACTS)
|
|
115
|
+
contacts = [to_legacy_contact(c) for c in merged]
|
|
116
|
+
qqmap(contacts)
|
|
117
|
+
contacts_cache.update({"at": time.time(), "contacts": contacts})
|
|
118
|
+
return contacts
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def get_key_by_qq(qq_str):
|
|
122
|
+
for key, value in qq_map.items():
|
|
123
|
+
if isinstance(value, list):
|
|
124
|
+
if qq_str in value:
|
|
125
|
+
return key
|
|
126
|
+
else:
|
|
127
|
+
if qq_str == value:
|
|
128
|
+
return key
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def get_name_by_uuid(uuid, data_list=None):
|
|
133
|
+
if data_list is None:
|
|
134
|
+
data_list = await get_contacts()
|
|
135
|
+
for item in data_list:
|
|
136
|
+
if item["id"] == uuid:
|
|
137
|
+
name = item["姓名"]
|
|
138
|
+
if "蛋糕" in name:
|
|
139
|
+
return "可恶的" + name
|
|
140
|
+
return name
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def public_contact(contact_item: dict[str, Any]) -> dict[str, Any]:
|
|
145
|
+
c = to_ai_contact(contact_item)
|
|
146
|
+
return {
|
|
147
|
+
"id": c["id"],
|
|
148
|
+
"name": c["name"],
|
|
149
|
+
"phone": c["phone"],
|
|
150
|
+
"qq": c["qq"],
|
|
151
|
+
"postcode1": c["postcode1"],
|
|
152
|
+
"address1": c["address1"],
|
|
153
|
+
"postcode2": c["postcode2"],
|
|
154
|
+
"address2": c["address2"],
|
|
155
|
+
"source": c["source"],
|
|
156
|
+
"aliases": c.get("aliases") or [],
|
|
157
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# @Version : 0.7.0
|
|
4
|
+
# 规划备注:群聊图片下载、压缩/转 base64、图片临时保存
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
from nonebot.adapters.onebot.v11 import Bot, Event
|
|
14
|
+
|
|
15
|
+
from ..constants import DATA_DIR
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def to_base64(img):
|
|
19
|
+
with open(img, "rb") as im:
|
|
20
|
+
img_bytes = im.read()
|
|
21
|
+
base64_str = "base64://" + base64.b64encode(img_bytes).decode("utf-8")
|
|
22
|
+
return base64_str
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def extract_images(event_json: str) -> list[dict[str, Any]]:
|
|
26
|
+
try:
|
|
27
|
+
data = json.loads(event_json)
|
|
28
|
+
return [msg.get("data", {}) for msg in data.get("message", []) if msg.get("type") == "image"]
|
|
29
|
+
except Exception:
|
|
30
|
+
return []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def _download_image(bot: Bot, image_data: dict[str, Any]) -> bytes:
|
|
34
|
+
url = image_data.get("url")
|
|
35
|
+
if not url and image_data.get("file"):
|
|
36
|
+
try:
|
|
37
|
+
info = await bot.get_image(file=image_data["file"])
|
|
38
|
+
url = info.get("url") or info.get("file")
|
|
39
|
+
except Exception:
|
|
40
|
+
url = ""
|
|
41
|
+
if not url:
|
|
42
|
+
raise RuntimeError("未能获取图片地址")
|
|
43
|
+
async with httpx.AsyncClient(timeout=30) as client:
|
|
44
|
+
resp = await client.get(url)
|
|
45
|
+
resp.raise_for_status()
|
|
46
|
+
return resp.content
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _maybe_compress_jpeg(raw: bytes) -> tuple[bytes, str]:
|
|
50
|
+
try:
|
|
51
|
+
from io import BytesIO
|
|
52
|
+
from PIL import Image
|
|
53
|
+
|
|
54
|
+
image = Image.open(BytesIO(raw))
|
|
55
|
+
image.thumbnail((2200, 2200))
|
|
56
|
+
if image.mode not in ("RGB", "L"):
|
|
57
|
+
image = image.convert("RGB")
|
|
58
|
+
out = BytesIO()
|
|
59
|
+
image.save(out, format="JPEG", quality=92, optimize=True)
|
|
60
|
+
return out.getvalue(), "image/jpeg"
|
|
61
|
+
except Exception:
|
|
62
|
+
return raw, "image/jpeg"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
async def collect_message_images(bot: Bot, event: Event) -> list[dict[str, str]]:
|
|
66
|
+
images = []
|
|
67
|
+
for index, image_data in enumerate(extract_images(event.json())):
|
|
68
|
+
raw = await _download_image(bot, image_data)
|
|
69
|
+
payload, mime = _maybe_compress_jpeg(raw)
|
|
70
|
+
filename = f"mail_recognize_{int(time.time())}_{index}.jpg"
|
|
71
|
+
path = Path(DATA_DIR) / filename
|
|
72
|
+
path.write_bytes(payload)
|
|
73
|
+
images.append({
|
|
74
|
+
"name": filename,
|
|
75
|
+
"path": str(path),
|
|
76
|
+
"dataUrl": f"data:{mime};base64,{base64.b64encode(payload).decode('utf-8')}",
|
|
77
|
+
"barcodeText": "",
|
|
78
|
+
})
|
|
79
|
+
return images
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# @Version : 0.7.0
|
|
4
|
+
# 规划备注:联系人匹配:电话、QQ、姓名/别名、地址片段评分
|
|
5
|
+
# 匹配内容 分数 优先级
|
|
6
|
+
# 电话号码 0.98 最高
|
|
7
|
+
# QQ 号 0.90 很高
|
|
8
|
+
# 完整姓名 / 昵称 0.86 高
|
|
9
|
+
# 地址前 12 字符 0.82 中等
|
|
10
|
+
# 纯姓名(无昵称) 0.76 最低合格线
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .contacts import normalize_text, to_ai_contact, unique
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def match_contact(text: str, contacts: list[dict[str, Any]]):
|
|
18
|
+
haystack = normalize_text(text)
|
|
19
|
+
if not haystack:
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
candidates = []
|
|
23
|
+
for raw_contact in contacts:
|
|
24
|
+
contact_item = to_ai_contact(raw_contact)
|
|
25
|
+
score = 0.0
|
|
26
|
+
names = unique([contact_item.get("name"), *(contact_item.get("aliases") or [])])
|
|
27
|
+
for name in names:
|
|
28
|
+
normalized_name = normalize_text(name)
|
|
29
|
+
if normalized_name and normalized_name in haystack:
|
|
30
|
+
score = max(score, 0.86)
|
|
31
|
+
plain_name = re.sub(r"[((].*?[))]", "", normalized_name)
|
|
32
|
+
if plain_name and len(plain_name) >= 2 and plain_name in haystack:
|
|
33
|
+
score = max(score, 0.76)
|
|
34
|
+
|
|
35
|
+
for phone in re.split(r"[;,,\s]+", str(contact_item.get("phone") or "")):
|
|
36
|
+
if len(phone) >= 7 and normalize_text(phone) in haystack:
|
|
37
|
+
score = max(score, 0.98)
|
|
38
|
+
|
|
39
|
+
for qq in re.split(r"[;,,\s]+", str(contact_item.get("qq") or "")):
|
|
40
|
+
if len(qq) >= 5 and normalize_text(qq) in haystack:
|
|
41
|
+
score = max(score, 0.9)
|
|
42
|
+
|
|
43
|
+
for value in [contact_item.get("address1"), contact_item.get("address2")]:
|
|
44
|
+
normalized_address = normalize_text(value)
|
|
45
|
+
if normalized_address and len(normalized_address) >= 8:
|
|
46
|
+
if normalized_address[:12] in haystack or haystack[:12] in normalized_address:
|
|
47
|
+
score = max(score, 0.82)
|
|
48
|
+
|
|
49
|
+
if score >= 0.76:
|
|
50
|
+
candidates.append({"contact": raw_contact, "score": score})
|
|
51
|
+
|
|
52
|
+
candidates.sort(key=lambda item: item["score"], reverse=True)
|
|
53
|
+
if not candidates:
|
|
54
|
+
return None
|
|
55
|
+
if len(candidates) > 1 and candidates[0]["score"] == candidates[1]["score"]:
|
|
56
|
+
return None
|
|
57
|
+
return {**candidates[0]["contact"], "score": candidates[0]["score"]}
|