Skip to content

Commit 6acbc0e

Browse files
committed
✨ support Mail adapter
1 parent ab93f41 commit 6acbc0e

File tree

11 files changed

+321
-3
lines changed

11 files changed

+321
-3
lines changed

pdm.lock

Lines changed: 98 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ dev = [
6767
"pytest-mock>=3.14.0",
6868
"nonebot-plugin-localstore>=0.7.1",
6969
"pyyaml>=6.0.1",
70+
"nonebot-adapter-mail>=1.0.0a3",
7071
]
7172
[tool.pdm.build]
7273
includes = ["src/nonebot_plugin_alconna"]

src/nonebot_plugin_alconna/uniseg/adapters/console/exporter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def get_adapter(cls) -> SupportAdapter:
2121
def get_target(self, event: Event, bot: Union[Bot, None] = None) -> Target:
2222
return Target(
2323
event.get_user_id(),
24+
private=True,
2425
adapter=self.get_adapter(),
2526
self_id=bot.self_id if bot else None,
2627
scope=SupportScope.console,

src/nonebot_plugin_alconna/uniseg/adapters/discord/builder.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from mimetypes import guess_type
12
from typing import TYPE_CHECKING
23

34
from nonebot.adapters import Bot, Event
@@ -18,7 +19,7 @@
1819

1920
from nonebot_plugin_alconna.uniseg.constraint import SupportAdapter
2021
from nonebot_plugin_alconna.uniseg.builder import MessageBuilder, build
21-
from nonebot_plugin_alconna.uniseg.segment import At, AtAll, Image, Other, Reply, Button, Keyboard
22+
from nonebot_plugin_alconna.uniseg.segment import At, File, AtAll, Audio, Image, Other, Reply, Video, Button, Keyboard
2223

2324

2425
class DiscordMessageBuilder(MessageBuilder):
@@ -54,7 +55,17 @@ def sticker(self, seg: StickerSegment):
5455

5556
@build("attachment")
5657
def attachment(self, seg: AttachmentSegment):
57-
return Image(id=seg.data["attachment"].filename)
58+
mtype = guess_type(seg.data["attachment"].filename)[0]
59+
if mtype and mtype.startswith("image"):
60+
return Image(id=seg.data["attachment"].filename, name=seg.data["attachment"].filename)
61+
if mtype and mtype.startswith("video"):
62+
return Video(id=seg.data["attachment"].filename, name=seg.data["attachment"].filename)
63+
if mtype and mtype.startswith("audio"):
64+
return Audio(id=seg.data["attachment"].filename, name=seg.data["attachment"].filename)
65+
return File(
66+
id=seg.data["attachment"].filename,
67+
name=seg.data["attachment"].filename,
68+
)
5869

5970
@build("reference")
6071
def reference(self, seg: ReferenceSegment):
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from nonebot_plugin_alconna.uniseg.loader import BaseLoader
2+
from nonebot_plugin_alconna.uniseg.constraint import SupportAdapter
3+
4+
5+
class Loader(BaseLoader):
6+
def get_adapter(self) -> SupportAdapter:
7+
return SupportAdapter.mail
8+
9+
def get_builder(self):
10+
from .builder import MailMessageBuilder
11+
12+
return MailMessageBuilder()
13+
14+
def get_exporter(self):
15+
from .exporter import MailMessageExporter
16+
17+
return MailMessageExporter()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import TYPE_CHECKING
2+
3+
from nonebot.adapters import Bot, Event
4+
from nonebot.adapters.mail.event import NewMailMessageEvent
5+
from nonebot.adapters.mail.message import Html, Attachment, MessageSegment
6+
7+
from nonebot_plugin_alconna.uniseg.constraint import SupportAdapter
8+
from nonebot_plugin_alconna.uniseg.builder import MessageBuilder, build
9+
from nonebot_plugin_alconna.uniseg.segment import File, Text, Audio, Image, Reply, Video
10+
11+
12+
class MailMessageBuilder(MessageBuilder[MessageSegment]):
13+
@classmethod
14+
def get_adapter(cls) -> SupportAdapter:
15+
return SupportAdapter.mail
16+
17+
@build("html")
18+
def html(self, seg: Html):
19+
return Text(seg.data["html"]).mark(0, len(seg.data["html"]), "html")
20+
21+
@build("attachment")
22+
def attachment(self, seg: Attachment):
23+
mtype = seg.data["content_type"]
24+
if mtype and mtype.startswith("image"):
25+
return Image(
26+
raw=seg.data["data"],
27+
mimetype=mtype,
28+
name=seg.data["name"],
29+
)
30+
if mtype and mtype.startswith("audio"):
31+
return Audio(
32+
raw=seg.data["data"],
33+
mimetype=mtype,
34+
name=seg.data["name"],
35+
)
36+
if mtype and mtype.startswith("video"):
37+
return Video(
38+
raw=seg.data["data"],
39+
mimetype=mtype,
40+
name=seg.data["name"],
41+
)
42+
return File(
43+
raw=seg.data["data"],
44+
mimetype=seg.data["content_type"],
45+
name=seg.data["name"],
46+
)
47+
48+
async def extract_reply(self, event: Event, bot: Bot):
49+
if TYPE_CHECKING:
50+
assert isinstance(event, NewMailMessageEvent)
51+
52+
if event.reply:
53+
return Reply(event.reply.id, msg=event.reply.message, origin=event.reply)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from pathlib import Path
2+
from typing import Union
3+
4+
from tarina import lang
5+
from nonebot.adapters import Bot, Event
6+
from nonebot.internal.driver import Request
7+
from nonebot.adapters.mail import Bot as MailBot
8+
from nonebot.adapters.mail.event import NewMailMessageEvent
9+
from nonebot.adapters.mail.message import Message, MessageSegment
10+
11+
from nonebot_plugin_alconna.uniseg.constraint import SupportScope, SerializeFailed
12+
from nonebot_plugin_alconna.uniseg.exporter import Target, SupportAdapter, MessageExporter, export
13+
from nonebot_plugin_alconna.uniseg.segment import At, File, Text, Audio, Image, Reply, Video, Voice
14+
15+
16+
class MailMessageExporter(MessageExporter[Message]):
17+
def get_message_type(self):
18+
return Message
19+
20+
@classmethod
21+
def get_adapter(cls) -> SupportAdapter:
22+
return SupportAdapter.mail
23+
24+
def get_target(self, event: Event, bot: Union[Bot, None] = None) -> Target:
25+
assert isinstance(event, NewMailMessageEvent)
26+
return Target(
27+
event.get_user_id(),
28+
private=True,
29+
adapter=self.get_adapter(),
30+
self_id=bot.self_id if bot else None,
31+
scope=SupportScope.mail,
32+
)
33+
34+
def get_message_id(self, event: Event) -> str:
35+
assert isinstance(event, NewMailMessageEvent)
36+
return event.id
37+
38+
@export
39+
async def text(self, seg: Text, bot: Union[Bot, None]) -> "MessageSegment":
40+
if not seg.styles:
41+
return MessageSegment.text(seg.text)
42+
style = seg.extract_most_style()
43+
if style == "link":
44+
if not getattr(seg, "_children", []):
45+
return MessageSegment.html(f'<a href="{seg.text}">{seg.text}</a>')
46+
else:
47+
return MessageSegment.html(f'<a href="{seg.text}">{seg._children[0].text}</a>') # type: ignore
48+
return MessageSegment.html(str(seg))
49+
50+
@export
51+
async def at(self, seg: At, bot: Union[Bot, None]) -> "MessageSegment":
52+
if seg.flag == "user":
53+
return MessageSegment.html(f'<a href="mailto:{seg.target}">@{seg.target}</a>')
54+
elif seg.flag == "channel":
55+
return MessageSegment.html(f" #{seg.target}")
56+
else:
57+
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type="at", seg=seg))
58+
59+
@export
60+
async def media(self, seg: Union[Image, Voice, Video, Audio, File], bot: Union[Bot, None]) -> "MessageSegment":
61+
name = seg.__class__.__name__.lower()
62+
63+
if seg.raw and (seg.id or seg.name):
64+
return MessageSegment.attachment(seg.raw, seg.id or seg.name, seg.mimetype)
65+
elif seg.path:
66+
path = Path(seg.path)
67+
return MessageSegment.attachment(path, path.name)
68+
elif bot and seg.url:
69+
if name == "image":
70+
return MessageSegment.html(f'<img src="{seg.url}" />')
71+
elif name == "video":
72+
return MessageSegment.html(f'<video src="{seg.url}" controls />')
73+
elif name in ["audio", "voice"]:
74+
return MessageSegment.html(f'<audio src="{seg.url}" controls />')
75+
resp = await bot.adapter.request(Request("GET", seg.url))
76+
return MessageSegment.attachment(
77+
resp.content, # type: ignore
78+
seg.id or seg.name or seg.url.split("/")[-1],
79+
)
80+
else:
81+
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type=name, seg=seg))
82+
83+
@export
84+
async def reply(self, seg: Reply, bot: Union[Bot, None]) -> "MessageSegment":
85+
return MessageSegment("mail:reply", {"message_id": seg.id}) # type: ignore
86+
87+
async def send_to(self, target: Union[Target, Event], bot: Bot, message: Message, **kwargs):
88+
assert isinstance(bot, MailBot)
89+
90+
in_reply_to = None
91+
if message.has("$mail:reply"):
92+
reply = message["mail:reply", 0]
93+
message = message.exclude("mail:reply")
94+
in_reply_to = reply.data["message_id"]
95+
96+
if isinstance(target, Event):
97+
return await bot.send(target, message, in_reply_to=in_reply_to, **kwargs) # type: ignore
98+
return await bot.send_to(recipient=target.id, message=message, in_reply_to=in_reply_to, **kwargs)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import TYPE_CHECKING, Union
2+
3+
from nonebot.adapters import Bot
4+
from nonebot.adapters.mail.bot import Bot as MailBot
5+
6+
from nonebot_plugin_alconna.uniseg.target import Target, TargetFetcher
7+
from nonebot_plugin_alconna.uniseg.constraint import SupportScope, SupportAdapter
8+
9+
10+
class MailTargetFetcher(TargetFetcher):
11+
@classmethod
12+
def get_adapter(cls) -> SupportAdapter:
13+
return SupportAdapter.mail
14+
15+
async def fetch(self, bot: Bot, target: Union[Target, None] = None):
16+
if TYPE_CHECKING:
17+
assert isinstance(bot, MailBot)
18+
if target and not target.private:
19+
return
20+
for uid in await bot.get_unseen_uids():
21+
yield Target(uid, private=True, adapter=self.get_adapter(), self_id=bot.self_id, scope=SupportScope.mail)

0 commit comments

Comments
 (0)