From 01b838fe4fd4e9316c2b6c2dd051a0c56f815674 Mon Sep 17 00:00:00 2001 From: "Vicente.Yu" <^@^> Date: Sun, 8 Sep 2024 23:28:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7.net=208.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加事件回调服务 增加长连接服务 更新事件回调使用方法 --- .gitignore | 1 - EventCallbackList.md | 158 +++++++ .../FeishuNetSdk.Endpoint.csproj | 39 ++ .../FeishuNetSdkEndpointExtension.cs | 53 +++ FeishuNetSdk.Endpoint/LICENSE | 21 + .../Properties/launchSettings.json | 12 + FeishuNetSdk.Endpoint/README.md | 45 ++ .../FeishuNetSdk.WebSocket.csproj | 43 ++ .../FeishuNetSdkWebSocketExtensions.cs | 41 ++ FeishuNetSdk.WebSocket/FrameProto.cs | 150 +++++++ FeishuNetSdk.WebSocket/LICENSE | 21 + FeishuNetSdk.WebSocket/README.md | 45 ++ FeishuNetSdk.WebSocket/WssService.cs | 136 ++++++ FeishuNetSdk.sln | 12 + README.md | 90 +++- samples/WebApplication1/Program.cs | 26 +- src/Core/EventDto.cs | 344 +++++++++++++++ .../FeishuNetSdkExtensions.cs | 8 +- src/Extensions/DtoExtensions.cs | 23 + src/Extensions/InnerExtensions.cs | 135 ++++++ src/FeishuNetSdk.csproj | 9 +- src/Services/CallbackV2Dto.cs | 30 ++ src/Services/EventCallbackServiceProvider.cs | 203 +++++++++ src/Services/EventDto.cs | 35 ++ src/Services/EventHandlerDescriptor.cs | 46 ++ src/Services/EventV1Dto.cs | 48 +++ src/Services/EventV2Dto.cs | 87 ++++ src/Services/IEventCallbackHandler.cs | 58 +++ src/Services/IEventCallbackServiceProvider.cs | 42 ++ src/Services/ReflectionHelper.cs | 407 ++++++++++++++++++ src/Services/UrlVerificationDto.cs | 30 ++ 31 files changed, 2378 insertions(+), 20 deletions(-) create mode 100644 EventCallbackList.md create mode 100644 FeishuNetSdk.Endpoint/FeishuNetSdk.Endpoint.csproj create mode 100644 FeishuNetSdk.Endpoint/FeishuNetSdkEndpointExtension.cs create mode 100644 FeishuNetSdk.Endpoint/LICENSE create mode 100644 FeishuNetSdk.Endpoint/Properties/launchSettings.json create mode 100644 FeishuNetSdk.Endpoint/README.md create mode 100644 FeishuNetSdk.WebSocket/FeishuNetSdk.WebSocket.csproj create mode 100644 FeishuNetSdk.WebSocket/FeishuNetSdkWebSocketExtensions.cs create mode 100644 FeishuNetSdk.WebSocket/FrameProto.cs create mode 100644 FeishuNetSdk.WebSocket/LICENSE create mode 100644 FeishuNetSdk.WebSocket/README.md create mode 100644 FeishuNetSdk.WebSocket/WssService.cs create mode 100644 src/Core/EventDto.cs create mode 100644 src/Extensions/InnerExtensions.cs create mode 100644 src/Services/CallbackV2Dto.cs create mode 100644 src/Services/EventCallbackServiceProvider.cs create mode 100644 src/Services/EventDto.cs create mode 100644 src/Services/EventHandlerDescriptor.cs create mode 100644 src/Services/EventV1Dto.cs create mode 100644 src/Services/EventV2Dto.cs create mode 100644 src/Services/IEventCallbackHandler.cs create mode 100644 src/Services/IEventCallbackServiceProvider.cs create mode 100644 src/Services/ReflectionHelper.cs create mode 100644 src/Services/UrlVerificationDto.cs diff --git a/.gitignore b/.gitignore index 3524b8af..1ee53850 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,3 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd - diff --git a/EventCallbackList.md b/EventCallbackList.md new file mode 100644 index 00000000..a421752d --- /dev/null +++ b/EventCallbackList.md @@ -0,0 +1,158 @@ +## 事件回调类型清单 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
事件代码事件类型描述
approval.approval.updated_v4EventV2Dto<Approval.Events.ApprovalApprovalUpdatedV4EventBodyDto>【审批】审批定义更新
approval.instance.trip_group_update_v4EventV2Dto<Approval.Events.ApprovalInstanceTripGroupUpdateV4EventBodyDto>【审批】出差审批
approval.instance.remedy_group_update_v4EventV2Dto<Approval.Events.ApprovalInstanceRemedyGroupUpdateV4EventBodyDto>【审批】补卡审批
approval_ccEventV1Dto<Approval.Events.ApprovalCcEventBodyDto>【审批】审批抄送状态变更
approval_taskEventV1Dto<Approval.Events.ApprovalTaskEventBodyDto>【审批】审批任务状态变更
approval_instanceEventV1Dto<Approval.Events.ApprovalInstanceEventBodyDto>【审批】审批实例状态变更
approvalEventV1Dto<Approval.Events.ApprovalEventBodyDto>【审批】审批通过通知
out_approvalEventV1Dto<Approval.Events.OutApprovalEventBodyDto>【审批】外出审批
shift_approvalEventV1Dto<Approval.Events.ShiftApprovalEventBodyDto>【审批】换班审批
work_approvalEventV1Dto<Approval.Events.WorkApprovalEventBodyDto>【审批】加班审批
leave_approvalV2EventV1Dto<Approval.Events.LeaveApprovalV2EventBodyDto>【审批】请假审批
leave_approval_revertEventV1Dto<Approval.Events.LeaveApprovalRevertEventBodyDto>【审批】请假撤销
url.preview.getCallbackV2Dto<CallbackEvents.UrlPreviewGetEventBodyDto>【回调】拉取链接预览数据
card.action.triggerCallbackV2Dto<CallbackEvents.CardActionTriggerEventBodyDto>【回调】卡片回传交互
corehr.process.cc.updated_v2EventV2Dto<Corehr.Events.CorehrProcessCcUpdatedV2EventBodyDto>【飞书人事(企业版)】抄送单据状态变更
corehr.offboarding.checklist_updated_v2EventV2Dto<Corehr.Events.CorehrOffboardingChecklistUpdatedV2EventBodyDto>【飞书人事(企业版)】离职流转状态变更
corehr.offboarding.status_updated_v2EventV2Dto<Corehr.Events.CorehrOffboardingStatusUpdatedV2EventBodyDto>【飞书人事(企业版)】离职申请状态变更
corehr.offboarding.updated_v2EventV2Dto<Corehr.Events.CorehrOffboardingUpdatedV2EventBodyDto>【飞书人事(企业版)】离职信息变更
corehr.process.node.updated_v2EventV2Dto<Corehr.Events.CorehrProcessNodeUpdatedV2EventBodyDto>【飞书人事(企业版)】流程节点状态变更
corehr.process.updated_v2EventV2Dto<Corehr.Events.CorehrProcessUpdatedV2EventBodyDto>【飞书人事(企业版)】流程实例信息变更
corehr.employee.domain_event_v2EventV2Dto<Corehr.Events.CorehrEmployeeDomainEventV2EventBodyDto>【飞书人事(企业版)】人员信息变更
corehr.process.approver.updated_v2EventV2Dto<Corehr.Events.CorehrProcessApproverUpdatedV2EventBodyDto>【飞书人事(企业版)】审批任务状态变更
corehr.probation.updated_v2EventV2Dto<Corehr.Events.CorehrProbationUpdatedV2EventBodyDto>【飞书人事(企业版)】试用期状态变更
corehr.job_change.updated_v2EventV2Dto<Corehr.Events.CorehrJobChangeUpdatedV2EventBodyDto>【飞书人事(企业版)】异动信息变更
corehr.job_change.status_updated_v2EventV2Dto<Corehr.Events.CorehrJobChangeStatusUpdatedV2EventBodyDto>【飞书人事(企业版)】异动状态变更
corehr.department.created_v1EventV2Dto<FeishuPeople.Events.CorehrDepartmentCreatedV1EventBodyDto>【飞书人事】【事件】创建部门
corehr.employment.created_v1EventV2Dto<FeishuPeople.Events.CorehrEmploymentCreatedV1EventBodyDto>【飞书人事】【事件】创建雇佣信息
corehr.person.created_v1EventV2Dto<FeishuPeople.Events.CorehrPersonCreatedV1EventBodyDto>【飞书人事】【事件】个人信息创建
corehr.person.deleted_v1EventV2Dto<FeishuPeople.Events.CorehrPersonDeletedV1EventBodyDto>【飞书人事】【事件】个人信息删除
corehr.department.updated_v1EventV2Dto<FeishuPeople.Events.CorehrDepartmentUpdatedV1EventBodyDto>【飞书人事】【事件】更新部门
corehr.person.updated_v1EventV2Dto<FeishuPeople.Events.CorehrPersonUpdatedV1EventBodyDto>【飞书人事】【事件】更新个人信息
corehr.employment.updated_v1EventV2Dto<FeishuPeople.Events.CorehrEmploymentUpdatedV1EventBodyDto>【飞书人事】【事件】更新雇佣信息
corehr.department.deleted_v1EventV2Dto<FeishuPeople.Events.CorehrDepartmentDeletedV1EventBodyDto>【飞书人事】【事件】删除部门
corehr.employment.deleted_v1EventV2Dto<FeishuPeople.Events.CorehrEmploymentDeletedV1EventBodyDto>【飞书人事】【事件】删除雇佣信息
corehr.org_role_authorization.updated_v1EventV2Dto<FeishuPeople.Events.CorehrOrgRoleAuthorizationUpdatedV1EventBodyDto>【飞书人事】【事件】组织角色授权变更
corehr.job.created_v1EventV2Dto<FeishuPeople.Events.CorehrJobCreatedV1EventBodyDto>【飞书人事】创建职务
corehr.job.updated_v1EventV2Dto<FeishuPeople.Events.CorehrJobUpdatedV1EventBodyDto>【飞书人事】更新职务
corehr.contract.created_v1EventV2Dto<FeishuPeople.Events.CorehrContractCreatedV1EventBodyDto>【飞书人事】合同创建
corehr.contract.updated_v1EventV2Dto<FeishuPeople.Events.CorehrContractUpdatedV1EventBodyDto>【飞书人事】合同更新
corehr.contract.deleted_v1EventV2Dto<FeishuPeople.Events.CorehrContractDeletedV1EventBodyDto>【飞书人事】合同删除
corehr.offboarding.updated_v1EventV2Dto<FeishuPeople.Events.CorehrOffboardingUpdatedV1EventBodyDto>【飞书人事】离职申请状态变更(不推荐)
corehr.job_data.created_v1EventV2Dto<FeishuPeople.Events.CorehrJobDataCreatedV1EventBodyDto>【飞书人事】任职信息创建
corehr.job_data.updated_v1EventV2Dto<FeishuPeople.Events.CorehrJobDataUpdatedV1EventBodyDto>【飞书人事】任职信息更新
corehr.job_data.deleted_v1EventV2Dto<FeishuPeople.Events.CorehrJobDataDeletedV1EventBodyDto>【飞书人事】任职信息删除
corehr.pre_hire.updated_v1EventV2Dto<FeishuPeople.Events.CorehrPreHireUpdatedV1EventBodyDto>【飞书人事】入职信息变更
corehr.job.deleted_v1EventV2Dto<FeishuPeople.Events.CorehrJobDeletedV1EventBodyDto>【飞书人事】删除职务
corehr.job_change.updated_v1EventV2Dto<FeishuPeople.Events.CorehrJobChangeUpdatedV1EventBodyDto>【飞书人事】异动状态变更(不推荐)
corehr.employment.resigned_v1EventV2Dto<FeishuPeople.Events.CorehrEmploymentResignedV1EventBodyDto>【飞书人事】员工完成离职
corehr.job_data.employed_v1EventV2Dto<FeishuPeople.Events.CorehrJobDataEmployedV1EventBodyDto>【飞书人事】员工完成入职
corehr.job_data.changed_v1EventV2Dto<FeishuPeople.Events.CorehrJobDataChangedV1EventBodyDto>【飞书人事】员工完成异动
corehr.employment.converted_v1EventV2Dto<FeishuPeople.Events.CorehrEmploymentConvertedV1EventBodyDto>【飞书人事】员工完成转正
helpdesk.ticket.created_v1EventV2Dto<Helpdesk.Events.HelpdeskTicketCreatedV1EventBodyDto>【服务台】创建工单
helpdesk.ticket_message.created_v1EventV2Dto<Helpdesk.Events.HelpdeskTicketMessageCreatedV1EventBodyDto>【服务台】工单消息事件
helpdesk.ticket.updated_v1EventV2Dto<Helpdesk.Events.HelpdeskTicketUpdatedV1EventBodyDto>【服务台】工单状态变更
helpdesk.notification.approve_v1EventV2Dto<Helpdesk.Events.HelpdeskNotificationApproveV1EventBodyDto>【服务台】推送审核通知
moments.reaction.created_v1EventV2Dto<Moments.Events.MomentsReactionCreatedV1EventBodyDto>【公司圈】表情互动
moments.dislike.created_v1EventV2Dto<Moments.Events.MomentsDislikeCreatedV1EventBodyDto>【公司圈】点踩
moments.comment.created_v1EventV2Dto<Moments.Events.MomentsCommentCreatedV1EventBodyDto>【公司圈】发布评论
moments.post.created_v1EventV2Dto<Moments.Events.MomentsPostCreatedV1EventBodyDto>【公司圈】发布帖子
moments.reaction.deleted_v1EventV2Dto<Moments.Events.MomentsReactionDeletedV1EventBodyDto>【公司圈】取消表情互动
moments.dislike.deleted_v1EventV2Dto<Moments.Events.MomentsDislikeDeletedV1EventBodyDto>【公司圈】取消点踩
moments.comment.deleted_v1EventV2Dto<Moments.Events.MomentsCommentDeletedV1EventBodyDto>【公司圈】删除评论
moments.post.deleted_v1EventV2Dto<Moments.Events.MomentsPostDeletedV1EventBodyDto>【公司圈】删除帖子
moments.post_statistics.updated_v1EventV2Dto<Moments.Events.MomentsPostStatisticsUpdatedV1EventBodyDto>【公司圈】帖子统计数据变更
third_party_meeting_room_event_createdEventV1Dto<MeetingRoom.Events.ThirdPartyMeetingRoomEventCreatedEventBodyDto>【会议室】第三方会议室日程变动
meeting_room.meeting_room.created_v1EventV2Dto<MeetingRoom.Events.MeetingRoomMeetingRoomCreatedV1EventBodyDto>【会议室】会议室创建
meeting_room.meeting_room.deleted_v1EventV2Dto<MeetingRoom.Events.MeetingRoomMeetingRoomDeletedV1EventBodyDto>【会议室】会议室删除
meeting_room.meeting_room.updated_v1EventV2Dto<MeetingRoom.Events.MeetingRoomMeetingRoomUpdatedV1EventBodyDto>【会议室】会议室属性变更
meeting_room.meeting_room.status_changed_v1EventV2Dto<MeetingRoom.Events.MeetingRoomMeetingRoomStatusChangedV1EventBodyDto>【会议室】会议室状态信息变更
performance.stage_task.open_result_v2EventV2Dto<Performance.Events.PerformanceStageTaskOpenResultV2EventBodyDto>【绩效】绩效结果开通
performance.review_data.changed_v2EventV2Dto<Performance.Events.PerformanceReviewDataChangedV2EventBodyDto>【绩效】绩效详情变更
task.task.comment.updated_v1EventV2Dto<Task.Events.TaskTaskCommentUpdatedV1EventBodyDto>【任务】任务评论信息变更
task.task.updated_v1EventV2Dto<Task.Events.TaskTaskUpdatedV1EventBodyDto>【任务】任务信息变更(应用维度)
task.task.update_tenant_v1EventV2Dto<Task.Events.TaskTaskUpdateTenantV1EventBodyDto>【任务】任务信息变更(租户维度)
calendar.calendar.acl.created_v4EventV2Dto<Calendar.Events.CalendarCalendarAclCreatedV4EventBodyDto>【日历】创建 ACL
calendar.calendar.event.changed_v4EventV2Dto<Calendar.Events.CalendarCalendarEventChangedV4EventBodyDto>【日历】日程变更
calendar.calendar.changed_v4EventV2Dto<Calendar.Events.CalendarCalendarChangedV4EventBodyDto>【日历】日历变更
calendar.calendar.acl.deleted_v4EventV2Dto<Calendar.Events.CalendarCalendarAclDeletedV4EventBodyDto>【日历】删除 ACL
vc.room.created_v1EventV2Dto<Vc.Events.VcRoomCreatedV1EventBodyDto>【视频会议】创建会议室
vc.room_level.created_v1EventV2Dto<Vc.Events.VcRoomLevelCreatedV1EventBodyDto>【视频会议】创建会议室层级
vc.room.updated_v1EventV2Dto<Vc.Events.VcRoomUpdatedV1EventBodyDto>【视频会议】更新会议室
vc.room_level.updated_v1EventV2Dto<Vc.Events.VcRoomLevelUpdatedV1EventBodyDto>【视频会议】更新会议室层级
vc.reserve_config.updated_v1EventV2Dto<Vc.Events.VcReserveConfigUpdatedV1EventBodyDto>【视频会议】更新会议室预定限制
vc.meeting.meeting_ended_v1EventV2Dto<Vc.Events.VcMeetingMeetingEndedV1EventBodyDto>【视频会议】会议结束
vc.meeting.meeting_started_v1EventV2Dto<Vc.Events.VcMeetingMeetingStartedV1EventBodyDto>【视频会议】会议开始
vc.meeting.join_meeting_v1EventV2Dto<Vc.Events.VcMeetingJoinMeetingV1EventBodyDto>【视频会议】加入会议
vc.meeting.share_ended_v1EventV2Dto<Vc.Events.VcMeetingShareEndedV1EventBodyDto>【视频会议】结束屏幕共享
vc.meeting.recording_started_v1EventV2Dto<Vc.Events.VcMeetingRecordingStartedV1EventBodyDto>【视频会议】开始录制
vc.meeting.share_started_v1EventV2Dto<Vc.Events.VcMeetingShareStartedV1EventBodyDto>【视频会议】开始屏幕共享
vc.meeting.leave_meeting_v1EventV2Dto<Vc.Events.VcMeetingLeaveMeetingV1EventBodyDto>【视频会议】离开会议
vc.meeting.recording_ready_v1EventV2Dto<Vc.Events.VcMeetingRecordingReadyV1EventBodyDto>【视频会议】录制完成
vc.meeting.all_meeting_ended_v1EventV2Dto<Vc.Events.VcMeetingAllMeetingEndedV1EventBodyDto>【视频会议】企业会议结束
vc.meeting.all_meeting_started_v1EventV2Dto<Vc.Events.VcMeetingAllMeetingStartedV1EventBodyDto>【视频会议】企业会议开始
vc.room.deleted_v1EventV2Dto<Vc.Events.VcRoomDeletedV1EventBodyDto>【视频会议】删除会议室
vc.room_level.deleted_v1EventV2Dto<Vc.Events.VcRoomLevelDeletedV1EventBodyDto>【视频会议】删除会议室层级
vc.meeting.recording_ended_v1EventV2Dto<Vc.Events.VcMeetingRecordingEndedV1EventBodyDto>【视频会议】停止录制
contact.department.deleted_v3EventV2Dto<Contact.Events.ContactDepartmentDeletedV3EventBodyDto>【通讯录】部门被删除
contact.department.created_v3EventV2Dto<Contact.Events.ContactDepartmentCreatedV3EventBodyDto>【通讯录】部门新建
contact.department.updated_v3EventV2Dto<Contact.Events.ContactDepartmentUpdatedV3EventBodyDto>【通讯录】部门信息变化
contact.custom_attr_event.updated_v3EventV2Dto<Contact.Events.ContactCustomAttrEventUpdatedV3EventBodyDto>【通讯录】成员字段变更
contact.employee_type_enum.actived_v3EventV2Dto<Contact.Events.ContactEmployeeTypeEnumActivedV3EventBodyDto>【通讯录】启用人员类型
contact.employee_type_enum.deleted_v3EventV2Dto<Contact.Events.ContactEmployeeTypeEnumDeletedV3EventBodyDto>【通讯录】删除人员类型
contact.employee_type_enum.deactivated_v3EventV2Dto<Contact.Events.ContactEmployeeTypeEnumDeactivatedV3EventBodyDto>【通讯录】停用人员类型
contact.scope.updated_v3EventV2Dto<Contact.Events.ContactScopeUpdatedV3EventBodyDto>【通讯录】通讯录权限范围变更
contact.employee_type_enum.created_v3EventV2Dto<Contact.Events.ContactEmployeeTypeEnumCreatedV3EventBodyDto>【通讯录】新建人员类型
contact.employee_type_enum.updated_v3EventV2Dto<Contact.Events.ContactEmployeeTypeEnumUpdatedV3EventBodyDto>【通讯录】修改人员类型名称
contact.user.deleted_v3EventV2Dto<Contact.Events.ContactUserDeletedV3EventBodyDto>【通讯录】员工离职
contact.user.created_v3EventV2Dto<Contact.Events.ContactUserCreatedV3EventBodyDto>【通讯录】员工入职
contact.user.updated_v3EventV2Dto<Contact.Events.ContactUserUpdatedV3EventBodyDto>【通讯录】员工信息被修改
im.message.recalled_v1EventV2Dto<Im.Events.ImMessageRecalledV1EventBodyDto>【消息与群组】撤回消息
im.chat.member.user.withdrawn_v1EventV2Dto<Im.Events.ImChatMemberUserWithdrawnV1EventBodyDto>【消息与群组】撤销拉用户进群
im.chat.member.bot.deleted_v1EventV2Dto<Im.Events.ImChatMemberBotDeletedV1EventBodyDto>【消息与群组】机器人被移出群
im.chat.member.bot.added_v1EventV2Dto<Im.Events.ImChatMemberBotAddedV1EventBodyDto>【消息与群组】机器人进群
im.message.receive_v1EventV2Dto<Im.Events.ImMessageReceiveV1EventBodyDto>【消息与群组】接收消息
im.chat.disbanded_v1EventV2Dto<Im.Events.ImChatDisbandedV1EventBodyDto>【消息与群组】群解散
im.chat.updated_v1EventV2Dto<Im.Events.ImChatUpdatedV1EventBodyDto>【消息与群组】群配置修改
im.message.reaction.deleted_v1EventV2Dto<Im.Events.ImMessageReactionDeletedV1EventBodyDto>【消息与群组】删除消息表情回复
im.message.message_read_v1EventV2Dto<Im.Events.ImMessageMessageReadV1EventBodyDto>【消息与群组】消息已读
im.message.reaction.created_v1EventV2Dto<Im.Events.ImMessageReactionCreatedV1EventBodyDto>【消息与群组】新增消息表情回复
im.chat.member.user.deleted_v1EventV2Dto<Im.Events.ImChatMemberUserDeletedV1EventBodyDto>【消息与群组】用户出群
im.chat.member.user.added_v1EventV2Dto<Im.Events.ImChatMemberUserAddedV1EventBodyDto>【消息与群组】用户进群
im.chat.access_event.bot_p2p_chat_entered_v1EventV2Dto<Im.Events.ImChatAccessEventBotP2pChatEnteredV1EventBodyDto>【消息与群组】用户进入与机器人的会话
application.application.app_version.publish_revoke_v6EventV2Dto<Application.Events.ApplicationApplicationAppVersionPublishRevokeV6EventBodyDto>【应用信息】撤回应用发布申请
application.application.feedback.updated_v6EventV2Dto<Application.Events.ApplicationApplicationFeedbackUpdatedV6EventBodyDto>【应用信息】反馈更新
application.bot.menu_v6EventV2Dto<Application.Events.ApplicationBotMenuV6EventBodyDto>【应用信息】机器人自定义菜单事件
application.application.app_version.publish_apply_v6EventV2Dto<Application.Events.ApplicationApplicationAppVersionPublishApplyV6EventBodyDto>【应用信息】申请发布应用
application.application.feedback.created_v6EventV2Dto<Application.Events.ApplicationApplicationFeedbackCreatedV6EventBodyDto>【应用信息】新增应用反馈
application.application.created_v6EventV2Dto<Application.Events.ApplicationApplicationCreatedV6EventBodyDto>【应用信息】应用创建
application.application.app_version.audit_v6EventV2Dto<Application.Events.ApplicationApplicationAppVersionAuditV6EventBodyDto>【应用信息】应用审核
application.application.visibility.added_v6EventV2Dto<Application.Events.ApplicationApplicationVisibilityAddedV6EventBodyDto>【应用信息】员工免审安装应用
drive.file.bitable_record_changed_v1EventV2Dto<Ccm.Events.DriveFileBitableRecordChangedV1EventBodyDto>【云文档】多维表格记录变更
drive.file.bitable_field_changed_v1EventV2Dto<Ccm.Events.DriveFileBitableFieldChangedV1EventBodyDto>【云文档】多维表格字段变更
drive.file.edit_v1EventV2Dto<Ccm.Events.DriveFileEditV1EventBodyDto>【云文档】文件编辑
drive.file.title_updated_v1EventV2Dto<Ccm.Events.DriveFileTitleUpdatedV1EventBodyDto>【云文档】文件标题变更
drive.file.deleted_v1EventV2Dto<Ccm.Events.DriveFileDeletedV1EventBodyDto>【云文档】文件彻底删除
drive.file.trashed_v1EventV2Dto<Ccm.Events.DriveFileTrashedV1EventBodyDto>【云文档】文件删除到回收站
drive.file.permission_member_added_v1EventV2Dto<Ccm.Events.DriveFilePermissionMemberAddedV1EventBodyDto>【云文档】文件协作者添加
drive.file.permission_member_removed_v1EventV2Dto<Ccm.Events.DriveFilePermissionMemberRemovedV1EventBodyDto>【云文档】文件协作者移除
drive.file.read_v1EventV2Dto<Ccm.Events.DriveFileReadV1EventBodyDto>【云文档】文件已读
hire.eco_background_check.created_v1EventV2Dto<Hire.Events.HireEcoBackgroundCheckCreatedV1EventBodyDto>【招聘】创建背调
hire.eco_exam.created_v1EventV2Dto<Hire.Events.HireEcoExamCreatedV1EventBodyDto>【招聘】创建笔试
hire.ehr_import_task.imported_v1EventV2Dto<Hire.Events.HireEhrImportTaskImportedV1EventBodyDto>【招聘】导入 e-HR
hire.ehr_import_task_for_internship_offer.imported_v1EventV2Dto<Hire.Events.HireEhrImportTaskForInternshipOfferImportedV1EventBodyDto>【招聘】导入 e-HR(实习 Offer)
hire.referral_account.assets_update_v1EventV2Dto<Hire.Events.HireReferralAccountAssetsUpdateV1EventBodyDto>【招聘】内推账户余额变更
hire.talent.deleted_v1EventV2Dto<Hire.Events.HireTalentDeletedV1EventBodyDto>【招聘】删除人才
hire.application.deleted_v1EventV2Dto<Hire.Events.HireApplicationDeletedV1EventBodyDto>【招聘】删除投递
hire.application.stage_changed_v1EventV2Dto<Hire.Events.HireApplicationStageChangedV1EventBodyDto>【招聘】投递阶段变更
hire.eco_account.created_v1EventV2Dto<Hire.Events.HireEcoAccountCreatedV1EventBodyDto>【招聘】账号绑定
hire.eco_background_check.canceled_v1EventV2Dto<Hire.Events.HireEcoBackgroundCheckCanceledV1EventBodyDto>【招聘】终止背调
hire.offer.status_changed_v1EventV2Dto<Hire.Events.HireOfferStatusChangedV1EventBodyDto>【招聘】Offer 状态变更
acs.access_record.created_v1EventV2Dto<Acs.Events.AcsAccessRecordCreatedV1EventBodyDto>【智能门禁】新增门禁访问记录
acs.user.updated_v1EventV2Dto<Acs.Events.AcsUserUpdatedV1EventBodyDto>【智能门禁】用户信息变更
elearning.course_registration.updated_v2EventV2Dto<Elearning.Events.ElearningCourseRegistrationUpdatedV2EventBodyDto>【eLearning】课程学习进度更新事件
elearning.course_registration.deleted_v2EventV2Dto<Elearning.Events.ElearningCourseRegistrationDeletedV2EventBodyDto>【eLearning】课程学习进度删除事件
elearning.course_registration.created_v2EventV2Dto<Elearning.Events.ElearningCourseRegistrationCreatedV2EventBodyDto>【eLearning】课程学习进度新增事件
diff --git a/FeishuNetSdk.Endpoint/FeishuNetSdk.Endpoint.csproj b/FeishuNetSdk.Endpoint/FeishuNetSdk.Endpoint.csproj new file mode 100644 index 00000000..c77ac11e --- /dev/null +++ b/FeishuNetSdk.Endpoint/FeishuNetSdk.Endpoint.csproj @@ -0,0 +1,39 @@ + + + + net8.0 + enable + enable + Library + true + True + true + FeishuNetSdk.Endpoint + Vicente Yu + https://github.com/vicenteyu/FeishuNetSdk + README.md + https://github.com/vicenteyu/FeishuNetSdk + git + feishu; sdk; dotnet; .net8.0 + MIT + 适用于飞书开放平台的.Net开发包 + LICENSE + 3.0.0 + + + + + True + \ + + + True + \ + + + + + + + + diff --git a/FeishuNetSdk.Endpoint/FeishuNetSdkEndpointExtension.cs b/FeishuNetSdk.Endpoint/FeishuNetSdkEndpointExtension.cs new file mode 100644 index 00000000..2d16db57 --- /dev/null +++ b/FeishuNetSdk.Endpoint/FeishuNetSdkEndpointExtension.cs @@ -0,0 +1,53 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-09-07 +// +// Last Modified By : yxr +// Last Modified On : 2024-09-07 +// ************************************************************************ +// +// MIT +// +// ṩ¼صַչ +// ************************************************************************ +using FeishuNetSdk.Services; +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; + +#pragma warning disable IDE0130 +namespace Microsoft.Extensions.DependencyInjection +#pragma warning restore IDE0130 +{ + /// + /// ÷¼صַ + /// + public static class FeishuNetSdkEndpointExtension + { + /// + /// ¼صַ + /// + /// + /// + /// + public static IEndpointRouteBuilder UseFeishuEndpoint(this IEndpointRouteBuilder app, string pattern) + { + app.MapPost(pattern, async (IEventCallbackServiceProvider provider, ILogger logger, [FromBody] object input) => + { + logger.LogInformation("FeishuEndpoint: {json}", input); + var result = await provider.HandleAsync(input); + logger.LogInformation("EventHandle: {json}", JsonSerializer.Serialize(result)); + + if (result?.Success != true) + return Results.Problem(result?.Error); + + if (result?.Dto != null) + return Results.Json(result.Dto); + + return Results.Ok(); + }); + + return app; + } + } +} \ No newline at end of file diff --git a/FeishuNetSdk.Endpoint/LICENSE b/FeishuNetSdk.Endpoint/LICENSE new file mode 100644 index 00000000..2a1604a6 --- /dev/null +++ b/FeishuNetSdk.Endpoint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 vicenteyu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/FeishuNetSdk.Endpoint/Properties/launchSettings.json b/FeishuNetSdk.Endpoint/Properties/launchSettings.json new file mode 100644 index 00000000..2b6358a5 --- /dev/null +++ b/FeishuNetSdk.Endpoint/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "FeishuNetSdk.Endpoint": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5155;http://localhost:5156" + } + } +} \ No newline at end of file diff --git a/FeishuNetSdk.Endpoint/README.md b/FeishuNetSdk.Endpoint/README.md new file mode 100644 index 00000000..f2f56889 --- /dev/null +++ b/FeishuNetSdk.Endpoint/README.md @@ -0,0 +1,45 @@ +# FeishuNetSdk.Endpoint + +FeishuNetSdk 开发包的事件回调终结点扩展。 + +[![.NET](https://github.com/vicenteyu/FeishuNetSdk/actions/workflows/dotnet.yml/badge.svg?branch=main&event=push)](https://github.com/vicenteyu/FeishuNetSdk/actions/workflows/dotnet.yml) [![FeishuNetSdk](https://buildstats.info/nuget/FeishuNetSdk "FeishuNetSdk")](https://www.nuget.org/packages/FeishuNetSdk/ "FeishuNetSdk") + +飞书开放平台网址:[https://open.feishu.cn/](https://open.feishu.cn/) + +接口清单详见: + +[TenantAccessToken 适用接口清单](https://github.com/vicenteyu/FeishuNetSdk/blob/main/TenantAccessList.md) + +[UserAccessToken 适用接口清单](https://github.com/vicenteyu/FeishuNetSdk/blob/main/UserAccessList.md) + +## 注意事项: + +### 在开始配置之前,你需要确保已了解以下注意事项: + +- 配置终结点并运行程序 +- 将终点地址配置到 飞开发者后台 事件与回调的请求地址中。 + +## 用法: + +### 1、安装Nuget包 +```csharp +PM> Install-Package FeishuNetSdk.Endpoint +``` + +### 2、服务注册 + +**(1)输入`应用凭证`的方式** +```csharp +builder.Services + .AddFeishuNetSdk( + AppId: "cli_a*********013", + AppSecret: "H2wl******************UBfyVn", + EncryptKey: "75vyV******************wpkjy", + VerificationToken: "WVrlO******************2EsMSJw"); +``` + +**(2)配置终结点** +```csharp +//启用飞书事件回调地址服务 +app.UseFeishuEndpoint("/a/b/c/d"); +``` \ No newline at end of file diff --git a/FeishuNetSdk.WebSocket/FeishuNetSdk.WebSocket.csproj b/FeishuNetSdk.WebSocket/FeishuNetSdk.WebSocket.csproj new file mode 100644 index 00000000..9a07457b --- /dev/null +++ b/FeishuNetSdk.WebSocket/FeishuNetSdk.WebSocket.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + enable + enable + True + true + FeishuNetSdk.WebSocket + Vicente Yu + https://github.com/vicenteyu/FeishuNetSdk + README.md + https://github.com/vicenteyu/FeishuNetSdk + git + feishu; sdk; dotnet; .net8.0 + MIT + 适用于飞书开放平台的.Net开发包 + LICENSE + 3.0.0 + + + + + + + + + + + True + \ + + + True + \ + + + + + + + + diff --git a/FeishuNetSdk.WebSocket/FeishuNetSdkWebSocketExtensions.cs b/FeishuNetSdk.WebSocket/FeishuNetSdkWebSocketExtensions.cs new file mode 100644 index 00000000..395b99e7 --- /dev/null +++ b/FeishuNetSdk.WebSocket/FeishuNetSdkWebSocketExtensions.cs @@ -0,0 +1,41 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-09-07 +// +// Last Modified By : yxr +// Last Modified On : 2024-09-07 +// ************************************************************************ +// +// MIT +// +// 提供注册SDK的扩展 +// ************************************************************************ +#pragma warning disable IDE0130 +namespace Microsoft.Extensions.DependencyInjection +#pragma warning restore IDE0130 +{ + /// + /// 提供注册SDK的扩展 + /// + public static class FeishuNetSdkWebSocketExtensions + { + /// + /// 注册SDK + /// + /// + /// + public static IServiceCollection AddFeishuWebSocket(this IServiceCollection services) + { + var serviceProvider = services.BuildServiceProvider(); + + _ = serviceProvider.GetService() + ?? throw new NotSupportedException("缺少关键服务:FeishuNetSdk"); + _ = serviceProvider.GetService>() + ?? throw new NotSupportedException("缺少关键服务:FeishuNetSdk"); + + services.AddHostedService(); + return services; + } + } +} diff --git a/FeishuNetSdk.WebSocket/FrameProto.cs b/FeishuNetSdk.WebSocket/FrameProto.cs new file mode 100644 index 00000000..623d7ea6 --- /dev/null +++ b/FeishuNetSdk.WebSocket/FrameProto.cs @@ -0,0 +1,150 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-09-07 +// +// Last Modified By : yxr +// Last Modified On : 2024-09-07 +// ************************************************************************ +// +// MIT +// +// 飞书WebSocket序列化消息类 +// ************************************************************************ +using ProtoBuf; +using System.Text; + +namespace FeishuNetSdk.WebSocket +{ + /// + /// + /// + [ProtoContract] + public class Header + { + /// + /// + /// + [ProtoMember(1, Name = "key")] + public string Key { get; set; } = string.Empty; + + /// + /// + /// + [ProtoMember(2, Name = "value")] + public string Value { get; set; } = string.Empty; + } + + /// + /// + /// + [ProtoContract] + public class Frame + { + /// + /// + /// + [ProtoMember(1)] + public ulong SeqID { get; set; } + + /// + /// + /// + [ProtoMember(2)] + public ulong LogID { get; set; } + + /// + /// + /// + [ProtoMember(3, Name = "service")] + public int Service { get; set; } + + /// + /// + /// + [ProtoMember(4, Name = "method")] + public int Method { get; set; } + + /// + /// + /// + [ProtoMember(5, Name = "headers")] + public Header[]? Headers { get; set; } + + /// + /// + /// + [ProtoMember(6, Name = "payload_encoding")] + public string? PayloadEncoding { get; set; } = string.Empty; + + /// + /// + /// + [ProtoMember(7, Name = "payload_type")] + public string? PayloadType { get; set; } = string.Empty; + + /// + /// + /// + [ProtoMember(8, Name = "payload")] + public byte[]? Payload { get; set; } + + /// + /// + /// + [ProtoMember(9)] + public string? LogIDNew { get; set; } = string.Empty; + + /// + /// + /// + public string? PayloadToJson() => Payload is null ? null : Encoding.UTF8.GetString(Payload); + + /// + /// + /// + public MessageType MessageType + { + get + { + if (Headers is null || Headers.Length == 0) return MessageType.Unknown; + if (Headers.Any(p => p.Key == "type" && p.Value == "ping")) return MessageType.Ping; + if (Headers.Any(p => p.Key == "type" && p.Value == "pong")) return MessageType.Pong; + if (Headers.Any(p => p.Key == "type" && p.Value == "card")) return MessageType.Card; + if (Headers.Any(p => p.Key == "type" && p.Value == "event")) return MessageType.Event; + return MessageType.Unknown; + } + } + } + + /// + /// + /// + public enum MessageType + { + /// + /// + /// + Ping, + + /// + /// + /// + Pong, + + /// + /// + /// + Card, + + /// + /// + /// + Event, + + /// + /// + /// + Unknown + } +} diff --git a/FeishuNetSdk.WebSocket/LICENSE b/FeishuNetSdk.WebSocket/LICENSE new file mode 100644 index 00000000..2a1604a6 --- /dev/null +++ b/FeishuNetSdk.WebSocket/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 vicenteyu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/FeishuNetSdk.WebSocket/README.md b/FeishuNetSdk.WebSocket/README.md new file mode 100644 index 00000000..518b2e60 --- /dev/null +++ b/FeishuNetSdk.WebSocket/README.md @@ -0,0 +1,45 @@ +# FeishuNetSdk.WebSocket + +FeishuNetSdk 开发包的长连接扩展。 + +[![.NET](https://github.com/vicenteyu/FeishuNetSdk/actions/workflows/dotnet.yml/badge.svg?branch=main&event=push)](https://github.com/vicenteyu/FeishuNetSdk/actions/workflows/dotnet.yml) [![FeishuNetSdk](https://buildstats.info/nuget/FeishuNetSdk "FeishuNetSdk")](https://www.nuget.org/packages/FeishuNetSdk/ "FeishuNetSdk") + +飞书开放平台网址:[https://open.feishu.cn/](https://open.feishu.cn/) + +接口清单详见: + +[TenantAccessToken 适用接口清单](https://github.com/vicenteyu/FeishuNetSdk/blob/main/TenantAccessList.md) + +[UserAccessToken 适用接口清单](https://github.com/vicenteyu/FeishuNetSdk/blob/main/UserAccessList.md) + +## 注意事项: + +### 在开始配置之前,你需要确保已了解以下注意事项: + +- 目前长连接模式仅支持企业自建应用,并且仅支持应用内的事件订阅,不支持回调订阅。 +- 与 将事件发送至开发者服务器 方式的要求相同,长连接模式下接收到消息后,也需要在 3 秒内处理完成,否则会触发超时重推机制。 +- 每个应用最多建立 50 个连接(在配置长连接时,每初始化一个 client 就是一个连接)。 +- 长连接模式的消息推送为 集群模式,不支持广播,即如果同一应用部署了多个客户端(client),那么只有其中随机一个客户端会收到消息。 +- **启用长连接并启动项目,进入开发者后台,将事件配置中的订阅方式变更为:使用长连接接收事件,重新发布版本之后才能生效。** + +## 用法: + +### 1、安装Nuget包 +```csharp +PM> Install-Package FeishuNetSdk.WebSocket +``` + +### 2、服务注册 + +**(1)输入`应用凭证`的方式** +```csharp +builder.Services + .AddFeishuNetSdk( + AppId: "cli_a*********013", + AppSecret: "H2wl******************UBfyVn", + EncryptKey: "75vyV******************wpkjy", + VerificationToken: "WVrlO******************2EsMSJw") + //添加飞书长连接服务 + .AddFeishuWebSocket(); + +``` \ No newline at end of file diff --git a/FeishuNetSdk.WebSocket/WssService.cs b/FeishuNetSdk.WebSocket/WssService.cs new file mode 100644 index 00000000..6b1a541c --- /dev/null +++ b/FeishuNetSdk.WebSocket/WssService.cs @@ -0,0 +1,136 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-09-07 +// +// Last Modified By : yxr +// Last Modified On : 2024-09-07 +// ************************************************************************ +// +// MIT +// +// 启用长连接服务 +// ************************************************************************ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ProtoBuf; +using System.Text; +using System.Text.Json; +using WatsonWebsocket; +using WebApiClientCore.Extensions.OAuths.Exceptions; + +namespace FeishuNetSdk.WebSocket +{ + /// + /// 长连接服务 + /// 优势: + /// 测试阶段无需使用内网穿透工具,通过长连接模式在本地开发环境中即可接收事件回调。 + /// 只在建连时进行鉴权,后续事件推送均为明文数据,无需再处理解密和验签逻辑。 + /// 只需保证运行环境具备访问公网能力即可,无需提供公网 IP 或域名。 + /// 无需部署防火墙和配置白名单。 + /// + /// 注意事项: + /// 长连接模式仅支持企业自建应用,并且仅支持应用内的事件订阅,不支持回调订阅。 + /// 与 将事件发送至开发者服务器 方式的要求相同,长连接模式下接收到消息后,也需要在 3 秒内处理完成,否则会触发超时重推机制。 + /// 每个应用最多建立 50 个连接(在配置长连接时,每初始化一个 client 就是一个连接)。 + /// 长连接模式的消息推送为 集群模式,不支持广播,即如果同一应用部署了多个客户端(client),那么只有其中随机一个客户端会收到消息。 + /// 如果同一应用部署了多个客户端(client),那么只有其中随机一个客户端会收到消息。 + /// 如果同一应用部署了多个客户端(client),那么只有其中随机一个客户端会收到消息。 + /// 务必注意:仅建议在测试环境使用长连接模式,不当使用则可能导致正式环境的事件消息被误收!!! + /// + public class WssService(IFeishuApi feishuApi, IOptionsMonitor options, ILogger logger, Services.IEventCallbackServiceProvider eventCallback) : BackgroundService + { + private WatsonWsClient? _wsClient = null; + + /// + /// 执行长连接 + /// + protected override async System.Threading.Tasks.Task ExecuteAsync(CancellationToken stoppingToken) + { + await StartConnectAsync(); + } + + /// + /// 服务停止 + /// + public override async System.Threading.Tasks.Task StopAsync(CancellationToken cancellationToken) + { + if (_wsClient?.Connected == true) + { + await _wsClient.StopAsync(); + } + await base.StopAsync(cancellationToken); + } + + private async Task GetWssEndpointAsync() + { + var result = await feishuApi.PostCallbackWsEndpointAsync(new() + { + AppId = options.CurrentValue.AppId, + AppSecret = options.CurrentValue.AppSecret + }); + + if (result?.IsSuccess != true || result?.Data?.Url == null) + throw new TokenException(result?.Msg ?? "长连接出现异常"); + + return result.Data.Url; + } + + private async System.Threading.Tasks.Task StartConnectAsync() + { + var endpoint = await GetWssEndpointAsync(); + + _wsClient = new WatsonWsClient(new Uri(endpoint)); + _wsClient.ServerConnected += ServerConnected; + _wsClient.ServerDisconnected += ServerDisconnected; + _wsClient.MessageReceived += MessageReceived; + _wsClient.Logger = (string s) => { logger.LogInformation("{s}", s); }; + + await _wsClient.StartAsync(); + } + + private async void MessageReceived(object? sender, MessageReceivedEventArgs e) + { + if (sender is WatsonWsClient client && e.Data.Count > 0) + { + var frame = Serializer.Deserialize(e.Data.AsSpan()); + + var json = frame?.PayloadToJson() ?? throw new Exception("无法序列化消息"); + + logger.LogInformation("{json}", json); + try + { + var result = await eventCallback.HandleAsync(json); + if (result.Success != true) + { + logger.LogError("{error}", result?.Error); + return; + } + + using var messageStream = new MemoryStream(); + frame.Payload = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(new { code = 200 })); + Serializer.Serialize(messageStream, frame); + if (messageStream.TryGetBuffer(out var arraySegment)) + { + await client.SendAsync(arraySegment, System.Net.WebSockets.WebSocketMessageType.Binary); + } + } + catch (Exception ex) + { + logger.LogError(ex, "事件执行异常"); + } + } + } + + private void ServerDisconnected(object? sender, EventArgs e) + { + logger.LogInformation("长连接已断开"); + } + + private void ServerConnected(object? sender, EventArgs e) + { + logger.LogInformation("长连接已连接"); + } + } +} diff --git a/FeishuNetSdk.sln b/FeishuNetSdk.sln index 66ed0254..bed6a5c7 100644 --- a/FeishuNetSdk.sln +++ b/FeishuNetSdk.sln @@ -19,6 +19,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleApp1", "samples\Cons EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApplication1", "samples\WebApplication1\WebApplication1.csproj", "{4969EB46-D7B9-4A13-A732-8E826D1CF79F}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FeishuNetSdk.WebSocket", "FeishuNetSdk.WebSocket\FeishuNetSdk.WebSocket.csproj", "{2B0A530E-54E9-445F-977B-BC64165AB71E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FeishuNetSdk.Endpoint", "FeishuNetSdk.Endpoint\FeishuNetSdk.Endpoint.csproj", "{E7A299FF-4B33-4DE2-A8CF-5149C27CEDB9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +45,14 @@ Global {4969EB46-D7B9-4A13-A732-8E826D1CF79F}.Debug|Any CPU.Build.0 = Debug|Any CPU {4969EB46-D7B9-4A13-A732-8E826D1CF79F}.Release|Any CPU.ActiveCfg = Release|Any CPU {4969EB46-D7B9-4A13-A732-8E826D1CF79F}.Release|Any CPU.Build.0 = Release|Any CPU + {2B0A530E-54E9-445F-977B-BC64165AB71E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B0A530E-54E9-445F-977B-BC64165AB71E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B0A530E-54E9-445F-977B-BC64165AB71E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B0A530E-54E9-445F-977B-BC64165AB71E}.Release|Any CPU.Build.0 = Release|Any CPU + {E7A299FF-4B33-4DE2-A8CF-5149C27CEDB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7A299FF-4B33-4DE2-A8CF-5149C27CEDB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7A299FF-4B33-4DE2-A8CF-5149C27CEDB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7A299FF-4B33-4DE2-A8CF-5149C27CEDB9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/README.md b/README.md index e4da2416..8a3fcf43 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ [UserAccessToken 适用接口清单](https://github.com/vicenteyu/FeishuNetSdk/blob/main/UserAccessList.md) +[事件回调类型清单](https://github.com/vicenteyu/FeishuNetSdk/blob/main/EventCallbackList.md) + 商业合作、定制开发 ## 用法: @@ -19,18 +21,24 @@ ### 1、安装Nuget包 ```csharp PM> Install-Package FeishuNetSdk +PM> Install-Package FeishuNetSdk.Endpoint //事件回调请求地址扩展包 +PM> Install-Package FeishuNetSdk.WebSocket //长连接扩展包 ``` ### 2、服务注册 **(1)输入`应用凭证`的方式** ```csharp -builder.Services.AddFeishuNetSdk(options => -{ - options.AppId = "cli_test"; - options.AppSecret = "secret_test"; - //options.EnableLogging = true; //启用日志 (true = 启用, false = 关闭, 默认 = 启用) - //options.IgnoreStatusException = true; //忽略状态异常错误(true = 忽略, false = 启用, 默认 = 忽略) -}); +builder.Services + .AddFeishuNetSdk(options => + { + options.AppId = "cli_test"; + options.AppSecret = "secret_test"; + options.EncryptKey: "75vyV*************Clrwpkjy"; //解密密钥 + options.VerificationToken: "WVr*************MSJw"; //验证密钥 + //options.EnableLogging = true; //启用日志 (true = 启用, false = 关闭, 默认 = 启用) + //options.IgnoreStatusException = true; //忽略状态异常错误(true = 忽略, false = 启用, 默认 = 忽略) + }) + .AddFeishuWebSocket(); //添加飞书长连接服务 ``` **(2)使用`配置文件`的方式** ```csharp @@ -41,11 +49,19 @@ builder.Services.AddFeishuNetSdk(builder.Configuration.GetSection("FeishuNetSdk" "FeishuNetSdk": { "AppId": "cli_test", "AppSecret": "secret_test", + "EncryptKey": "75vyV*************Clrwpkjy", //解密密钥 + "VerificationToken": "WVr*************MSJw", //验证密钥 "EnableLogging": true, //启用日志 (true = 启用, false = 关闭, 默认 = 启用) "IgnoreStatusException": true //忽略状态异常错误(true = 忽略, false = 启用, 默认 = 忽略) } ``` +**(3)启用`事件与回调`终结点** +```csharp +//启用飞书事件回调地址服务 +app.UseFeishuEndpoint("/a/b/c/d"); //示例:https://www.abc.com/a/b/c/d +``` + ### 3、注入和调用 ```csharp public class TestController : ControllerBase @@ -77,6 +93,66 @@ public class TestController : ControllerBase ## 部分示例: +### 事件回调(v3.0.0 新增) + +**(1)事件订阅示例** + +项目内任意位置创建继承类: + +1. IEventHandler:事件方法接口 +1. EventV2Dto<>:完整消息体,V2 -> 2.0 +1. xxxxEventBodyDto:事件体,从`EventBodyDto`继承 + +事件体类型参照:[事件回调类型清单](https://github.com/vicenteyu/FeishuNetSdk/blob/main/EventCallbackList.md) + +**注意:需要3秒内响应** + +```csharp +public class EventHandler1(ILogger logger) : IEventHandler, ImMessageReceiveV1EventBodyDto> +{ + public async Task ExecuteAsync(EventV2Dto input) + { + await Task.Delay(600); + logger.LogInformation("ExecuteAsync1: {info}", System.Text.Json.JsonSerializer.Serialize(input)); + } +} +``` + +**(2)回调订阅示例** + +项目内任意位置创建继承类: + +1. ICallbackHandler:回调方法接口 +1. CallbackV2Dto<>:完整消息体,V2 -> 2.0 +1. xxxxEventBodyDto:事件体,从`EventBodyDto`继承 +1. xxxxResponseDto:响应体,从`CallbackResponseDto`继承 + +事件体类型参照:[事件回调类型清单](https://github.com/vicenteyu/FeishuNetSdk/blob/main/EventCallbackList.md) + +**注意:需要3秒内响应** + +```csharp +public class MyCallbackHandler(ILogger logger) : ICallbackHandler, CardActionTriggerEventBodyDto, CardActionTriggerResponseDto> +{ + public async Task ExecuteAsync(CallbackV2Dto input) + { + await Task.CompletedTask; + logger.LogWarning("{json}", JsonSerializer.Serialize(input)); + + return new CardActionTriggerResponseDto().SetCard(new ElementsCardV2Dto() + { + Header = new() { Title = new("Button-updated"), Template = "blue" }, + Config = new() { EnableForward = true }, + Body = new() + { + Elements = [new DivElement().SetText(new PlainTextElement(Content: $"xxoo{DateTime.Now:yyyy-MM-dd HH:mm:ss}"))] + } + }); + } +} +``` + + ### 扩展方法(v2.2.9 新增) 主要针对复杂参数的扩展,例如元素组合卡片等,可以提高易用性。 diff --git a/samples/WebApplication1/Program.cs b/samples/WebApplication1/Program.cs index 68dc0f88..b91dd7d0 100644 --- a/samples/WebApplication1/Program.cs +++ b/samples/WebApplication1/Program.cs @@ -1,5 +1,7 @@ +using FeishuNetSdk; using FeishuNetSdk.Approval.Events; using FeishuNetSdk.CallbackEvents; +using FeishuNetSdk.Im.Dtos; using FeishuNetSdk.Im.Events; using FeishuNetSdk.Services; using Serilog; @@ -65,7 +67,7 @@ public class EventHandler3(ILogger logger) : IEventHandler input) { - await Task.Delay(2500); + await Task.Delay(1500); logger.LogInformation("ExecuteAsync3: {info}", System.Text.Json.JsonSerializer.Serialize(input)); } } @@ -83,23 +85,31 @@ public Task ExecuteAsync(EventV1Dto input) /// /// /// - public class MyCallbackHandler(ILogger logger) : ICallbackHandler, CardActionTriggerEventBodyDto, CardActionTriggerResponseDto> + public class MyCallbackHandler(ILogger logger) : ICallbackHandler, CardActionTriggerEventBodyDto, CardActionTriggerResponseDto> { - public async Task ExecuteAsync(CallbackDto input) + public async Task ExecuteAsync(CallbackV2Dto input) { - await Task.Delay(2900); + await Task.CompletedTask; logger.LogWarning("{json}", JsonSerializer.Serialize(input)); - return new(); + return new CardActionTriggerResponseDto().SetCard(new ElementsCardV2Dto() + { + Header = new() { Title = new("Button-updated"), Template = "blue" }, + Config = new() { EnableForward = true }, + Body = new() + { + Elements = [new DivElement().SetText(new PlainTextElement(Content: $"xxoo{DateTime.Now:yyyy-MM-dd HH:mm:ss}"))] + } + }); } } /// /// /// - //public class MyCallbackHandler2(ILogger logger) : ICallbackHandler, CardActionTriggerEventBodyDto, CardActionTriggerResponseDto> + //public class MyCallbackHandler2(ILogger logger) : ICallbackHandler, CardActionTriggerEventBodyDto, CardActionTriggerResponseDto> //{ - // public async Task ExecuteAsync(CallbackDto input) + // public async Task ExecuteAsync(CallbackV2Dto input) // { // await Task.Delay(1900); // logger.LogWarning("{json}", JsonSerializer.Serialize(input)); @@ -107,13 +117,13 @@ public async Task ExecuteAsync(CallbackDto logger) : IEventHandler, ImMessageReceiveV1EventBodyDto> { public async Task ExecuteAsync(EventV2Dto input) { await Task.Delay(1200); logger.LogInformation("ExecuteAsync2: {info}", System.Text.Json.JsonSerializer.Serialize(input)); - throw new NotImplementedException(); } } } diff --git a/src/Core/EventDto.cs b/src/Core/EventDto.cs new file mode 100644 index 00000000..3ed57386 --- /dev/null +++ b/src/Core/EventDto.cs @@ -0,0 +1,344 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-09-01 +// +// Last Modified By : yxr +// Last Modified On : 2024-09-06 +// ************************************************************************ +// +// MIT +// +// 事件序列化定义 +// ************************************************************************ +namespace FeishuNetSdk.Core; + +/// 事件序列化定义 +[JsonPolymorphic(TypeDiscriminatorPropertyName = FeishuNetSdkOptions.Discriminator, + UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor, + IgnoreUnrecognizedTypeDiscriminators = true)] +[JsonDerivedType(typeof(UrlVerificationDto), typeDiscriminator: "url_verification")] +//【审批】审批定义更新 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "approval.approval.updated_v4")] +//【审批】出差审批 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "approval.instance.trip_group_update_v4")] +//【审批】补卡审批 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "approval.instance.remedy_group_update_v4")] +//【审批】审批抄送状态变更 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "approval_cc")] +//【审批】审批任务状态变更 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "approval_task")] +//【审批】审批实例状态变更 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "approval_instance")] +//【审批】审批通过通知 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "approval")] +//【审批】外出审批 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "out_approval")] +//【审批】换班审批 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "shift_approval")] +//【审批】加班审批 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "work_approval")] +//【审批】请假审批 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "leave_approvalV2")] +//【审批】请假撤销 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "leave_approval_revert")] +//【回调】拉取链接预览数据 +[JsonDerivedType(typeof(CallbackV2Dto), typeDiscriminator: "url.preview.get")] +//【回调】卡片回传交互 +[JsonDerivedType(typeof(CallbackV2Dto), typeDiscriminator: "card.action.trigger")] +//【飞书人事(企业版)】抄送单据状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.process.cc.updated_v2")] +//【飞书人事(企业版)】离职流转状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.offboarding.checklist_updated_v2")] +//【飞书人事(企业版)】离职申请状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.offboarding.status_updated_v2")] +//【飞书人事(企业版)】离职信息变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.offboarding.updated_v2")] +//【飞书人事(企业版)】流程节点状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.process.node.updated_v2")] +//【飞书人事(企业版)】流程实例信息变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.process.updated_v2")] +//【飞书人事(企业版)】人员信息变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.employee.domain_event_v2")] +//【飞书人事(企业版)】审批任务状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.process.approver.updated_v2")] +//【飞书人事(企业版)】试用期状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.probation.updated_v2")] +//【飞书人事(企业版)】异动信息变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job_change.updated_v2")] +//【飞书人事(企业版)】异动状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job_change.status_updated_v2")] +//【飞书人事】【事件】创建部门 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.department.created_v1")] +//【飞书人事】【事件】创建雇佣信息 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.employment.created_v1")] +//【飞书人事】【事件】个人信息创建 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.person.created_v1")] +//【飞书人事】【事件】个人信息删除 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.person.deleted_v1")] +//【飞书人事】【事件】更新部门 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.department.updated_v1")] +//【飞书人事】【事件】更新个人信息 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.person.updated_v1")] +//【飞书人事】【事件】更新雇佣信息 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.employment.updated_v1")] +//【飞书人事】【事件】删除部门 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.department.deleted_v1")] +//【飞书人事】【事件】删除雇佣信息 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.employment.deleted_v1")] +//【飞书人事】【事件】组织角色授权变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.org_role_authorization.updated_v1")] +//【飞书人事】创建职务 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job.created_v1")] +//【飞书人事】更新职务 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job.updated_v1")] +//【飞书人事】合同创建 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.contract.created_v1")] +//【飞书人事】合同更新 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.contract.updated_v1")] +//【飞书人事】合同删除 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.contract.deleted_v1")] +//【飞书人事】离职申请状态变更(不推荐) +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.offboarding.updated_v1")] +//【飞书人事】任职信息创建 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job_data.created_v1")] +//【飞书人事】任职信息更新 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job_data.updated_v1")] +//【飞书人事】任职信息删除 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job_data.deleted_v1")] +//【飞书人事】入职信息变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.pre_hire.updated_v1")] +//【飞书人事】删除职务 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job.deleted_v1")] +//【飞书人事】异动状态变更(不推荐) +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job_change.updated_v1")] +//【飞书人事】员工完成离职 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.employment.resigned_v1")] +//【飞书人事】员工完成入职 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job_data.employed_v1")] +//【飞书人事】员工完成异动 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.job_data.changed_v1")] +//【飞书人事】员工完成转正 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "corehr.employment.converted_v1")] +//【服务台】创建工单 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "helpdesk.ticket.created_v1")] +//【服务台】工单消息事件 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "helpdesk.ticket_message.created_v1")] +//【服务台】工单状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "helpdesk.ticket.updated_v1")] +//【服务台】推送审核通知 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "helpdesk.notification.approve_v1")] +//【公司圈】表情互动 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.reaction.created_v1")] +//【公司圈】点踩 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.dislike.created_v1")] +//【公司圈】发布评论 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.comment.created_v1")] +//【公司圈】发布帖子 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.post.created_v1")] +//【公司圈】取消表情互动 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.reaction.deleted_v1")] +//【公司圈】取消点踩 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.dislike.deleted_v1")] +//【公司圈】删除评论 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.comment.deleted_v1")] +//【公司圈】删除帖子 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.post.deleted_v1")] +//【公司圈】帖子统计数据变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "moments.post_statistics.updated_v1")] +//【会议室】第三方会议室日程变动 +[JsonDerivedType(typeof(EventV1Dto), typeDiscriminator: "third_party_meeting_room_event_created")] +//【会议室】会议室创建 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "meeting_room.meeting_room.created_v1")] +//【会议室】会议室删除 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "meeting_room.meeting_room.deleted_v1")] +//【会议室】会议室属性变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "meeting_room.meeting_room.updated_v1")] +//【会议室】会议室状态信息变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "meeting_room.meeting_room.status_changed_v1")] +//【绩效】绩效结果开通 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "performance.stage_task.open_result_v2")] +//【绩效】绩效详情变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "performance.review_data.changed_v2")] +//【任务】任务评论信息变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "task.task.comment.updated_v1")] +//【任务】任务信息变更(应用维度) +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "task.task.updated_v1")] +//【任务】任务信息变更(租户维度) +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "task.task.update_tenant_v1")] +//【日历】创建 ACL +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "calendar.calendar.acl.created_v4")] +//【日历】日程变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "calendar.calendar.event.changed_v4")] +//【日历】日历变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "calendar.calendar.changed_v4")] +//【日历】删除 ACL +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "calendar.calendar.acl.deleted_v4")] +//【视频会议】创建会议室 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.room.created_v1")] +//【视频会议】创建会议室层级 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.room_level.created_v1")] +//【视频会议】更新会议室 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.room.updated_v1")] +//【视频会议】更新会议室层级 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.room_level.updated_v1")] +//【视频会议】更新会议室预定限制 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.reserve_config.updated_v1")] +//【视频会议】会议结束 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.meeting_ended_v1")] +//【视频会议】会议开始 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.meeting_started_v1")] +//【视频会议】加入会议 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.join_meeting_v1")] +//【视频会议】结束屏幕共享 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.share_ended_v1")] +//【视频会议】开始录制 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.recording_started_v1")] +//【视频会议】开始屏幕共享 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.share_started_v1")] +//【视频会议】离开会议 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.leave_meeting_v1")] +//【视频会议】录制完成 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.recording_ready_v1")] +//【视频会议】企业会议结束 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.all_meeting_ended_v1")] +//【视频会议】企业会议开始 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.all_meeting_started_v1")] +//【视频会议】删除会议室 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.room.deleted_v1")] +//【视频会议】删除会议室层级 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.room_level.deleted_v1")] +//【视频会议】停止录制 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "vc.meeting.recording_ended_v1")] +//【通讯录】部门被删除 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.department.deleted_v3")] +//【通讯录】部门新建 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.department.created_v3")] +//【通讯录】部门信息变化 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.department.updated_v3")] +//【通讯录】成员字段变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.custom_attr_event.updated_v3")] +//【通讯录】启用人员类型 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.employee_type_enum.actived_v3")] +//【通讯录】删除人员类型 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.employee_type_enum.deleted_v3")] +//【通讯录】停用人员类型 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.employee_type_enum.deactivated_v3")] +//【通讯录】通讯录权限范围变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.scope.updated_v3")] +//【通讯录】新建人员类型 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.employee_type_enum.created_v3")] +//【通讯录】修改人员类型名称 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.employee_type_enum.updated_v3")] +//【通讯录】员工离职 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.user.deleted_v3")] +//【通讯录】员工入职 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.user.created_v3")] +//【通讯录】员工信息被修改 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "contact.user.updated_v3")] +//【消息与群组】撤回消息 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.message.recalled_v1")] +//【消息与群组】撤销拉用户进群 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.chat.member.user.withdrawn_v1")] +//【消息与群组】机器人被移出群 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.chat.member.bot.deleted_v1")] +//【消息与群组】机器人进群 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.chat.member.bot.added_v1")] +//【消息与群组】接收消息 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.message.receive_v1")] +//【消息与群组】群解散 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.chat.disbanded_v1")] +//【消息与群组】群配置修改 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.chat.updated_v1")] +//【消息与群组】删除消息表情回复 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.message.reaction.deleted_v1")] +//【消息与群组】消息已读 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.message.message_read_v1")] +//【消息与群组】新增消息表情回复 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.message.reaction.created_v1")] +//【消息与群组】用户出群 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.chat.member.user.deleted_v1")] +//【消息与群组】用户进群 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.chat.member.user.added_v1")] +//【消息与群组】用户进入与机器人的会话 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "im.chat.access_event.bot_p2p_chat_entered_v1")] +//【应用信息】撤回应用发布申请 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "application.application.app_version.publish_revoke_v6")] +//【应用信息】反馈更新 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "application.application.feedback.updated_v6")] +//【应用信息】机器人自定义菜单事件 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "application.bot.menu_v6")] +//【应用信息】申请发布应用 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "application.application.app_version.publish_apply_v6")] +//【应用信息】新增应用反馈 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "application.application.feedback.created_v6")] +//【应用信息】应用创建 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "application.application.created_v6")] +//【应用信息】应用审核 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "application.application.app_version.audit_v6")] +//【应用信息】员工免审安装应用 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "application.application.visibility.added_v6")] +//【云文档】多维表格记录变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.bitable_record_changed_v1")] +//【云文档】多维表格字段变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.bitable_field_changed_v1")] +//【云文档】文件编辑 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.edit_v1")] +//【云文档】文件标题变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.title_updated_v1")] +//【云文档】文件彻底删除 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.deleted_v1")] +//【云文档】文件删除到回收站 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.trashed_v1")] +//【云文档】文件协作者添加 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.permission_member_added_v1")] +//【云文档】文件协作者移除 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.permission_member_removed_v1")] +//【云文档】文件已读 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "drive.file.read_v1")] +//【招聘】创建背调 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.eco_background_check.created_v1")] +//【招聘】创建笔试 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.eco_exam.created_v1")] +//【招聘】导入 e-HR +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.ehr_import_task.imported_v1")] +//【招聘】导入 e-HR(实习 Offer) +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.ehr_import_task_for_internship_offer.imported_v1")] +//【招聘】内推账户余额变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.referral_account.assets_update_v1")] +//【招聘】删除人才 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.talent.deleted_v1")] +//【招聘】删除投递 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.application.deleted_v1")] +//【招聘】投递阶段变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.application.stage_changed_v1")] +//【招聘】账号绑定 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.eco_account.created_v1")] +//【招聘】终止背调 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.eco_background_check.canceled_v1")] +//【招聘】Offer 状态变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "hire.offer.status_changed_v1")] +//【智能门禁】新增门禁访问记录 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "acs.access_record.created_v1")] +//【智能门禁】用户信息变更 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "acs.user.updated_v1")] +//【eLearning】课程学习进度更新事件 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "elearning.course_registration.updated_v2")] +//【eLearning】课程学习进度删除事件 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "elearning.course_registration.deleted_v2")] +//【eLearning】课程学习进度新增事件 +[JsonDerivedType(typeof(EventV2Dto), typeDiscriminator: "elearning.course_registration.created_v2")] +public record EventDto +{ + /// 类型鉴别器 + [JsonPropertyName(FeishuNetSdkOptions.Discriminator), JsonPropertyOrder(-1)] + public virtual string? Discriminator { get; set; } + + /// 事件 Token,即Verification Token。用于验证来自于同一个应用 + [JsonPropertyName("token")] + public virtual string Token { get; set; } = string.Empty; + + /// 事件唯一Id + [JsonPropertyName("event_id")] + public virtual string EventId { get; set; } = string.Empty; +} diff --git a/src/DependencyInjection/FeishuNetSdkExtensions.cs b/src/DependencyInjection/FeishuNetSdkExtensions.cs index 555920f2..5f81f48d 100644 --- a/src/DependencyInjection/FeishuNetSdkExtensions.cs +++ b/src/DependencyInjection/FeishuNetSdkExtensions.cs @@ -89,6 +89,8 @@ private static IServiceCollection AddFeishuNetSdk(this IServiceCollection servic services.AddHttpApi(option => option.KeyValueSerializeOptions.IgnoreNullValues = true); services.AddHttpApi(option => option.KeyValueSerializeOptions.IgnoreNullValues = true); + services.TryAddSingleton(); + using var serviceProvider = services.BuildServiceProvider(); var options = serviceProvider.GetRequiredService>(); @@ -102,7 +104,11 @@ private static IServiceCollection AddFeishuNetSdk(this IServiceCollection servic return new AppAccessTokenProvider(serviceProvider, options.Value); }); - services.TryAddSingleton(); + var eventServiceProvider = serviceProvider.GetRequiredService(); + var handlers = eventServiceProvider.FindAllHandlers(); + foreach (var eventHandlerDescriptor in handlers) + services.Add(new ServiceDescriptor(eventHandlerDescriptor.EventHandlerType, + eventHandlerDescriptor.EventHandlerType, ServiceLifetime.Transient)); return services; } diff --git a/src/Extensions/DtoExtensions.cs b/src/Extensions/DtoExtensions.cs index a432767a..65e99485 100644 --- a/src/Extensions/DtoExtensions.cs +++ b/src/Extensions/DtoExtensions.cs @@ -435,5 +435,28 @@ public static Im.Dtos.TableElement SetRows(this Im.Dtos.TableElement Dto, IEnume return Dto; } + + /// + /// 添加卡片信息 + /// + /// 卡片交互响应体 + /// 卡片内容 + /// + public static CallbackEvents.CardActionTriggerResponseDto SetCard(this CallbackEvents.CardActionTriggerResponseDto Dto, Im.Dtos.MessageCard Card) + { + Dto.Card ??= new(); + switch (Card) + { + case Im.Dtos.TemplateCardDto templateCardDto: + Dto.Card.Data = templateCardDto.Data; + Dto.Card.Type = templateCardDto.Type; + break; + default: + Dto.Card.Data = Card; + Dto.Card.Type = "raw"; + break; + } + return Dto; + } } } diff --git a/src/Extensions/InnerExtensions.cs b/src/Extensions/InnerExtensions.cs new file mode 100644 index 00000000..da432938 --- /dev/null +++ b/src/Extensions/InnerExtensions.cs @@ -0,0 +1,135 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-09-07 +// +// Last Modified By : yxr +// Last Modified On : 2024-09-07 +// ************************************************************************ +// +// MIT +// +// 扩展方法 +// ************************************************************************ +global using FeishuNetSdk.Extensions; +using System.Text.RegularExpressions; + +namespace FeishuNetSdk.Extensions +{ + internal static partial class InnerExtensions + { + /// + /// 合并两个字典,并覆盖相同键名的值 + /// + /// + /// + /// + /// + public static void Merge(this IDictionary to, IDictionary? from) + { + ArgumentNullException.ThrowIfNull(to); + if (from is null) return; + foreach (var kv in from) + { + if (to.ContainsKey(kv.Key)) + { + to[kv.Key] = kv.Value; + } + else + { + to.Add(kv.Key, kv.Value); + } + } + } + + /// + /// 是否为类型的子类或自身 + /// + /// + /// 父类 + /// + public static bool IsSubTypeOrEqualsOf(this Type type, Type parentType) + { + return parentType.IsAssignableFrom(type); + } + + /// + /// 判断给定的类型是否继承自泛型类型, + /// + /// e.g.: typeof(Child<>).IsSubTypeOfGenericType(typeof(IParent<>)); result->true + /// + /// + /// e.g.: typeof(Child<int>).IsSubTypeOfGenericType(typeof(IParent<>)); result->true + /// + /// + /// 子类型 + /// 泛型父级,例: typeof(IParent<>) + /// + public static bool IsSubTypeOfGenericType(this Type childType, Type genericType) + { + if (childType == genericType) + return false; + if (!genericType.IsGenericTypeDefinition) + return false; + var interfaceTypes = childType.GetTypeInfo().ImplementedInterfaces; + + foreach (var it in interfaceTypes) + { + if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) + return true; + } + + if (childType.IsGenericType && childType.GetGenericTypeDefinition() == genericType) + return true; + + var baseType = childType.BaseType; + if (baseType is null) return false; + + return IsSubTypeOfGenericType(baseType, genericType); + } + + /// + /// 获取字典值,没有则返回默认值 + /// + /// + /// + /// + /// + /// + public static TValue? GetOrDefault(this IDictionary dic, TKey key) + { + if (key is null) + return default; + return dic.TryGetValue(key, out var value) ? value : default; + } + + public static string FixDiscriminator(this string value, string propertyName = FeishuNetSdkOptions.Discriminator) + => MatchEventV2Type().Match(value) is Match ma && ma.Success + ? value.Insert(1, ma.Value) + : MatchEventV1Type().Matches(value) is MatchCollection mb + && mb.FirstOrDefault(m => m.Groups[1].Value != "event_callback") is Match mc + && mc.Success + ? value.Insert(1, $"\"{propertyName}\":\"{mc.Groups[1].Value}\",") + : value; + + public static bool IsEncryptedObject(this string value, out string? encryptedString) + { + encryptedString = null; + if (MatchEncryptedType().Match(value) is Match ma && ma.Success) + { + encryptedString = Regex.Unescape(ma.Groups[1].Value); + return true; + } + return false; + } + + [GeneratedRegex("\"event_type\"[: \t\n\r]+\"([^\"]+)\"[ \n\r\t]*,")] + private static partial Regex MatchEventV2Type(); + + [GeneratedRegex("\"type\"[: \t\n\r]+\"([^\"]+)\"")] + private static partial Regex MatchEventV1Type(); + + [GeneratedRegex("^\\{[^\"]*\"encrypt\"[: ]+\"([^\"]+)\"[^\"]*\\}$")] + private static partial Regex MatchEncryptedType(); + } +} diff --git a/src/FeishuNetSdk.csproj b/src/FeishuNetSdk.csproj index a6695547..1790b1d6 100644 --- a/src/FeishuNetSdk.csproj +++ b/src/FeishuNetSdk.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable True @@ -12,19 +12,21 @@ README.md https://github.com/vicenteyu/FeishuNetSdk git - feishu; sdk; dotnet; .net6.0 + feishu; sdk; dotnet; .net8.0 MIT 适用于飞书开放平台的.Net开发包 LICENSE - 2.4.6 + 3.0.0 4 + 1701;1702;IDE0301 4 + 1701;1702;IDE0301 @@ -39,6 +41,7 @@ + diff --git a/src/Services/CallbackV2Dto.cs b/src/Services/CallbackV2Dto.cs new file mode 100644 index 00000000..65b33bfe --- /dev/null +++ b/src/Services/CallbackV2Dto.cs @@ -0,0 +1,30 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-08-31 +// ************************************************************************ +// +// MIT +// +// 回调事件 +// ************************************************************************ +namespace FeishuNetSdk.Services +{ + /// + /// 回调事件接口 + /// + public interface IAmCallbackDto + { + } + + /// + /// 回调事件 + /// + /// + public record CallbackV2Dto : EventV2Dto, IAmCallbackDto where T : EventBodyDto + { + } +} diff --git a/src/Services/EventCallbackServiceProvider.cs b/src/Services/EventCallbackServiceProvider.cs new file mode 100644 index 00000000..f3989308 --- /dev/null +++ b/src/Services/EventCallbackServiceProvider.cs @@ -0,0 +1,203 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-09-07 +// ************************************************************************ +// +// MIT +// +// +// ************************************************************************ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Text.Json; + +namespace FeishuNetSdk.Services +{ + /// + /// ¼ص + /// + /// + /// + /// + public class EventCallbackServiceProvider(ILogger logger, IOptionsMonitor options, IServiceScopeFactory scopeFactory) : IEventCallbackServiceProvider + { + private static EventHandlerDescriptor[]? _cache; + + /// + /// ¼ִз + /// + /// + public IEnumerable FindAllHandlers() + { + if (_cache is not null) return _cache; + lock (this) + { + if (_cache is not null) return _cache; + + var baseType = typeof(IEventCallbackHandler<,>); + var types = ReflectionHelper.GetFinalSubTypes(baseType); + + _cache = types + .Select(typeInfo => + { + var firstArgumentType = GetArgumentTypes(typeInfo, baseType); + return new EventHandlerDescriptor + { + EventHandlerName = typeInfo.Name, + EventHandlerType = typeInfo, + EventName = firstArgumentType.Name, + EventType = firstArgumentType, + NotAllowRepeat = firstArgumentType.IsAssignableTo(typeof(IAmCallbackDto)) + }; + }) + .ToArray(); + + var check_repeated = _cache.Where(p => p.NotAllowRepeat) + .GroupBy(p => p.EventType) + .Where(p => p.Count() > 1) + .Select(p => p.Key.FullName); + if (check_repeated.Any()) + throw new Exception($"ظĻص{string.Join("", check_repeated)}"); + } + + return _cache; + } + private static Type GetArgumentTypes(Type type, Type baseType) + { + return type.GetTypeInfo() + .ImplementedInterfaces + .Single(r => r.IsGenericType && r.GetGenericTypeDefinition() == baseType) + .GetGenericArguments() + .First(); + } + private IEnumerable? GetHandlerDescriptorByEvent(Type eventType) + { + if (_cache is null) + FindAllHandlers(); + + return _cache?.Where(p => p.EventType == eventType); + } + + /// + /// ִ¼ + /// + public async Task HandleAsync(object input) + { + var serializeString = JsonSerializer.Serialize(input); + if (serializeString.IsEncryptedObject(out var encryptedString) && encryptedString != null) + { + if (options.CurrentValue.EncryptKey == null) return new HandleResult(Error: "δýԿ"); + try + { + serializeString = AesCipher.DecryptString(encryptedString, options.CurrentValue.EncryptKey); + } + catch (Exception ex) + { + return new HandleResult(Error: $"ʧܣ{(ex.InnerException ?? ex).Message}"); + } + } + try + { + return await HandleAsync(serializeString); + } + catch (Exception ex) + { + return new HandleResult(Error: $"¼ִг쳣{(ex.InnerException ?? ex).Message}"); + } + } + + /// + /// ִ¼ + /// + /// + /// + public async Task HandleAsync(string json) + { + logger.LogInformation("¼Ϣ{json}", json); + + json = json.FixDiscriminator(); + var dto = JsonSerializer.Deserialize(json); + + if (dto is null) return new HandleResult(Error: $"л¼ʧܣ{json}"); + logger.LogInformation("лɹ{event_type}", dto.Discriminator); + + if (!string.IsNullOrWhiteSpace(dto.Token) && dto.Token != options.CurrentValue.VerificationToken) + return new HandleResult(Error: $"Ӧñʶһ£VerificationToken: {options.CurrentValue.VerificationToken}"); + + if (dto is UrlVerificationDto urlVerification) + return new HandleResult(true, Dto: urlVerification); + + return await HandleAsync(dto); + } + + /// + /// ִ¼ + /// + /// + /// + private async Task HandleAsync(EventDto eventDto) + { + var handlers = GetHandlerDescriptorByEvent(eventDto.GetType())?.ToArray(); + if (handlers is null || handlers.Length == 0) + return new HandleResult(Error: $"δ¼{eventDto.Discriminator}"); + + logger.LogInformation("¼ {count}", handlers.Length); + + (int millisecond, string message) = (2700, "ִгʱδ3Ӧ"); + + using var scope = scopeFactory.CreateScope(); + if (eventDto.GetType().GetGenericTypeDefinition() == typeof(CallbackV2Dto<>)) + { + if (handlers.Length > 1) + return new HandleResult(Error: $"صظ壺{(string.Join("", handlers.Select(k => k.EventHandlerName)))}"); + + //صͬһصֻһ + var eventHandlerType = handlers[0].EventHandlerType; + var handlerInstance = scope.ServiceProvider.GetRequiredService(eventHandlerType); + dynamic? task = eventHandlerType.GetMethod("ExecuteAsync")?.Invoke(handlerInstance, [eventDto]); + await task?.WaitAsync(TimeSpan.FromMilliseconds(millisecond)); + return new HandleResult(true, Dto: task?.Result); + } + else + { + var tasks = handlers + .Select(p => new + { + p.EventHandlerType, + EventMethod = p.EventHandlerType.GetMethod("ExecuteAsync", [p.EventType]) + }) + .Where(p => p.EventMethod is not null) + .Select(p => + { + var handlerInstance = scope.ServiceProvider.GetRequiredService(p.EventHandlerType); + return (System.Threading.Tasks.Task?)p.EventMethod!.Invoke(handlerInstance, [eventDto]); + }) + .Where(p => p is not null) + .Select(p => p!) + .ToArray(); + + //¼ִɲ + var is_all_success = System.Threading.Tasks.Task.WaitAll(tasks, millisecond); + if (!is_all_success) return new HandleResult(Error: message); + + return new HandleResult(true); + } + } + + /// + /// ¼ִн + /// + /// + /// + /// + public record HandleResult( + [property: JsonPropertyName("success")] bool Success = false, + [property: JsonPropertyName("error")] string? Error = null, + [property: JsonPropertyName("dto")] object? Dto = null); + } +} diff --git a/src/Services/EventDto.cs b/src/Services/EventDto.cs new file mode 100644 index 00000000..685eac68 --- /dev/null +++ b/src/Services/EventDto.cs @@ -0,0 +1,35 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-08-31 +// ************************************************************************ +// +// MIT +// +// 事件体 +// ************************************************************************ +namespace FeishuNetSdk.Services +{ + /// + /// 事件序列化基类 + /// + /// 事件主体 + public record EventDto : EventDto where T : EventBodyDto + { + /// + /// 类型鉴别器 + /// + [JsonPropertyName(FeishuNetSdkOptions.Discriminator), JsonPropertyOrder(-1)] + public override string? Discriminator => Event?.Discriminator; + + /// + /// 事件体 + /// 必填:否 + /// + [JsonPropertyName("event")] + public T? Event { get; set; } + } +} diff --git a/src/Services/EventHandlerDescriptor.cs b/src/Services/EventHandlerDescriptor.cs new file mode 100644 index 00000000..5ebd19eb --- /dev/null +++ b/src/Services/EventHandlerDescriptor.cs @@ -0,0 +1,46 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-08-31 +// ************************************************************************ +// +// MIT +// +// 事件处理方法类 +// ************************************************************************ +namespace FeishuNetSdk.Services +{ + /// + /// 事件处理方法类 + /// + public class EventHandlerDescriptor + { + /// + /// 事件处理方法名称 + /// + public required string EventHandlerName { get; set; } + + /// + /// 事件处理类名称 + /// + public required Type EventHandlerType { get; set; } + + /// + /// 事件名称 + /// + public required string EventName { get; set; } + + /// + /// 事件类型 + /// + public required Type EventType { get; set; } + + /// + /// 不允许重复的事件处理方法 + /// + public bool NotAllowRepeat { get; set; } + } +} diff --git a/src/Services/EventV1Dto.cs b/src/Services/EventV1Dto.cs new file mode 100644 index 00000000..363e47b0 --- /dev/null +++ b/src/Services/EventV1Dto.cs @@ -0,0 +1,48 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-08-31 +// ************************************************************************ +// +// MIT +// +// 事件体 +// ************************************************************************ +namespace FeishuNetSdk.Services +{ + /// + /// 事件 V1.0 + /// + public record EventV1Dto : EventDto where T : EventBodyDto + { + /// + /// 事件唯一Id + /// + [JsonPropertyName("event_id")] + public override string EventId => Uuid; + + /// + /// 事件发送的时间,一般近似于事件发生的时间。 + /// 必填:否 + /// + [JsonPropertyName("ts")] + public string? Ts { get; set; } + + /// + /// 事件的唯一标识。 + /// 必填:否 + /// + [JsonPropertyName("uuid")] + public string Uuid { get; set; } = string.Empty; + + /// + /// 此事件此处始终为event_callback。 + /// 必填:否 + /// + [JsonPropertyName("type")] + public string? Type { get; set; } + } +} diff --git a/src/Services/EventV2Dto.cs b/src/Services/EventV2Dto.cs new file mode 100644 index 00000000..bb9eb6a8 --- /dev/null +++ b/src/Services/EventV2Dto.cs @@ -0,0 +1,87 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-08-31 +// ************************************************************************ +// +// MIT +// +// 事件体 +// ************************************************************************ +namespace FeishuNetSdk.Services +{ + /// + /// 事件 V2.0 + /// + public record EventV2Dto : EventDto where T : EventBodyDto + { + /// + /// 事件唯一Id + /// + [JsonPropertyName("event_id")] + public override string EventId => Header.EventId; + + /// + /// 事件 Token,即Verification Token。用于验证来自于同一个应用 + /// + [JsonPropertyName("token")] + public override string Token => Header.Token; + + /// + /// 事件模式 + /// + [JsonPropertyName("schema")] + public string Schema { get; set; } = string.Empty; + + /// + /// 事件头 + /// + [JsonPropertyName("header")] + public HeaderSuffix Header { get; set; } = new(); + + /// + /// 事件头 + /// + public record HeaderSuffix + { + /// + /// 事件 ID + /// + [JsonPropertyName("event_id")] + public string EventId { get; set; } = string.Empty; + + /// + /// 事件类型 + /// + [JsonPropertyName("event_type")] + public string EventType { get; set; } = string.Empty; + + /// + /// 事件创建时间戳(单位:毫秒) + /// + [JsonPropertyName("create_time")] + public string CreateTime { get; set; } = string.Empty; + + /// + /// 事件 Token,验证来自于同一个应用 + /// + [JsonPropertyName("token")] + public string Token { get; set; } = string.Empty; + + /// + /// 应用 ID + /// + [JsonPropertyName("app_id")] + public string AppId { get; set; } = string.Empty; + + /// + /// 租户 Key + /// + [JsonPropertyName("tenant_key")] + public string TenantKey { get; set; } = string.Empty; + } + } +} diff --git a/src/Services/IEventCallbackHandler.cs b/src/Services/IEventCallbackHandler.cs new file mode 100644 index 00000000..158f1e9a --- /dev/null +++ b/src/Services/IEventCallbackHandler.cs @@ -0,0 +1,58 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-08-31 +// ************************************************************************ +// +// MIT +// +// 事件回调处理接口 +// ************************************************************************ +namespace FeishuNetSdk.Services +{ + /// + /// 事件回调处理接口 + /// + /// 消息体 + /// 事件体 + public interface IEventCallbackHandler where T1 : EventDto where T2 : EventBodyDto + { + } + + /// + /// + /// + /// 消息体 + /// 事件体 + public interface IEventHandler : IEventCallbackHandler where T1 : EventDto where T2 : EventBodyDto + { + /// + /// 事件执行方法 + /// + /// + /// + System.Threading.Tasks.Task ExecuteAsync(T1 input); + } + + /// + /// + /// + /// 消息体 + /// 事件体 + /// 响应体 + public interface ICallbackHandler : IEventCallbackHandler + where T1 : CallbackV2Dto + where T2 : EventBodyDto + where T3 : CallbackResponseDto + { + /// + /// 回调执行方法 + /// + /// + /// + System.Threading.Tasks.Task ExecuteAsync(T1 input); + } +} diff --git a/src/Services/IEventCallbackServiceProvider.cs b/src/Services/IEventCallbackServiceProvider.cs new file mode 100644 index 00000000..bfc8913e --- /dev/null +++ b/src/Services/IEventCallbackServiceProvider.cs @@ -0,0 +1,42 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-08-31 +// ************************************************************************ +// +// MIT +// +// +// ************************************************************************ + +namespace FeishuNetSdk.Services +{ + /// + /// 事件回调服务 + /// + public interface IEventCallbackServiceProvider + { + /// + /// + /// + /// + /// + Task HandleAsync(object input); + + /// + /// + /// + /// + /// + Task HandleAsync(string json); + + /// + /// + /// + /// + IEnumerable FindAllHandlers(); + } +} \ No newline at end of file diff --git a/src/Services/ReflectionHelper.cs b/src/Services/ReflectionHelper.cs new file mode 100644 index 00000000..aa40f420 --- /dev/null +++ b/src/Services/ReflectionHelper.cs @@ -0,0 +1,407 @@ +global using System.Reflection; +using Microsoft.Extensions.DependencyModel; +using System.Collections.Concurrent; + +namespace FeishuNetSdk.Services +{ + /// + /// 反射相关方法 + /// + public static class ReflectionHelper + { + /// + /// 获取引用了程序集的所有的程序集 + /// + /// 引用这个程序集 + /// 依赖上下文,null则使用默认 + /// + public static List GetReferredAssemblies(Assembly assembly, + DependencyContext? dependencyContext = null) + { + dependencyContext ??= DependencyContext.Default + ?? throw new ArgumentNullException(nameof(dependencyContext)); + + var res = CacheAssemblyReferred.GetOrDefault(assembly); + if (res is not null) + return res; + + var allLib = dependencyContext + .RuntimeLibraries + .OrderBy(r => r.Name) + .ToList(); + + var name = assembly.GetName().Name; + if (name is null) return []; + + Dictionary> allDependencies = []; + foreach (var item in allLib) + { + allDependencies.Add(item.Name, []); + LoadAllDependency(allLib, item.Name, [], item, allDependencies); + } + + var list = allDependencies + .Where(r => r.Value.Contains(name)) + .Select(r => + { + try + { + return Assembly.Load(r.Key); + } + catch + { + return null; + } + }) + .Where(r => r is not null) + .ToList(); + + res = list!; + CacheAssemblyReferred.TryAdd(assembly, res); + return res; + } + + /// + /// 获取引用了的程序集 + /// + /// 依赖上下文,null则使用默认 + /// + public static List GetReferredAssemblies(DependencyContext? dependencyContext = null) + { + return GetReferredAssemblies(typeof(TType).Assembly, dependencyContext); + } + + /// + /// 获取引用了的程序集 + /// + /// 依赖上下文,null则使用默认 + /// + public static List GetReferredAssemblies(Type type, DependencyContext? dependencyContext = null) + { + return GetReferredAssemblies(type.Assembly, dependencyContext); + } + + /// + /// 获取所有的子类,不包括接口和抽象类,包含泛型定义 + /// + /// 基类,可以是泛型定义 + /// + /// + public static List GetFinalSubTypes(Type baseType, Assembly assembly) + { + return assembly.DefinedTypes.Where(r => + !r.IsAbstract && r.IsClass && (r.IsSubTypeOrEqualsOf(baseType) || r.IsSubTypeOfGenericType(baseType))).ToList(); + } + + /// + /// 获取所有的子类和自身,不包括接口和抽象类,包含泛型定义 + /// + /// 基类,可以是泛型定义 + /// + /// + public static List GetFinalSubTypes(Type baseType, DependencyContext? dependencyContext = null) + { + List types = []; + foreach (var item in GetReferredAssemblies(baseType, dependencyContext)) + { + types.AddRange(GetFinalSubTypes(baseType, item)); + } + + types.AddRange(GetFinalSubTypes(baseType, baseType.Assembly)); + return types; + } + + /// + /// 获取所有的子类和自身,不包括接口和抽象类,包含泛型定义 + /// + /// + /// + public static List GetFinalSubTypes(Assembly assembly) + { + return GetFinalSubTypes(typeof(TBaseType), assembly); + } + + /// + /// 获取所有的子类和自身,不包括接口和抽象类,包含泛型定义 + /// + /// + /// + /// + public static List GetFinalSubTypes(DependencyContext? dependencyContext = null) + { + return GetFinalSubTypes(typeof(TBaseType), dependencyContext); + } + + /// + /// 获取所有的子类和自身,包括接口和抽象类,包含泛型定义 + /// + /// 基类,可以是泛型定义 + /// + /// + public static List GetSubTypes(Type baseType, Assembly assembly) + { + return assembly + .DefinedTypes + .Where(r => r.IsSubTypeOrEqualsOf(baseType) || r.IsSubTypeOfGenericType(baseType)) + .ToList(); + } + + /// + /// 获取所有的子类和自身,包括接口和抽象类,包含泛型定义 + /// + /// 基类,可以是泛型定义 + /// + /// + public static List GetSubTypes(Type baseType, DependencyContext? dependencyContext = null) + { + var types = new List(); + foreach (var item in GetReferredAssemblies(baseType, dependencyContext)) + types.AddRange(GetSubTypes(baseType, item)); + + types.AddRange(GetSubTypes(baseType, baseType.Assembly)); + return types; + } + + /// + /// 获取所有的子类和自身,包括接口和抽象类,包含泛型定义 + /// + /// + /// + public static List GetSubTypes(Assembly assembly) + { + return GetSubTypes(typeof(TBaseType), assembly); + } + + /// + /// 获取所有的子类和自身,包括接口和抽象类,包含泛型定义 + /// + /// + /// + /// + public static List GetSubTypes(DependencyContext? dependencyContext = null) + { + return GetSubTypes(typeof(TBaseType), dependencyContext); + } + + /// + /// 根据特性获取类和特性值,不支持多特性Multiple + /// + /// + /// + /// + /// + public static IDictionary GetTypesAndAttribute(Type baseType, Assembly assembly, + bool inherit = true) + { + Dictionary dic = []; + + foreach (var item in assembly.DefinedTypes) + { + var attr = item.GetCustomAttribute(baseType, inherit); + if (attr is null) + continue; + + dic.Add(item, attr); + } + + return dic; + } + + /// + /// 根据特性获取类和特性值,不支持多特性Multiple + /// + /// 基类,可以是泛型定义 + /// + /// + /// + public static IDictionary GetTypesAndAttribute(Type baseType, + DependencyContext? dependencyContext = null, bool inherit = true) + { + Dictionary dic = []; + foreach (var item in GetReferredAssemblies(baseType, dependencyContext)) + { + dic.Merge(GetTypesAndAttribute(baseType, item, inherit)); + } + + dic.Merge(GetTypesAndAttribute(baseType, baseType.Assembly, inherit)); + + return dic; + } + + /// + /// 根据特性获取类和特性值,不支持多特性Multiple + /// + /// + /// + /// + public static IDictionary GetTypesAndAttribute(Assembly assembly, + bool inherit = true) + where TAttribute : Attribute + { + Dictionary dic = []; + + foreach (var item in assembly.DefinedTypes) + { + var attr = item.GetCustomAttribute(inherit); + if (attr is null) + continue; + + dic.Add(item, attr); + } + + return dic; + } + + /// + /// 根据特性获取类和特性值,不支持多特性Multiple + /// + /// + /// + /// + /// + public static IDictionary GetTypesAndAttribute( + DependencyContext? dependencyContext = null, bool inherit = true) + where TAttribute : Attribute + { + Dictionary dic = []; + foreach (var item in GetReferredAssemblies(dependencyContext)) + { + dic.Merge(GetTypesAndAttribute(item, inherit)); + } + + dic.Merge(GetTypesAndAttribute(typeof(TAttribute).Assembly, inherit)); + + return dic; + } + + /// + /// 根据特性获取类和特性值,不支持多特性Multiple + /// + /// + /// + /// + /// + public static IDictionary> GetTypesByAttributes(Type baseType, Assembly assembly, + bool inherit = true) + { + Dictionary> dic = []; + + foreach (var item in assembly.DefinedTypes) + { + var attrs = item.GetCustomAttributes(baseType, inherit); + if (attrs is null || attrs.Length == 0) + continue; + + dic.Add(item, attrs); + } + + return dic; + } + + /// + /// 根据特性获取类和特性值,不支持多特性Multiple + /// + /// + /// + /// + /// + public static IDictionary> GetTypesByAttributes(Type baseType, + DependencyContext? dependencyContext = null, bool inherit = true) + { + Dictionary> dic = []; + foreach (var item in GetReferredAssemblies(baseType, dependencyContext)) + { + dic.Merge(GetTypesByAttributes(baseType, item, inherit)); + } + + dic.Merge(GetTypesByAttributes(baseType, baseType.Assembly, inherit)); + + return dic; + } + + /// + /// 根据特性获取类和特性值,不支持多特性Multiple + /// + /// + /// + /// + public static IDictionary> GetTypesByAttributes(Assembly assembly, + bool inherit = true) + where TAttribute : Attribute + { + Dictionary> dic = []; + + foreach (var item in assembly.DefinedTypes) + { + var attrs = item.GetCustomAttributes(inherit).ToList(); + if (attrs.Count == 0) + continue; + + dic.Add(item, attrs); + } + + return dic; + } + + /// + /// 根据特性获取类和特性值,不支持多特性Multiple + /// + /// + /// + /// + /// + public static IDictionary> GetTypesByAttributes( + DependencyContext? dependencyContext = null, bool inherit = true) + where TAttribute : Attribute + { + Dictionary> dic = []; + foreach (var item in GetReferredAssemblies(dependencyContext)) + { + dic.Merge(GetTypesByAttributes(item, inherit)); + } + + dic.Merge(GetTypesByAttributes(typeof(TAttribute).Assembly, inherit)); + + return dic; + } + + #region private + + /// + /// 缓存程序集被引用数据 + /// + private static readonly ConcurrentDictionary> CacheAssemblyReferred = + new(); + + /// + /// 加载所有的依赖 + /// + /// 应用包含的所有程序集 + /// 当前计算的程序集name + /// 当前计算的程序集,已经处理过的依赖程序集名称 + /// 递归正在处理的程序集名称 + /// 所有的依赖数据 + private static void LoadAllDependency(IEnumerable allLibs, string key, HashSet handled, + RuntimeLibrary current, Dictionary> allDependencies) + { + if (current.Dependencies.Count == 0) + return; + if (handled.Contains(current.Name)) + return; + handled.Add(current.Name); + var runtimeLibraries = allLibs.ToList(); + foreach (var item in current.Dependencies) + { + allDependencies[key].Add(item.Name); + + var next = runtimeLibraries.FirstOrDefault(r => r.Name == item.Name); + if (next is null || next.Dependencies.Count == 0) + continue; + LoadAllDependency(runtimeLibraries, key, handled, next, allDependencies); + } + } + + #endregion + } + +} diff --git a/src/Services/UrlVerificationDto.cs b/src/Services/UrlVerificationDto.cs new file mode 100644 index 00000000..1181872d --- /dev/null +++ b/src/Services/UrlVerificationDto.cs @@ -0,0 +1,30 @@ +// ************************************************************************ +// Assembly : FeishuNetSdk +// Author : yxr +// Created : 2024-08-31 +// +// Last Modified By : yxr +// Last Modified On : 2024-08-31 +// ************************************************************************ +// +// MIT +// +// 事件终结点验证结构体 +// ************************************************************************ +namespace FeishuNetSdk.Services +{ + /// + /// 事件终结点验证结构体 + /// + /// + /// + public record UrlVerificationDto([property: JsonPropertyName("challenge")] string Challenge, + [property: JsonPropertyName("type")] string Type) : EventDto + { + /// + /// + /// + [JsonPropertyName(FeishuNetSdkOptions.Discriminator), JsonPropertyOrder(-1)] + public override string? Discriminator => "url_verification"; + } +}