diff --git a/docs/guide/feishu-guide.md b/docs/guide/feishu-guide.md index 342ea3af..b58aca83 100644 --- a/docs/guide/feishu-guide.md +++ b/docs/guide/feishu-guide.md @@ -11,7 +11,7 @@ pip install nonebot-adapter-feishu ## 创建应用与启用应用“机器人”能力 ::: tip -此部分可参考[飞书开放平台-快速开发机器人-创建应用](https://open.feishu.cn/document/home/develop-a-bot-in-5-minutes/create-an-app)部分文档。 +此部分可参考[飞书开放平台-快速开发机器人-创建应用](https://open.feishu.cn/document/home/develop-a-bot-in-5-minutes/create-an-app)部分的文档。 ::: @@ -44,7 +44,7 @@ pip install nonebot-adapter-feishu ## 在 NoneBot 配置中添加相应配置 -在 `.env` 文件中添加以下部分 +在 `.env` 文件中添加以下配置 ``` APP_ID= @@ -52,6 +52,51 @@ APP_SECRET= VERIFICATION_TOKEN= ``` -复制所创建应用**“凭证和基础信息”**中的**App ID**与**App Secret**及**“事件订阅”**中的**Verification Token**,替换上面相应的配置的值。 +复制所创建应用**“凭证和基础信息”**中的 **App ID** 、 **App Secret** 和 **“事件订阅”** 中的 **Verification Token** ,替换上面相应的配置的值。 + +此外,对于飞书平台的事件订阅加密机制,飞书适配器也提供 `ENCRYPT_KEY` 配置项。 + +``` +ENCRYPT_KEY= +``` + +当此项不为空时,飞书适配器会认为用户启用了加密机制,并对事件上报中的密文进行解密。 + +对于[Lark(飞书平台海外版)](https://www.larksuite.com) 的用户,飞书适配器也提供**实验性**支持,仅需要在配置文件中添加 `IS_LARK=true` 即可。 + +``` +IS_LARK=true +``` + +## 注册飞书适配器 + +在 `bot.py` 中添加: + +```python +from nonebot.adapters.feishu import Bot as FeishuBot + +driver.register_adapter("feishu", FeishuBot) +``` + +## 编写一个适用于飞书适配器的插件并加载 + +插件代码范例: + +```python +from nonebot.plugin import on_command +from nonebot.typing import T_State +from nonebot.adapters.feishu import Bot as FeishuBot, MessageEvent + +helper = on_command("say") + + +@helper.handle() +async def feishu_helper(bot: FeishuBot, event: MessageEvent, state: T_State): + message = event.get_message() + await helper.finish(message, at_sender=True) +``` + +以上代码注册了一个对飞书平台适用的`say`指令,并会提取`/say`之后的内容发送到事件所对应的群或私聊。 + +大功告成!现在可以试试向机器人发送类似`/say Hello, Feishu!`的消息进行测试了。 -大功告成!现在可以试试向机器人发送消息进行测试了。 diff --git a/packages/nonebot-adapter-feishu/nonebot/adapters/feishu/event.py b/packages/nonebot-adapter-feishu/nonebot/adapters/feishu/event.py index f2b5e2c9..9c35ea9e 100644 --- a/packages/nonebot-adapter-feishu/nonebot/adapters/feishu/event.py +++ b/packages/nonebot-adapter-feishu/nonebot/adapters/feishu/event.py @@ -18,6 +18,8 @@ class EventHeader(BaseModel): token: str app_id: str tenant_key: str + resource_id: Optional[str] + user_list: Optional[List[dict]] class Event(BaseEvent): @@ -27,8 +29,9 @@ class Event(BaseEvent): .. _飞书文档: https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-list """ + __event__ = "" - schema_: str = Field("", alias='schema') + schema_: str = Field("", alias="schema") header: EventHeader event: Any @@ -136,8 +139,10 @@ class EventMessage(BaseModel): @root_validator(pre=True) def parse_message(cls, values: dict): values["content"] = MessageDeserializer( - values["message_type"], json.loads(values["content"]), - values.get("mentions")).deserialize() + values["message_type"], + json.loads(values["content"]), + values.get("mentions"), + ).deserialize() return values @@ -187,7 +192,8 @@ class MessageEvent(Event): return ( f"{self.event.message.message_id} from {self.get_user_id()}" f"@[{self.event.message.chat_type}:{self.event.message.chat_id}]" - f" {self.get_message()}") + f" {self.get_message()}" + ) @overrides(Event) def get_message(self) -> Message: @@ -395,6 +401,460 @@ class GroupMemberUserDeletedEvent(NoticeEvent): event: GroupMemberUserDeletedEventDetail +class AvatarInfo(BaseModel): + avatar_72: str + avatar_240: str + avatar_640: str + avatar_origin: str + + +class UserStatus(BaseModel): + is_frozen: bool + is_resigned: bool + is_activated: bool + + +class UserOrder(BaseModel): + department_id: str + user_order: int + department_order: int + + +class UserCustomAttrValue(BaseModel): + text: str + url: str + pc_url: str + + +class UserCustomAttr(BaseModel): + type: str + id: str + value: UserCustomAttrValue + + +class ContactUser(BaseModel): + open_id: str + user_id: str + name: str + en_name: str + email: str + mobile: str + gender: int + avatar: AvatarInfo + status: UserStatus + department_ids: Optional[List[str]] + leader_user_id: str + city: str + country: str + work_station: str + join_time: int + employee_no: str + employee_type: int + orders: Optional[List[UserOrder]] + custom_attrs: List[UserCustomAttr] + + +class OldContactUser(BaseModel): + department_ids: List[str] + open_id: str + + +class ContactUserUpdatedEventDetail(BaseModel): + object: ContactUser + old_object: ContactUser + + +class ContactUserUpdatedEvent(NoticeEvent): + __event__ = "contact.user.updated_v3" + event: ContactUserUpdatedEventDetail + + +class ContactUserDeletedEventDetail(NoticeEvent): + object: ContactUser + old_object: OldContactUser + + +class ContactUserDeletedEvent(NoticeEvent): + __event__ = "contact.user.deleted_v3" + event: ContactUserDeletedEventDetail + + +class ContactUserCreatedEventDetail(BaseModel): + object: ContactUser + + +class ContactUserCreatedEvent(NoticeEvent): + __event__ = "contact.user.created_v3" + event: ContactUserCreatedEventDetail + + +class ContactDepartmentStatus(BaseModel): + is_deleted: bool + + +class ContactDepartment(BaseModel): + name: str + parent_department_id: str + department_id: str + open_department_id: str + leader_user_id: str + chat_id: str + order: int + status: ContactDepartmentStatus + + +class ContactDepartmentUpdatedEventDetail(BaseModel): + object: ContactDepartment + old_object: ContactDepartment + + +class ContactDepartmentUpdatedEvent(NoticeEvent): + __event__ = "contact.department.updated_v3" + event: ContactDepartmentUpdatedEventDetail + + +class OldContactDepartment(BaseModel): + status: ContactDepartmentStatus + open_department_id: str + + +class ContactDepartmentDeletedEventDetail(NoticeEvent): + object: ContactDepartment + old_object: OldContactDepartment + + +class ContactDepartmentDeletedEvent(NoticeEvent): + __event__ = "contact.department.deleted_v3" + event: ContactDepartmentDeletedEventDetail + + +class ContactDepartmentCreatedEventDetail(BaseModel): + object: ContactDepartment + + +class ContactDepartmentCreatedEvent(NoticeEvent): + __event__ = "contact.department.created_v3" + event: ContactDepartmentCreatedEventDetail + + +class CalendarAclScope(BaseModel): + type: str + user_id: str + + +class CalendarAclCreatedEventDetail(BaseModel): + acl_id: str + role: str + scope: CalendarAclScope + + +class CalendarAclCreatedEvent(NoticeEvent): + __event__ = "calendar.calendar.acl.created_v4" + event: CalendarAclCreatedEventDetail + + +class CalendarAclDeletedEventDetail(BaseModel): + acl_id: str + role: str + scope: CalendarAclScope + + +class CalendarAclDeletedEvent(NoticeEvent): + __event__ = "calendar.calendar.acl.deleted_v4" + event: CalendarAclDeletedEventDetail + + +class CalendarChangedEvent(NoticeEvent): + __event__ = "calendar.calendar.changed_v4" + event: dict + + +class CalendarEventChangedEventDetail(BaseModel): + calendar_id: str + + +class CalendarEventChangedEvent(NoticeEvent): + __event__ = "calendar.calendar.event.changed_v4" + event: CalendarEventChangedEventDetail + + +class DriveFileReadEventDetail(BaseModel): + file_token: str + file_type: str + operator_id_list: List[UserId] + + +class DriveFileReadEvent(NoticeEvent): + __event__ = "drive.file.read_v1" + event: DriveFileReadEventDetail + + +class DriveFileTitleUpdatedEventDetail(BaseModel): + file_token: str + file_type: str + operator_id: UserId + + +class DriveFileTitleUpdatedEvent(NoticeEvent): + __event__ = "drive.file.title_updated_v1" + event: DriveFileTitleUpdatedEventDetail + + +class DriveFilePermissionMemberAddedEventDetail(BaseModel): + chat_list: List[str] + file_token: str + file_type: str + operator_id: UserId + user_list: List[UserId] + + +class DriveFilePermissionMemberAddedEvent(NoticeEvent): + __event__ = "drive.file.permission_member_added_v1" + event: DriveFilePermissionMemberAddedEventDetail + + +class DriveFilePermissionMemberRemovedEventDetail(BaseModel): + chat_list: List[str] + file_token: str + file_type: str + operator_id: UserId + user_list: List[UserId] + + +class DriveFilePermissionMemberRemovedEvent(NoticeEvent): + __event__ = "drive.file.permission_member_removed_v1" + event: DriveFilePermissionMemberRemovedEventDetail + + +class DriveFileTrashedEventDetail(BaseModel): + file_token: str + file_type: str + operator_id: UserId + + +class DriveFileTrashedEvent(NoticeEvent): + __event__ = "drive.file.trashed_v1" + event: DriveFileTrashedEventDetail + + +class DriveFileDeletedEventDetail(BaseModel): + file_token: str + file_type: str + operator_id: UserId + + +class DriveFileDeletedEvent(NoticeEvent): + __event__ = "drive.file.deleted_v1" + event: DriveFileDeletedEventDetail + + +class DriveFileEditedEventDetail(BaseModel): + file_token: str + file_type: str + operator_id_list: List[UserId] + subscriber_id_list: List[UserId] + + +class DriveFileEditedEvent(NoticeEvent): + __event__ = "drive.file.edit_v1" + event: DriveFileEditedEventDetail + + +class MeetingRoomCreatedEventDetail(BaseModel): + room_id: str + room_name: str + + +class MeetingRoomCreatedEvent(NoticeEvent): + __event__ = "meeting_room.meeting_room.created_v1" + event: MeetingRoomCreatedEventDetail + + +class MeetingRoomUpdatedEventDetail(BaseModel): + room_id: str + room_name: str + + +class MeetingRoomUpdatedEvent(NoticeEvent): + __event__ = "meeting_room.meeting_room.updated_v1" + event: MeetingRoomUpdatedEventDetail + + +class MeetingRoomDeletedEventDetail(BaseModel): + room_id: str + room_name: str + + +class MeetingRoomDeletedEvent(NoticeEvent): + __event__ = "meeting_room.meeting_room.deleted_v1" + event: MeetingRoomDeletedEventDetail + + +class MeetingRoomStatusChangedEventDetail(BaseModel): + room_id: str + room_name: str + + +class MeetingRoomStatusChangedEvent(NoticeEvent): + __event__ = "meeting_room.meeting_room.status_changed_v1" + event: MeetingRoomStatusChangedEventDetail + + +class MeetingUser(BaseModel): + id: UserId + user_role: Optional[int] + user_type: Optional[int] + + +class Meeting(BaseModel): + id: str + topic: str + meeting_no: str + start_time: Optional[str] + end_time: Optional[str] + host_user: Optional[MeetingUser] + owner: MeetingUser + + +class VCMeetingStartedEventDetail(BaseModel): + meeting: Meeting + operator: MeetingUser + + +class VCMeetingStartedEvent(NoticeEvent): + __event__ = "vc.meeting.meeting_started_v1" + event: VCMeetingStartedEventDetail + + +class VCMeetingEndedEventDetail(BaseModel): + meeting: Meeting + operator: MeetingUser + + +class VCMeetingEndedEvent(NoticeEvent): + __event__ = "vc.meeting.meeting_ended_v1" + event: VCMeetingEndedEventDetail + + +class VCMeetingJoinedEventDetail(BaseModel): + meeting: Meeting + operator: MeetingUser + + +class VCMeetingJoinedEvent(NoticeEvent): + __event__ = "vc.meeting.join_meeting_v1" + event: VCMeetingJoinedEventDetail + + +class VCMeetingLeftEventDetail(BaseModel): + meeting: Meeting + operator: MeetingUser + leave_reason: int + + +class VCMeetingLeftEvent(NoticeEvent): + __event__ = "vc.meeting.leave_meeting_v1" + event: VCMeetingLeftEventDetail + + +class VCMeetingRecordingStartedEventDetail(BaseModel): + meeting: Meeting + operator: MeetingUser + + +class VCMeetingRecordingStartedEvent(NoticeEvent): + __event__ = "vc.meeting.recording_started_v1" + event: VCMeetingRecordingStartedEventDetail + + +class VCMeetingRecordingEndedEventDetail(BaseModel): + meeting: Meeting + operator: MeetingUser + + +class VCMeetingRecordingEndedEvent(NoticeEvent): + __event__ = "vc.meeting.recording_ended_v1" + event: VCMeetingRecordingEndedEventDetail + + +class VCMeetingRecordingReadyEventDetail(BaseModel): + meeting: Meeting + url: str + duration: str + + +class VCMeetingRecordingReadyEvent(NoticeEvent): + __event__ = "vc.meeting.recording_ready_v1" + event: VCMeetingRecordingReadyEventDetail + + +class VCMeetingShareStartedEventDetail(BaseModel): + meeting: Meeting + operator: MeetingUser + + +class VCMeetingShareStartedEvent(NoticeEvent): + __event__ = "vc.meeting.share_started_v1" + event: VCMeetingShareStartedEventDetail + + +class VCMeetingShareEndedEventDetail(BaseModel): + meeting: Meeting + operator: MeetingUser + + +class VCMeetingShareEndedEvent(NoticeEvent): + __event__ = "vc.meeting.share_ended_v1" + event: VCMeetingShareEndedEventDetail + + +class AttendanceUserFlowCreatedEventDetail(BaseModel): + bssid: str + check_time: str + comment: str + employee_id: str + employee_no: str + is_field: bool + is_wifi: bool + latitude: float + location_name: str + longitude: float + photo_urls: Optional[List[str]] + record_id: str + ssid: str + type: int + + +class AttendanceUserFlowCreatedEvent(NoticeEvent): + __event__ = "attendance.user_flow.created_v1" + event: AttendanceUserFlowCreatedEventDetail + + +class AttendanceUserTaskStatusDiff(BaseModel): + before_status: str + before_supplement: str + current_status: str + current_supplement: str + index: int + work_type: str + + +class AttendanceUserTaskUpdatedEventDetail(BaseModel): + date: int + employee_id: str + employee_no: str + group_id: str + shift_id: str + status_changes: List[AttendanceUserTaskStatusDiff] + task_id: str + time_zone: str + + +class AttendanceUserTaskUpdatedEvent(NoticeEvent): + __event__ = "attendance.user_task.updated_v1" + event: AttendanceUserTaskUpdatedEventDetail + + _t = StringTrie(separator=".") # define `model` first to avoid globals changing while `for` diff --git a/tests/test_plugins/test_feishu.py b/tests/test_plugins/test_feishu.py index 54a0d7ae..960ae377 100644 --- a/tests/test_plugins/test_feishu.py +++ b/tests/test_plugins/test_feishu.py @@ -1,12 +1,11 @@ -from nonebot.adapters.feishu.event import MessageEvent -from nonebot.rule import to_me from nonebot.plugin import on_command -from nonebot.adapters.feishu import Bot as FeishuBot, MessageSegment, MessageEvent +from nonebot.typing import T_State +from nonebot.adapters.feishu import Bot as FeishuBot, MessageEvent helper = on_command("say") @helper.handle() -async def feishu_helper(bot: FeishuBot, event: MessageEvent): +async def feishu_helper(bot: FeishuBot, event: MessageEvent, state: T_State): message = event.get_message() await helper.finish(message, at_sender=True)