submodule对gitea不管用,所以直接拉了一份拉格兰

This commit is contained in:
2025-02-04 16:29:43 +08:00
parent b0bfc803e3
commit d149a2ea0f
1023 changed files with 43308 additions and 18 deletions

243
Lagrange.Core/.editorconfig Normal file
View File

@@ -0,0 +1,243 @@
# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
# C# files
[*.cs]
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
indent_style = space
tab_width = 4
# New line preferences
end_of_line = lf
insert_final_newline = false
#### .NET Coding Conventions ####
# Organize usings
dotnet_separate_import_directive_groups = false
dotnet_sort_system_directives_first = true
file_header_template = unset
# this. and Me. preferences
dotnet_style_qualification_for_event = false
dotnet_style_qualification_for_field = false
dotnet_style_qualification_for_method = false
dotnet_style_qualification_for_property = false
# Language keywords vs BCL types preferences
dotnet_style_predefined_type_for_locals_parameters_members = true
dotnet_style_predefined_type_for_member_access = true
# Parentheses preferences
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
dotnet_style_parentheses_in_other_operators = never_if_unnecessary
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
# Modifier preferences
dotnet_style_require_accessibility_modifiers = for_non_interface_members
# Expression-level preferences
dotnet_style_coalesce_expression = true
dotnet_style_collection_initializer = true
dotnet_style_explicit_tuple_names = true
dotnet_style_namespace_match_folder = false
dotnet_style_null_propagation = true
dotnet_style_object_initializer = true
dotnet_style_operator_placement_when_wrapping = beginning_of_line
dotnet_style_prefer_auto_properties = true
dotnet_style_prefer_collection_expression = when_types_loosely_match
dotnet_style_prefer_compound_assignment = true
dotnet_style_prefer_conditional_expression_over_assignment = true
dotnet_style_prefer_conditional_expression_over_return = true
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
dotnet_style_prefer_inferred_anonymous_type_member_names = true
dotnet_style_prefer_inferred_tuple_names = true
dotnet_style_prefer_is_null_check_over_reference_equality_method = true
dotnet_style_prefer_simplified_boolean_expressions = true
dotnet_style_prefer_simplified_interpolation = true
# Field preferences
dotnet_style_readonly_field = true
# Parameter preferences
dotnet_code_quality_unused_parameters = all
# Suppression preferences
dotnet_remove_unnecessary_suppression_exclusions = none
# New line preferences
dotnet_style_allow_multiple_blank_lines_experimental = false
dotnet_style_allow_statement_immediately_after_block_experimental = true
#### C# Coding Conventions ####
# var preferences
# lwx like var
csharp_style_var_elsewhere = true
# but don't like built in types use var
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = true
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
csharp_style_expression_bodied_constructors = false:silent
csharp_style_expression_bodied_indexers = true:silent
csharp_style_expression_bodied_lambdas = true:silent
csharp_style_expression_bodied_local_functions = false:silent
csharp_style_expression_bodied_methods = false:silent
csharp_style_expression_bodied_operators = false:silent
csharp_style_expression_bodied_properties = true:silent
# Pattern matching preferences
csharp_style_pattern_matching_over_as_with_null_check = true
csharp_style_pattern_matching_over_is_with_cast_check = true
csharp_style_prefer_extended_property_pattern = true
csharp_style_prefer_not_pattern = true
csharp_style_prefer_pattern_matching = true
csharp_style_prefer_switch_expression = true
# Null-checking preferences
csharp_style_conditional_delegate_call = true
# Modifier preferences
csharp_prefer_static_local_function = true
csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
csharp_style_prefer_readonly_struct = true
csharp_style_prefer_readonly_struct_member = true
# Code-block preferences
csharp_prefer_braces = true:silent
csharp_prefer_simple_using_statement = true:suggestion
csharp_style_namespace_declarations = block_scoped:silent
csharp_style_prefer_method_group_conversion = true:silent
csharp_style_prefer_primary_constructors = true:suggestion
csharp_style_prefer_top_level_statements = true:silent
# Expression-level preferences
csharp_prefer_simple_default_expression = true
csharp_style_deconstructed_variable_declaration = true
csharp_style_implicit_object_creation_when_type_is_apparent = true
csharp_style_inlined_variable_declaration = true
csharp_style_prefer_index_operator = true
csharp_style_prefer_local_over_anonymous_function = true
csharp_style_prefer_null_check_over_type_check = true
csharp_style_prefer_range_operator = true
csharp_style_prefer_tuple_swap = true
csharp_style_prefer_utf8_string_literals = true
csharp_style_throw_expression = true
csharp_style_unused_value_assignment_preference = discard_variable
csharp_style_unused_value_expression_statement_preference = discard_variable
# 'using' directive preferences
csharp_using_directive_placement = outside_namespace:silent
# New line preferences
csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true
csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true
csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true
csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true
csharp_style_allow_embedded_statements_on_same_line_experimental = true
#### C# Formatting Rules ####
# New line preferences
csharp_new_line_before_catch = true
csharp_new_line_before_else = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = false
csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = false
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
[*.{cs,vb}]
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
dotnet_style_prefer_auto_properties = true:silent
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = lf

View File

@@ -0,0 +1,149 @@
name: 错误报告
description: 在使用 Lagrange 的过程中遇到了错误
title: '[Bug?]: '
labels: [ "bug?" ]
body:
# User's README and agreement
- type: markdown
attributes:
value: |
## 感谢您愿意填写错误回报!
## 以下是一些注意事项,请务必阅读让我们能够更容易处理
### ❗| 确定没有相同问题的ISSUE已被提出.
### 🌎| 请准确填写环境信息
### ❔| 修改配置文件中log参数为trace或debug并提供出现问题前后的完整日志内容。**请自行删除日志内存在的个人信息及敏感内容。**
### ⚠ | 如果您有能力请使用VS提供更加详细的信息.
## 如果您不知道如何有效、精准地表述,我们建议您先阅读[《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)
---
- type: checkboxes
id: terms
attributes:
label: 请确保您已阅读以上注意事项,并勾选下方的确认框。
options:
- label: "我已经仔细阅读上述内容"
required: true
- label: "我已经使用 [最新构建](https://github.com/LagrangeDev/Lagrange.Core/actions/) 测试过,问题依旧存在。"
required: true
- label: "我已经在 [Issue Tracker](https://github.com/LagrangeDev/Lagrange.Core/issues) 中找过我要提出的问题没有找到相同问题的ISSUE。"
required: true
- label: 我已知晓并同意,此处仅用于汇报程序中存在的问题。若这个 Issue 是关于其他非程序本身问题,则我的 Issue 可能会被无条件自动关闭或/并锁定。其它疑问请考虑加入TG群询问或在discussions中提问
required: true
# User's data
- type: markdown
attributes:
value: |
## 环境信息
请根据实际使用环境修改以下信息。
# Env | Lagrange Project
- type: dropdown
id: lagrange-project
attributes:
label: Lagrange项目
description: 请选择具体使用/依赖的Lagrange项目
options:
- Audio
- Core
- OneBot
- 其它(请在下方说明)
validations:
required: true
# Env | Lagrange Commit
- type: input
id: lagrange-commit
attributes:
label: 所使用/依赖的Lagrange项目对应的commit
validations:
required: true
# Env | VM Version
- type: dropdown
id: env-vm-ver
attributes:
label: 运行环境
description: 选择运行 Lagrange 的系统版本
options:
- Windows
- MacOS
- Linux
- 其它(请在下方说明)
validations:
required: true
# Env | VM Arch
- type: dropdown
id: env-vm-arch
attributes:
label: 运行架构
description: 请选择运行 Lagrange 的系统架构
options:
- x64
- x86
- arm
- arm64
- 其它
validations:
required: true
# Env | Connection type
- type: dropdown
id: env-conn-type
attributes:
label: 连接方式
description: 如果有必要请选择对接机器人的连接方式例如使用Lagrange.OneBot
options:
- HTTP
- 正向 WebSocket
- 反向 WebSocket
# Input | Reproduce
- type: textarea
id: reproduce-steps
attributes:
label: 重现步骤
description: |
我们需要执行哪些操作才能让 bug 出现?
简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在。
validations:
required: true
# Input | Expected result
- type: textarea
id: expected
attributes:
label: 期望的结果是什么?
validations:
required: true
# Input | Actual result
- type: textarea
id: actual
attributes:
label: 实际的结果是什么?
validations:
required: true
# Optional | Reproduce code
- type: textarea
id: reproduce-code
attributes:
label: 简单的复现代码/链接(可选)
render: C#
# Optional | Logging
- type: textarea
id: logging
attributes:
label: Trace 级别日志记录(可选)
render: Shell
# Optional | Extra description
- type: textarea
id: extra-desc
attributes:
label: 补充说明(可选)

View File

@@ -0,0 +1,37 @@
name: 新功能提议
description: 希望拥有新的功能
title: '[Feature Request]: '
labels: ["enhancement"]
body:
# User's README
- type: markdown
attributes:
value: |
### 为了项目的长久稳定,请不要要求提供一些敏感操作的新功能
### 项目人手有限如果您有能力请考虑提交PR
### 当 Owner 或 Member 认为该 Feature Request 不合适,可能直接关闭 ISSUE
# Env | Lagrange Project
- type: dropdown
id: lagrange-project
attributes:
label: Lagrange项目
description: 请选择您希望在哪个具体 Lagrange 项目中添加新需求
options:
- Audio
- Core
- OneBot
- 其它(请在下方说明)
validations:
required: true
# Input | Feature Content
- type: textarea
id: feature-content
attributes:
label: 新需求内容
description: |
请尽可能详细描述您的需求,如果可以,请提供代码样例或参考项目
validations:
required: true

View File

@@ -0,0 +1,39 @@
name: Lagrange.Core NuGet Push
on:
push:
branches:
- master
paths:
- "Lagrange.Core/**"
- "Lagrange.Core.sln"
- "LICENSE"
- "README.md"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Pack
run: |
dotnet build -c Release Lagrange.Core
dotnet pack -c Release Lagrange.Core
dotnet pack -c Release -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg Lagrange.Core
- name: Add private GitHub registry to NuGet
run: dotnet nuget add source --username Linwenxuan05 --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/LagrangeDev/index.json"
- name: Push generated package to GitHub registry
run: dotnet nuget push ./Lagrange.Core/bin/Release/*.nupkg --source "github" --skip-duplicate --api-key ${{ secrets.GIT_TOKEN }}
- name: Push generated package to NuGet
run: dotnet nuget push ./Lagrange.Core/bin/Release/*.nupkg --source https://api.nuget.org/v3/index.json --skip-duplicate --api-key ${{ secrets.NUGETAPIKEY }}

View File

@@ -0,0 +1,72 @@
name: Lagrange.OneBot Build
on:
push:
branches:
- master
paths:
- "Lagrange.Core/**"
- "Lagrange.OneBot/**"
- "Lagrange.Core.sln"
- "LICENSE"
- "!Lagrange.OneBot/Resources/Dockerfile"
- "!Lagrange.OneBot/Resources/docker-entrypoint.sh"
pull_request:
branches:
- master
paths:
- "Lagrange.Core/**"
- "Lagrange.OneBot/**"
- "Lagrange.Core.sln"
- "LICENSE"
- "!Lagrange.OneBot/Resources/Dockerfile"
- "!Lagrange.OneBot/Resources/docker-entrypoint.sh"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
runtimeIdentifier:
[
win-x64,
win-x86,
linux-x64,
linux-arm,
linux-arm64,
osx-x64,
osx-arm64,
linux-musl-x64,
linux-musl-arm,
linux-musl-arm64,
]
steps:
- uses: actions/checkout@v4
- name: Install .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Build Lagrange.OneBot .NET 8.0
run: dotnet publish Lagrange.OneBot/Lagrange.OneBot.csproj --no-self-contained -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=none -p:RuntimeIdentifier=${{ matrix.runtimeIdentifier }} --framework net8.0
- name: Build Lagrange.OneBot .NET 9.0
run: dotnet publish Lagrange.OneBot/Lagrange.OneBot.csproj --no-self-contained -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=none -p:RuntimeIdentifier=${{ matrix.runtimeIdentifier }} --framework net9.0
- name: Upload binary files(${{ matrix.runtimeIdentifier }}) for .NET 8.0
uses: actions/upload-artifact@v4
if: github.event_name != 'pull_request'
with:
name: Lagrange.OneBot_${{ matrix.runtimeIdentifier }}_net8.0_NoSelfContained
path: Lagrange.OneBot/bin/Release/net8.0/${{ matrix.runtimeIdentifier }}/publish
- name: Upload binary files(${{ matrix.runtimeIdentifier }}) for .NET 9.0
uses: actions/upload-artifact@v4
if: github.event_name != 'pull_request'
with:
name: Lagrange.OneBot_${{ matrix.runtimeIdentifier }}_net9.0_NoSelfContained
path: Lagrange.OneBot/bin/Release/net9.0/${{ matrix.runtimeIdentifier }}/publish

View File

@@ -0,0 +1,62 @@
name: Lagrange.OneBot Docker Push
on:
push:
branches:
- master
paths:
- "Lagrange.Core/**"
- "Lagrange.OneBot/**"
- "Lagrange.Core.sln"
- "LICENSE"
pull_request:
branches:
- master
paths:
- "Lagrange.Core/**"
- "Lagrange.OneBot/**"
- "Lagrange.Core.sln"
- "LICENSE"
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Github Registry
uses: docker/login-action@v3
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GIT_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository_owner }}/lagrange.onebot
tags: |
type=edge
type=sha,event=branch
type=ref,event=tag
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: "Lagrange.OneBot/Resources/Dockerfile"
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64, linux/arm64, linux/arm

View File

@@ -0,0 +1,76 @@
name: Lagrange.OneBot Release
on:
workflow_dispatch:
env:
GH_TOKEN: ${{ github.token }}
jobs:
delete-tag:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- run: gh release delete nightly --cleanup-tag -y -R ${{ github.repository }}
create-tag:
needs:
- delete-tag
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- run: echo -e "> ⚠This is a nightly release.\r\n> ⚠This is not the latest version." > note
- run: gh release create nightly -F ./note -p -t "Nightly Release" -R ${{ github.repository }}
build-and-upload-release:
if: ${{ always() }}
needs:
- create-tag
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
matrix:
runtimeIdentifier:
[
win-x64,
win-x86,
linux-x64,
linux-arm,
linux-arm64,
osx-x64,
osx-arm64,
linux-musl-x64,
linux-musl-arm,
linux-musl-arm64,
]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- run: dotnet publish Lagrange.OneBot/Lagrange.OneBot.csproj --self-contained -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:DebugType=none -p:RuntimeIdentifier=${{ matrix.runtimeIdentifier }} --framework net9.0
- run: |
if [[ ${{ matrix.runtimeIdentifier }} == 'win-x64' || ${{ matrix.runtimeIdentifier }} == 'win-x86' ]]; then
zip -r Lagrange.OneBot_${{ matrix.runtimeIdentifier }}_net9.0_SelfContained.zip ./Lagrange.OneBot/bin/Release/net9.0/${{ matrix.runtimeIdentifier }}/publish
gh release upload nightly Lagrange.OneBot_${{ matrix.runtimeIdentifier }}_net9.0_SelfContained.zip -R ${{ github.repository }}
else
tar -czvf Lagrange.OneBot_${{ matrix.runtimeIdentifier }}_net9.0_SelfContained.tar.gz ./Lagrange.OneBot/bin/Release/net9.0/${{ matrix.runtimeIdentifier }}/publish
gh release upload nightly Lagrange.OneBot_${{ matrix.runtimeIdentifier }}_net9.0_SelfContained.tar.gz -R ${{ github.repository }}
fi

271
Lagrange.Core/.gitignore vendored Normal file
View File

@@ -0,0 +1,271 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# backups
local-backup/
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Nuget packages directory
packages/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# vscode files
.vscode/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
#*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
*.DotSettings.user
riderModule.iml
/_ReSharper.Caches/
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
#macos
.DS_Store
#Lagrange.Core
Lagrange.Core/Utility/Crypto/Provider/Dandelion/*.cs
docs/node_modules
docs/.vitepress/dist
docs/.vitepress/cache
docs/api

51
Lagrange.Core/Docker.md Normal file
View File

@@ -0,0 +1,51 @@
<div align="center">
# Lagrange.Core - Docker guide
An Implementation of NTQQ Protocol, with Pure C#, Derived from Konata.Core
[![Core](https://img.shields.io/badge/Lagrange-Core-blue)](#)
[![OneBot](https://img.shields.io/badge/Lagrange-OneBot-blue)](#)
[![C#](https://img.shields.io/badge/Core-%20.NET_6-blue)](#)
[![C#](https://img.shields.io/badge/OneBot-%20.NET_7-blue)](#)
[![License](https://img.shields.io/static/v1?label=LICENSE&message=GPL-3.0&color=lightrey)](#)
[![Telegram](https://img.shields.io/endpoint?url=https%3A%2F%2Ftelegram-badge-4mbpu8e0fit4.runkit.sh%2F%3Furl%3Dhttps%3A%2F%2Ft.me%2F%2B6HNTeJO0JqtlNmRl)](https://t.me/+6HNTeJO0JqtlNmRl)
**&gt; English &lt;** | [简体中文](Docker_zh.md)
</div>
## Getting Started
```bash
# 8081 port for ForwardWebSocket and Http-Post
# /path-to-data is used to store the files needed for the runtime
# UID Env and GID Env are used to set file permissions
docker run -td -p 8081:8081 -v /path-to-data:/app/data -e UID=$UID -e GID=$(id -g) ghcr.io/lagrangedev/lagrange.onebot:edge
```
> [!IMPORTANT]
>
> 1. During the first run, you may be prompted with Please Edit the appsettings.json to set configs and press any key to continue. Please choose one of the following solutions to proceed:
>
> 1. 1. Modify `/path-to-data/appsettings.json`
> 2. Restart the container using `docker restart`
>
> 2. 1. Ensure that the `-t` option is used when executing `docker run`
> 2. Modify `/path-to-data/appsettings.json`
> 3. Enter the container using `docker attach`
> 4. Press any key
> 5. Exit the container using `Ctrl + P` `Ctrl + Q`
>
> 2. If the host needs to access the Implementation (e.g., `Http`, `ForwardWebSocket`), please configure the Host of Implementation as `*`
> 3. If the implementation needs to access the host network (e.g., `HttpPost`, `ReverseWebSocket`), please configure the `Host` of implementation to be `host.docker.internal`.
## Migration from older versions
Move `appsettings.json`, `device.json`, `keystore.json`, `lagrange-*.db` to the same folder where you want to put them.
For example `/path-to-data`
Delete the `ConfigPath` configuration entry in `/path-to-data/appsettings.json`
Start the container according to [Getting Started](#getting-started)

View File

@@ -0,0 +1,51 @@
<div align="center">
# Lagrange.Core - Docker 使用指南
一个基于纯 C# 的 NTQQ 协议实现,源自 Konata.Core
[![Core](https://img.shields.io/badge/Lagrange-Core-blue)](#)
[![OneBot](https://img.shields.io/badge/Lagrange-OneBot-blue)](#)
[![C#](https://img.shields.io/badge/Core-%20.NET_6-blue)](#)
[![C#](https://img.shields.io/badge/OneBot-%20.NET_7-blue)](#)
[![License](https://img.shields.io/static/v1?label=LICENSE&message=GPL-3.0&color=lightrey)](#)
[![Telegram](https://img.shields.io/endpoint?url=https%3A%2F%2Ftelegram-badge-4mbpu8e0fit4.runkit.sh%2F%3Furl%3Dhttps%3A%2F%2Ft.me%2F%2B6HNTeJO0JqtlNmRl)](https://t.me/+6HNTeJO0JqtlNmRl)
[English](Docker.md) | **&gt; 简体中文 &lt;**
</div>
## 开始使用
```bash
# 8081 端口用于正向 WebSocket 和 Http-post
# /path-to-data 被用于存储程序运行时产生的文件
# UID Env 和 GID Env 用于设置文件权限
docker run -td -p 8081:8081 -v /path-to-data:/app/data -e UID=$UID -e GID=$(id -g) ghcr.io/lagrangedev/lagrange.onebot:edge
```
> [!IMPORTANT]
>
> - 首次运行时可能会提示 `Please Edit the appsettings.json to set configs and press any key to continue`,请选择以下一种方案执行:
>
> - 1. 修改 `/path-to-data/appsettings.json`
> 2. 使用 `docker restart` 重新启动容器
>
> - 1. 确保在执行 `docker run` 时使用了 `-t` 选项
> 2. 修改 `/path-to-data/appsettings.json`
> 3. 使用 `docker attach` 进入容器
> 4. 按任意键
> 5. 使用 `Ctrl + P` `Ctrl + Q` 退出容器
>
> - 如果需要宿主需要访问实现(例如:`Http``ForwardWebSocket`),请将实现的 `Host` 配置为 `*`
> - 如果实现需要访问宿主网络(例如:`HttpPost``ReverseWebSocket`),请将实现的 `Host` 配置为 `host.docker.internal`
## 从旧版本迁移
`appsettings.json``device.json``keystore.json``lagrange-*.db` 移动到您想要放置它们的相同文件夹中。
例如 `/path-to-data`
删除 `/path-to-data/appsettings.json` 中的 `ConfigPath` 配置
按照 [# 开始使用](#开始使用) 启动容器

674
Lagrange.Core/LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Lagrange.Core\Lagrange.Core.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,13 @@
using Lagrange.Core.Test.Tests;
namespace Lagrange.Core.Test;
internal static class Program
{
public static async Task Main(string[] args)
{
// BenchmarkRunner.Run<ProtoBufTest>(new DebugBuildConfig());
await new NTLoginTest().LoginByPassword();
// await new WtLoginTest().FetchQrCode();
}
}

View File

@@ -0,0 +1,24 @@
using Lagrange.Core.Internal.Packets.Login.Ecdh;
using Lagrange.Core.Utility.Extension;
using ProtoBuf;
namespace Lagrange.Core.Test;
public class Protobuf
{
public void Test()
{
var test = new SsoKeyExchange()
{
PubKey = new byte[] { 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11 },
GcmCalc2 = new byte[] { 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11 },
GcmCalc1 = new byte[] { 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11 },
Timestamp = 23456789,
Type = 1
};
using var stream = new MemoryStream();
Serializer.Serialize(stream, test);
Console.WriteLine(stream.ToArray().Hex(false, true));
}
}

View File

@@ -0,0 +1,32 @@
using Lagrange.Core.Utility.Binary;
using Lagrange.Core.Utility.Extension;
namespace Lagrange.Core.Test.Tests;
public class BinaryTest
{
public class Binary
{
[BinaryProperty] public uint Value { get; set; } = 114514;
[BinaryProperty] public ulong Value2 { get; set; } = 1919810;
}
public void Test()
{
var binary = new Binary();
var bytes = BinarySerializer.Serialize(binary);
Console.WriteLine(bytes.ToArray().Hex());
Console.WriteLine(bytes.Length);
var binary2 = new BinaryPacket();
binary2.WriteUint(114514);
binary2.WriteUlong(1919810);
Console.WriteLine(binary2.ToArray().Hex());
var newPacket = new BinaryPacket(bytes.ToArray());
var binary3 = newPacket.Deserialize<Binary>();
Console.WriteLine(binary3.Value);
Console.WriteLine(binary3.Value2);
}
}

View File

@@ -0,0 +1,82 @@
using Lagrange.Core.Utility.Crypto.Provider.Ecdh;
namespace Lagrange.Core.Test.Tests;
public class EcdhTest
{
public static void Test()
{
{
var uncompressedPublicKey = new byte[]
{
0x04,
0xEB, 0xCA, 0x94, 0xD7, 0x33, 0xE3, 0x99, 0xB2,
0xDB, 0x96, 0xEA, 0xCD, 0xD3, 0xF6, 0x9A, 0x8B,
0xB0, 0xF7, 0x42, 0x24, 0xE2, 0xB4, 0x4E, 0x33,
0x57, 0x81, 0x22, 0x11, 0xD2, 0xE6, 0x2E, 0xFB,
0xC9, 0x1B, 0xB5, 0x53, 0x09, 0x8E, 0x25, 0xE3,
0x3A, 0x79, 0x9A, 0xDC, 0x7F, 0x76, 0xFE, 0xB2,
0x08, 0xDA, 0x7C, 0x65, 0x22, 0xCD, 0xB0, 0x71,
0x9A, 0x30, 0x51, 0x80, 0xCC, 0x54, 0xA8, 0x2E
};
var compressedPublicKey = new byte[]
{
0x02,
0xEB, 0xCA, 0x94, 0xD7, 0x33, 0xE3, 0x99, 0xB2,
0xDB, 0x96, 0xEA, 0xCD, 0xD3, 0xF6, 0x9A, 0x8B,
0xB0, 0xF7, 0x42, 0x24, 0xE2, 0xB4, 0x4E, 0x33,
0x57, 0x81, 0x22, 0x11, 0xD2, 0xE6, 0x2E, 0xFB
};
EcdhProvider ecdhProvider = new EcdhProvider(EllipticCurve.Prime256V1);
var unpacked1 = ecdhProvider.UnpackPublic(uncompressedPublicKey);
var unpacked2 = ecdhProvider.UnpackPublic(compressedPublicKey);
if (unpacked1.X == unpacked2.X && unpacked1.Y == unpacked2.Y)
{
Console.WriteLine("UnpackPublic() works correctly");
}
else
{
Console.WriteLine("UnpackPublic() does not work correctly");
}
}
{
// from https://github.com/KonataDev/Konata.Core/blob/develop/Konata.Core.Test/UtilTest/EcdhTest.cs
var uncompressedPublicKey = new byte[]
{
0x04,
0xD5, 0xCF, 0xB0, 0x2D, 0x5D, 0x4F, 0xCA, 0x2C,
0x84, 0xF6, 0xF1, 0x29, 0x4B, 0x45, 0x5B, 0xAB,
0x4C, 0x96, 0x98, 0xDD, 0x57, 0x2B, 0xF8, 0x63,
0x82, 0xA9, 0xDA, 0xF8, 0xAD, 0xE9, 0xD4, 0x5A,
0x57, 0xDE, 0x14, 0x04, 0xFA, 0x5D, 0x41, 0x29,
0x1E, 0x0A, 0x56, 0xCB, 0x45, 0x08, 0xD3, 0x2F
};
var compressedPublicKey = new byte[]
{
0x03,
0xD5, 0xCF, 0xB0, 0x2D, 0x5D, 0x4F, 0xCA, 0x2C,
0x84, 0xF6, 0xF1, 0x29, 0x4B, 0x45, 0x5B, 0xAB,
0x4C, 0x96, 0x98, 0xDD, 0x57, 0x2B, 0xF8, 0x63
};
EcdhProvider ecdhProvider = new EcdhProvider(EllipticCurve.Secp192K1);
var unpacked1 = ecdhProvider.UnpackPublic(uncompressedPublicKey);
var unpacked2 = ecdhProvider.UnpackPublic(compressedPublicKey);
if (unpacked1.X == unpacked2.X && unpacked1.Y == unpacked2.Y)
{
Console.WriteLine("UnpackPublic() works correctly");
}
else
{
Console.WriteLine("UnpackPublic() does not work correctly");
}
}
}
}

View File

@@ -0,0 +1,65 @@
using Lagrange.Core.Common;
using Lagrange.Core.Common.Interface;
using Lagrange.Core.Common.Interface.Api;
using Lagrange.Core.Message;
namespace Lagrange.Core.Test.Tests;
// ReSharper disable once InconsistentNaming
public class NTLoginTest
{
public async Task LoginByPassword()
{
var deviceInfo = WtLoginTest.GetDeviceInfo();
var keyStore = WtLoginTest.LoadKeystore();
if (keyStore == null)
{
Console.WriteLine("Please login by QrCode first");
return;
}
var bot = BotFactory.Create(new BotConfig()
{
UseIPv6Network = false,
GetOptimumServer = true,
AutoReconnect = true,
Protocol = Protocols.Linux
}, deviceInfo, keyStore);
bot.Invoker.OnBotLogEvent += (_, @event) =>
{
Utility.Console.ChangeColorByTitle(@event.Level);
Console.WriteLine(@event.ToString());
};
bot.Invoker.OnBotOnlineEvent += (_, @event) =>
{
Console.WriteLine(@event.ToString());
WtLoginTest.SaveKeystore(bot.UpdateKeystore());
};
bot.Invoker.OnBotCaptchaEvent += (_, @event) =>
{
Console.WriteLine(@event.ToString());
var captcha = Console.ReadLine();
var randStr = Console.ReadLine();
if (captcha != null && randStr != null) bot.SubmitCaptcha(captcha, randStr);
};
bot.Invoker.OnGroupInvitationReceived += (_, @event) =>
{
Console.WriteLine(@event.ToString());
};
await bot.LoginByPassword();
var friendChain = MessageBuilder.Group(411240674)
.Text("This is the friend message sent by Lagrange.Core")
.Mention(1925648680);
await bot.SendMessage(friendChain.Build());
await Task.Delay(1000);
}
}

View File

@@ -0,0 +1,79 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Lagrange.Core.Common;
using Lagrange.Core.Common.Interface;
using Lagrange.Core.Common.Interface.Api;
namespace Lagrange.Core.Test.Tests;
public class WtLoginTest
{
public async Task FetchQrCode()
{
var deviceInfo = GetDeviceInfo();
var keyStore = LoadKeystore() ?? new BotKeystore();
var bot = BotFactory.Create(new BotConfig
{
UseIPv6Network = false,
GetOptimumServer = true,
AutoReconnect = true,
Protocol = Protocols.Linux
}, deviceInfo, keyStore);
bot.Invoker.OnBotLogEvent += (context, @event) =>
{
Utility.Console.ChangeColorByTitle(@event.Level);
Console.WriteLine(@event.ToString());
};
bot.Invoker.OnBotOnlineEvent += (context, @event) =>
{
Console.WriteLine(@event.ToString());
SaveKeystore(bot.UpdateKeystore());
};
var qrCode = await bot.FetchQrCode();
if (qrCode != null)
{
await File.WriteAllBytesAsync("qr.png", qrCode.Value.QrCode);
await bot.LoginByQrCode();
}
}
public static BotDeviceInfo GetDeviceInfo()
{
if (File.Exists("Test/DeviceInfo.json"))
{
var info = JsonSerializer.Deserialize<BotDeviceInfo>(File.ReadAllText("Test/DeviceInfo.json"));
if (info != null) return info;
info = BotDeviceInfo.GenerateInfo();
File.WriteAllText("Test/DeviceInfo.json", JsonSerializer.Serialize(info));
return info;
}
var deviceInfo = BotDeviceInfo.GenerateInfo();
File.WriteAllText("Test/DeviceInfo.json", JsonSerializer.Serialize(deviceInfo));
return deviceInfo;
}
public static void SaveKeystore(BotKeystore keystore) =>
File.WriteAllText("Test/Keystore.json", JsonSerializer.Serialize(keystore));
public static BotKeystore? LoadKeystore()
{
try
{
var text = File.ReadAllText("Test/Keystore.json");
return JsonSerializer.Deserialize<BotKeystore>(text, new JsonSerializerOptions()
{
ReferenceHandler = ReferenceHandler.Preserve
});
}
catch
{
return null;
}
}
}

View File

@@ -0,0 +1,16 @@
using Lagrange.Core.Event.EventArg;
namespace Lagrange.Core.Test.Utility;
public static class Console
{
public static void ChangeColorByTitle(this LogLevel level) => System.Console.ForegroundColor = level switch
{
LogLevel.Debug => ConsoleColor.White,
LogLevel.Verbose => ConsoleColor.DarkGray,
LogLevel.Information => ConsoleColor.Blue,
LogLevel.Warning => ConsoleColor.Yellow,
LogLevel.Fatal => ConsoleColor.Red,
_ => System.Console.ForegroundColor
};
}

View File

@@ -0,0 +1,61 @@
using System.Reflection;
using System.Text;
using ProtoBuf;
namespace Lagrange.Core.Test.Utility;
internal static class ProtoGen
{
public static void GenerateProtoFiles()
{
var assembly = typeof(Lagrange.Core.Utility.ServiceInjector).Assembly;
var types = assembly.GetTypes();
var sb = new StringBuilder();
sb.AppendLine("syntax = \"proto3\";");
sb.AppendLine();
sb.AppendLine("package Lagrange.Core;");
foreach (var type in types)
{
if (type.Namespace?.StartsWith("Lagrange.Core.Internal.Packets") != true) continue;
sb.AppendLine($"message {type.Name} {{");
var properties = type.GetProperties();
foreach (var property in properties)
{
string typeString = ParseType(property.PropertyType);
sb.AppendLine($" {GetLastToken(typeString, '.')} {property.Name} = {property.GetCustomAttribute<ProtoMemberAttribute>()?.Tag};");
}
sb.AppendLine("}");
sb.AppendLine();
}
string proto = sb.ToString();
File.WriteAllText("packets.proto", proto);
}
private static string ParseType(Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
return $"repeated {ParseType(type.GetGenericArguments()[0])}";
}
return type.ToString() switch
{
"System.UInt64" => "varint",
"System.UInt32" => "varint",
"System.UInt16" => "varint",
"System.Int64" => "varint",
"System.Int32" => "varint",
"System.String" => "string",
"System.Boolean" => "bool",
"System.Byte[]" => "bytes",
_ => type.ToString()
};
}
private static string GetLastToken(string str, char separator) => str.Split(separator)[^1];
}

View File

@@ -0,0 +1,35 @@
using System.Text;
using Lagrange.Core.Utility.Extension;
using static Lagrange.Core.Utility.Binary.BitConverter;
namespace Lagrange.Core.Test.Utility;
public static class Tlv
{
public static Dictionary<string, string> GetTlvDictionary(byte[] tlvs, bool isCommand = true)
{
var result = new Dictionary<string, string>();
using var reader = new BinaryReader(new MemoryStream(tlvs));
ushort command;
if (isCommand)
{
command = ToUInt16(reader.ReadBytes(2), false);
}
ushort tlvCount = ToUInt16(reader.ReadBytes(2), false);
for (int i = 0; i < tlvCount; i++)
{
ushort tlvTag = ToUInt16(reader.ReadBytes(2), false);
ushort tlvLength = ToUInt16(reader.ReadBytes(2), false);
byte[] tlvValue = reader.ReadBytes(tlvLength);
result.Add($"0x{tlvTag:X} {tlvLength}", tlvValue.Hex());
result.Add($"0x{tlvTag:X} UTF8 {tlvLength}", Encoding.UTF8.GetString(tlvValue));
}
return result;
}
}

View File

@@ -0,0 +1,43 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.6.33829.357
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lagrange.Core", "Lagrange.Core\Lagrange.Core.csproj", "{909C99CC-0CB7-4A34-8C75-AD25E6AEA535}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lagrange.Core.Test", "Lagrange.Core.Test\Lagrange.Core.Test.csproj", "{D64B6BAB-CD20-4660-8A6E-BCC936652204}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lagrange.OneBot", "Lagrange.OneBot\Lagrange.OneBot.csproj", "{37AEDD3B-9B9F-4782-ADD5-BA2436FB2507}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "SolutionItem", "{ACE96E15-65D1-471B-913A-A1014F0D003E}"
ProjectSection(SolutionItems) = preProject
README.md = README.md
README_zh.md = README_zh.md
Docker.md = Docker.md
Docker_zh.md = Docker_zh.md
LICENSE = LICENSE
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{909C99CC-0CB7-4A34-8C75-AD25E6AEA535}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{909C99CC-0CB7-4A34-8C75-AD25E6AEA535}.Debug|Any CPU.Build.0 = Debug|Any CPU
{909C99CC-0CB7-4A34-8C75-AD25E6AEA535}.Release|Any CPU.ActiveCfg = Release|Any CPU
{909C99CC-0CB7-4A34-8C75-AD25E6AEA535}.Release|Any CPU.Build.0 = Release|Any CPU
{D64B6BAB-CD20-4660-8A6E-BCC936652204}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D64B6BAB-CD20-4660-8A6E-BCC936652204}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D64B6BAB-CD20-4660-8A6E-BCC936652204}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D64B6BAB-CD20-4660-8A6E-BCC936652204}.Release|Any CPU.Build.0 = Release|Any CPU
{37AEDD3B-9B9F-4782-ADD5-BA2436FB2507}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{37AEDD3B-9B9F-4782-ADD5-BA2436FB2507}.Debug|Any CPU.Build.0 = Debug|Any CPU
{37AEDD3B-9B9F-4782-ADD5-BA2436FB2507}.Release|Any CPU.ActiveCfg = Release|Any CPU
{37AEDD3B-9B9F-4782-ADD5-BA2436FB2507}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Lagrange.Core.Test")] // Allow unit test to access internal members
[assembly: InternalsVisibleTo("Lagrange.OneBot")] // OneBot Implementation

View File

@@ -0,0 +1,45 @@
using Lagrange.Core.Common;
using Lagrange.Core.Event;
using Lagrange.Core.Internal.Context;
namespace Lagrange.Core;
public class BotContext : IDisposable
{
public readonly EventInvoker Invoker;
public uint BotUin => ContextCollection.Keystore.Uin;
public string? BotName => ContextCollection.Keystore.Info?.Name;
internal readonly Utility.TaskScheduler Scheduler;
internal readonly ContextCollection ContextCollection;
public BotAppInfo AppInfo { get; }
public BotConfig Config { get; }
private readonly BotDeviceInfo _deviceInfo;
private readonly BotKeystore _keystore;
internal BotContext(BotConfig config, BotDeviceInfo deviceInfo, BotKeystore keystore, BotAppInfo appInfo)
{
Invoker = new EventInvoker(this);
Scheduler = new Utility.TaskScheduler();
Config = config;
AppInfo = appInfo;
_deviceInfo = deviceInfo;
_keystore = keystore;
ContextCollection = new ContextCollection(_keystore, AppInfo, _deviceInfo, Config, Invoker, Scheduler);
}
public void Dispose()
{
ContextCollection.Dispose();
GC.SuppressFinalize(this);
}
}

View File

@@ -0,0 +1,109 @@
#pragma warning disable CS8618
namespace Lagrange.Core.Common;
public class BotAppInfo
{
public string Os { get; set; }
public string VendorOs { get; set; }
public string Kernel { get; set; }
public string CurrentVersion { get; set; }
public int MiscBitmap { get; set; }
public string PtVersion { get; set; }
public int SsoVersion { get; set; }
public string PackageName { get; set; }
public string WtLoginSdk { get; set; }
public int AppId { get; set; }
/// <summary>Or known as pubId in tencent log</summary>
public int SubAppId { get; set; }
public int AppIdQrCode { get; set; }
public ushort AppClientVersion { get; set; }
public uint MainSigMap { get; set; }
public ushort SubSigMap { get; set; }
public ushort NTLoginType { get; set; }
private static readonly BotAppInfo Linux = new()
{
Os = "Linux",
Kernel = "Linux",
VendorOs = "linux",
CurrentVersion = "3.2.15-30366",
MiscBitmap = 32764,
PtVersion = "2.0.0",
SsoVersion = 19,
PackageName = "com.tencent.qq",
WtLoginSdk = "nt.wtlogin.0.0.1",
AppId = 1600001615,
SubAppId = 537258424,
AppIdQrCode = 13697054,
AppClientVersion = 30366,
MainSigMap = 169742560,
SubSigMap = 0,
NTLoginType = 1
};
private static readonly BotAppInfo MacOs = new()
{
Os = "Mac",
Kernel = "Darwin",
VendorOs = "mac",
CurrentVersion = "6.9.23-20139",
PtVersion = "2.0.0",
MiscBitmap = 32764,
SsoVersion = 23,
PackageName = "com.tencent.qq",
WtLoginSdk = "nt.wtlogin.0.0.1",
AppId = 1600001602,
SubAppId = 537200848,
AppIdQrCode = 537200848,
AppClientVersion = 13172,
MainSigMap = 169742560,
SubSigMap = 0,
NTLoginType = 5
};
private static readonly BotAppInfo Windows = new()
{
Os = "Windows",
Kernel = "Windows_NT",
VendorOs = "win32",
CurrentVersion = "9.9.2-15962",
PtVersion = "2.0.0",
MiscBitmap = 32764,
SsoVersion = 23,
PackageName = "com.tencent.qq",
WtLoginSdk = "nt.wtlogin.0.0.1",
AppId = 1600001604,
SubAppId = 537138217,
AppIdQrCode = 537138217,
AppClientVersion = 13172,
MainSigMap = 169742560,
SubSigMap = 0,
NTLoginType = 5
};
public static readonly Dictionary<Protocols, BotAppInfo> ProtocolToAppInfo = new()
{
{ Protocols.Windows, Windows },
{ Protocols.Linux, Linux },
{ Protocols.MacOs, MacOs },
};
}

View File

@@ -0,0 +1,60 @@
using Lagrange.Core.Utility.Sign;
namespace Lagrange.Core.Common;
/// <summary>
/// Configuration for The bot client
/// </summary>
[Serializable]
public class BotConfig
{
/// <summary>
/// The protocol for the client, default is Linux
/// </summary>
public Protocols Protocol { get; set; } = Protocols.Linux;
/// <summary>
/// Auto reconnect to server when disconnected
/// </summary>
public bool AutoReconnect { get; set; } = true;
/// <summary>
/// Use the IPv6 to connect to server, only if your network support IPv6
/// </summary>
public bool UseIPv6Network { get; set; } = false;
/// <summary>
/// Get optimum server from Tencent MSF server, set to false to use hardcode server
/// </summary>
public bool GetOptimumServer { get; set; } = true;
/// <summary>
/// Custom Sign Provider
/// </summary>
public SignProvider? CustomSignProvider { get; set; } = null;
/// <summary>
/// The maximum size of the highway block in byte, max 1MB (1024 * 1024 byte)
/// </summary>
public uint HighwayChunkSize { get; set; } = 1024 * 1024;
/// <summary>
/// Highway Uploading Concurrency, if the image failed to send, set this to 1
/// </summary>
public uint HighwayConcurrent { get; set; } = 4;
/// <summary>
/// Refresh the session when the session is about to expired
/// </summary>
public bool AutoReLogin { get; set; } = true;
}
/// <summary>
/// The Protocol for the client
/// </summary>
public enum Protocols
{
Windows = 0,
MacOs = 1,
Linux = 2
}

View File

@@ -0,0 +1,28 @@
using Lagrange.Core.Utility.Generator;
#pragma warning disable CS8618
namespace Lagrange.Core.Common;
[Serializable]
public class BotDeviceInfo
{
public Guid Guid { get; set; }
public byte[] MacAddress { get; set; }
public string DeviceName { get; set; }
public string SystemKernel { get; set; }
public string KernelVersion { get; set; }
public static BotDeviceInfo GenerateInfo() => new()
{
Guid = Guid.NewGuid(),
MacAddress = ByteGen.GenRandomBytes(6),
DeviceName = $"Lagrange-{StringGen.GenerateHex(6).ToUpper()}",
SystemKernel = "Windows 10.0.19042",
KernelVersion = "10.0.19042.0"
};
}

View File

@@ -0,0 +1,131 @@
using System.Text;
using System.Text.Json.Serialization;
using Lagrange.Core.Utility.Crypto;
using Lagrange.Core.Utility.Extension;
using Lagrange.Core.Utility.Generator;
namespace Lagrange.Core.Common;
public class BotKeystore
{
[JsonConstructor]
public BotKeystore()
{
PasswordMd5 = "";
SecpImpl = new EcdhImpl(EcdhImpl.CryptMethod.Secp192K1);
PrimeImpl = new EcdhImpl(EcdhImpl.CryptMethod.Prime256V1, false);
TeaImpl = new TeaImpl();
Stub = new KeyCollection();
var tempPwd = Session?.TempPassword;
Session = tempPwd != null ? new WtLoginSession { TempPassword = tempPwd } : new WtLoginSession();
}
/// <summary>
/// Create the Bot keystore
/// </summary>
/// <param name="uin">Set this field 0 to use QrCode Login</param>
/// <param name="password">Password Raw</param>
internal BotKeystore(uint uin, string password)
{
Uin = uin;
PasswordMd5 = Encoding.UTF8.GetBytes(password).Md5();
SecpImpl = new EcdhImpl(EcdhImpl.CryptMethod.Secp192K1);
PrimeImpl = new EcdhImpl(EcdhImpl.CryptMethod.Prime256V1, false);
TeaImpl = new TeaImpl();
Session = new WtLoginSession();
Stub = new KeyCollection();
}
public uint Uin { get; set; }
public string? Uid { get; set; }
public string PasswordMd5 { get; set; }
internal EcdhImpl SecpImpl { get; set; }
internal EcdhImpl PrimeImpl { get; set; }
internal TeaImpl TeaImpl { get; set; }
internal KeyCollection Stub { get; }
public WtLoginSession Session { get; set; }
public BotInfo? Info { get; set; }
[Serializable]
public class KeyCollection
{
public byte[] RandomKey { get; set; } = ByteGen.GenRandomBytes(16);
public byte[] TgtgtKey { get; set; } = new byte[16];
}
[Serializable]
public class WtLoginSession
{
public byte[] D2Key { get; set; } = new byte[16];
public byte[] D2 { get; set; } = Array.Empty<byte>();
public byte[] Tgt { get; set; } = Array.Empty<byte>();
public DateTime SessionDate { get; set; }
internal byte[]? QrSign { get; set; } // size: 24
internal string? QrString { get; set; }
internal string? QrUrl { get; set; }
internal byte[]? ExchangeKey { get; set; }
internal byte[]? KeySign { get; set; }
internal byte[]? UnusualSign { get; set; }
internal string? UnusualCookies { get; set; }
internal string? CaptchaUrl { get; set; }
internal string? NewDeviceVerifyUrl { get; set; }
internal (string, string, string)? Captcha { get; set; }
public byte[]? TempPassword { get; set; }
internal byte[]? NoPicSig { get; set; } // size: 16, may be from Tlv19, for Tlv16A
private ushort _sequence;
internal ushort Sequence
{
get => _sequence++;
set => _sequence = value;
}
}
[Serializable]
public class BotInfo
{
internal BotInfo(byte age, byte gender, string name)
{
Age = age;
Gender = gender;
Name = name;
}
[JsonConstructor]
public BotInfo()
{
Name = "";
}
public byte Age { get; set; }
public byte Gender { get; set; }
public string Name { get; set; }
public override string ToString() => $"Bot name: {Name} | Gender: {Gender} | Age: {Age}";
}
internal void ClearSession()
{
Session.D2 = Array.Empty<byte>();
Session.Tgt = Array.Empty<byte>();
Session.D2Key = new byte[16];
}
}

View File

@@ -0,0 +1,86 @@
// ReSharper disable UnusedType.Global
// ReSharper disable UnusedMember.Global
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable UnusedAutoPropertyAccessor.Local
namespace Lagrange.Core.Common;
/// <summary>
/// Task Scheduler
/// </summary>
public class Scheduler
{
public const int Infinity = int.MaxValue;
private BotContext Bot { get; }
private Utility.TaskScheduler Instance { get; }
public string Name { get; }
public Action<BotContext> Action { get; }
internal Scheduler(BotContext bot, string name, Action<BotContext> action)
{
Bot = bot;
Name = name;
Action = action;
Instance = bot.Scheduler;
}
~Scheduler() => Cancel();
/// <summary>
/// Create a task scheduler
/// </summary>
/// <param name="bot"><b>[In]</b> Bot instance</param>
/// <param name="name"><b>[In]</b> Task identity name</param>
/// <param name="action"><b>[In]</b> Task callback action</param>
/// <returns></returns>
public static Scheduler Create(BotContext bot, string name, Action<BotContext> action) => new(bot, name, action);
/// <summary>
/// Execute the task with a specific interval
/// </summary>
/// <param name="interval"><b>[In]</b> Interval in milliseconds</param>
/// <param name="times"><b>[In]</b> Execute times</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
public void Interval(int interval, int times) => Instance.Interval(Name, interval, times, () => Action(Bot));
/// <summary>
/// Execute the task with a specific interval infinity
/// </summary>
/// <param name="interval"><b>[In]</b> Interval in milliseconds</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
public void Interval(int interval) => Instance.Interval(Name, interval, Infinity, () => Action(Bot));
/// <summary>
/// Execute the task once
/// </summary>
/// <param name="delay"><b>[In]</b> Delay time in milliseconds</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
public void RunOnce(int delay) => Instance.RunOnce(Name, delay, () => Action(Bot));
/// <summary>
/// Execute the task once
/// </summary>
/// <param name="date"><b>[In]</b> Execute date</param>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="ObjectDisposedException"></exception>
public void RunOnce(DateTime date) => Instance.RunOnce(Name, date, () => Action(Bot));
/// <summary>
/// Trigger a task to run
/// </summary>
public void Trigger() => Instance.Trigger(Name);
/// <summary>
/// Cancel the task
/// </summary>
/// <exception cref="ObjectDisposedException"></exception>
public void Cancel() => Instance.Cancel(Name);
}

View File

@@ -0,0 +1,32 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class AiCharacter
{
public string VoiceId { get; set; }
public string CharacterName { get; set; }
public string CharacterVoiceUrl { get; set; }
public AiCharacter(string voiceId, string characterName, string characterVoiceUrl)
{
VoiceId = voiceId;
CharacterName = characterName;
CharacterVoiceUrl = characterVoiceUrl;
}
}
[Serializable]
public class AiCharacterList
{
public string Type { get; set; }
public List<AiCharacter> Characters { get; set; }
public AiCharacterList(string type, List<AiCharacter> characters)
{
Type = type;
Characters = characters;
}
}

View File

@@ -0,0 +1,25 @@
namespace Lagrange.Core.Common.Entity
{
public class BusinessCustom
{
public uint Type;
public uint Level;
public string? Icon;
public uint IsYear;
public uint IsPro;
}
public class BusinessCustomList
{
public List<BusinessCustom> BusinessLists { get; set; }
public BusinessCustomList(List<BusinessCustom> businessLists)
{
BusinessLists = businessLists;
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Lagrange.Core.Common.Entity;
public class BotStatus
{
public uint StatusId { get; set; }
public uint? FaceId { get; set; }
public string? Msg { get; set; }
}

View File

@@ -0,0 +1,37 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotFileEntry : IBotFSEntry
{
public string FileId { get; }
public string FileName { get; }
public string ParentDirectory { get; }
public ulong FileSize { get; }
public DateTime ExpireTime { get; }
public DateTime ModifiedTime { get; }
public uint UploaderUin { get; }
public DateTime UploadedTime { get; }
public uint DownloadedTimes { get; }
internal BotFileEntry(string fileId, string fileName, string parentDirectory, ulong fileSize,
DateTime expireTime, DateTime modifiedTime, uint uploaderUin, DateTime uploadedTime, uint downloadedTimes)
{
FileId = fileId;
FileName = fileName;
ParentDirectory = parentDirectory;
FileSize = fileSize;
ExpireTime = expireTime;
ModifiedTime = modifiedTime;
UploaderUin = uploaderUin;
UploadedTime = uploadedTime;
DownloadedTimes = downloadedTimes;
}
}

View File

@@ -0,0 +1,31 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotFolderEntry : IBotFSEntry
{
public string FolderId { get; set; }
public string ParentFolderId { get; set; }
public string FolderName { get; set; }
public DateTime CreateTime { get; set; }
public DateTime ModifiedTime { get; set; }
public uint CreatorUin { get; set; }
public uint TotalFileCount { get; set; }
internal BotFolderEntry(string folderId, string parentFolderId, string folderName, DateTime createTime,
DateTime modifiedTime, uint creatorUin, uint totalFileCount)
{
FolderId = folderId;
ParentFolderId = parentFolderId;
FolderName = folderName;
CreateTime = createTime;
ModifiedTime = modifiedTime;
CreatorUin = creatorUin;
TotalFileCount = totalFileCount;
}
}

View File

@@ -0,0 +1,47 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotFriend
{
/// <summary>
/// The empty constructor for serialization
/// </summary>
internal BotFriend()
{
Uid = string.Empty;
Nickname = string.Empty;
Remarks = string.Empty;
PersonalSign = string.Empty;
Qid = string.Empty;
Group = default;
Avatar = string.Empty;
}
internal BotFriend(uint uin, string uid, string nickname, string remarks, string personalSign, string qid, BotFriendGroup group = default)
{
Uin = uin;
Uid = uid;
Nickname = nickname;
Remarks = remarks;
PersonalSign = personalSign;
Qid = qid;
Group = group;
Avatar = $"https://q1.qlogo.cn/g?b=qq&nk={uin}&s=640";
}
public uint Uin { get; set; }
internal string Uid { get; set; }
public string Nickname { get; set; }
public string Remarks { get; set; }
public string PersonalSign { get; set; }
public string Qid { get; set; }
public BotFriendGroup Group { get; set; }
public string Avatar { get; set; }
}

View File

@@ -0,0 +1,15 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public struct BotFriendGroup
{
public uint GroupId { get; }
public string GroupName { get; }
internal BotFriendGroup(uint groupId, string groupName)
{
GroupId = groupId;
GroupName = groupName;
}
}

View File

@@ -0,0 +1,39 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotFriendRequest
{
public BotFriendRequest(string targetUid, string sourceUid, uint eventState, string comment, string source, uint time)
{
TargetUid = targetUid;
SourceUid = sourceUid;
EventState = (State)eventState;
Comment = comment;
Source = source;
Time = DateTime.UnixEpoch.AddSeconds(time);
}
public string TargetUid { get; set; }
public uint TargetUin { get; set; }
public string SourceUid { get; set; }
public uint SourceUin { get; set; }
public State EventState { get; set; }
public string Comment { get; set; }
public string Source { get; set; }
public DateTime Time { get; set; }
public enum State
{
Pending = 1,
Disapproved = 2,
Approved = 3,
}
}

View File

@@ -0,0 +1,24 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotGetGroupTodoResult
{
public int Retcode { get; }
public string? ResultMessage { get; }
public uint GroupUin { get; }
public uint Sequence { get; }
public string Preview { get; }
public BotGetGroupTodoResult(int retcode, string? resultMessage, uint groupUin, uint sequence, string preview)
{
Retcode = retcode;
ResultMessage = resultMessage;
GroupUin = groupUin;
Sequence = sequence;
Preview = preview;
}
}

View File

@@ -0,0 +1,35 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotGroup
{
internal BotGroup(uint groupUin, string groupName, uint memberCount, uint maxMember, uint createTime, string? description, string? question, string? announcement)
{
GroupUin = groupUin;
GroupName = groupName;
MemberCount = memberCount;
MaxMember = maxMember;
CreateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(createTime);
Description = description;
Question = question;
Announcement = announcement;
}
public uint GroupUin { get; }
public string GroupName { get; }
public uint MemberCount { get; }
public uint MaxMember { get; }
public DateTime CreateTime { get; }
public string? Description { get; }
public string? Question { get; }
public string? Announcement { get; }
public string Avatar => $"https://p.qlogo.cn/gh/{GroupUin}/{GroupUin}/0/";
}

View File

@@ -0,0 +1,52 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotGroupClockInResult
{
public BotGroupClockInResult() { }
public BotGroupClockInResult(bool isSuccess)
{
IsSuccess = isSuccess;
}
/// <summary>
/// Is the clock in successful
/// </summary>
public bool IsSuccess { get; set; } = false;
/// <summary>
/// Maybe "今日已成功打卡"
/// </summary>
public string Title { get; set; } = string.Empty;
/// <summary>
/// Maybe "已打卡N天"
/// </summary>
public string KeepDayText { get; set; } = string.Empty;
/// <summary>
/// Maybe "群内排名第N位"
/// </summary>
public string GroupRankText { get; set; } = string.Empty;
/// <summary>
/// The utc time of clock in
/// </summary>
public DateTime ClockInUtcTime { get; set; } = DateTime.UnixEpoch; // 打卡时间
/// <summary>
/// Detail info url
/// </summary>
public string DetailUrl { get; set; } = string.Empty; // https://qun.qq.com/v2/signin/detail?...
public static BotGroupClockInResult Fail() => new BotGroupClockInResult()
{
IsSuccess = false
};
public static BotGroupClockInResult Success() => new BotGroupClockInResult()
{
IsSuccess = true
};
}

View File

@@ -0,0 +1,30 @@
namespace Lagrange.Core.Common.Entity;
public class BotGroupInfo
{
public string OwnerUid { get; set; } = string.Empty;
public ulong CreateTime { get; set; }
public ulong MaxMemberCount { get; set; }
public ulong MemberCount { get; set; }
public ulong Level { get; set; }
public string Name { get; set; } = string.Empty;
public string NoticePreview { get; set; } = string.Empty;
public ulong Uin { get; set; }
public ulong LastSequence { get; set; }
public ulong LastMessageTime { get; set; }
public string Question { get; set; } = string.Empty;
public string Answer { get; set; } = string.Empty;
public ulong MaxAdminCount { get; set; }
}

View File

@@ -0,0 +1,60 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotGroupMember
{
/// <summary>
/// The empty constructor for serialization
/// </summary>
internal BotGroupMember()
{
Uid = string.Empty;
MemberCard = string.Empty;
MemberName = string.Empty;
SpecialTitle = string.Empty;
}
internal BotGroupMember(uint uin, string uid, GroupMemberPermission permission, uint groupLevel, string? memberCard,
string memberName, string? specialTitle, DateTime joinTime, DateTime lastMsgTime,DateTime shutUpTimestamp)
{
Uin = uin;
Uid = uid;
Permission = permission;
GroupLevel = groupLevel;
MemberCard = memberCard;
MemberName = memberName;
SpecialTitle = specialTitle;
JoinTime = joinTime;
LastMsgTime = lastMsgTime;
ShutUpTimestamp = shutUpTimestamp;
}
public uint Uin { get; set; }
internal string Uid { get; set; }
public GroupMemberPermission Permission { get; set; }
public uint GroupLevel { get; set; }
public string? MemberCard { get; set; }
public string MemberName { get; set; }
public string? SpecialTitle { get; set; }
public DateTime JoinTime { get; set; }
public DateTime LastMsgTime { get; set; }
public DateTime ShutUpTimestamp { get; set; }
public string Avatar => $"https://q1.qlogo.cn/g?b=qq&nk={Uin}&s=640";
}
public enum GroupMemberPermission : uint
{
Member = 0,
Owner = 1,
Admin = 2,
}

View File

@@ -0,0 +1,20 @@
namespace Lagrange.Core.Common.Entity;
public class BotGroupReaction
{
public string FaceId { get; set; }
public uint Type { get; set; }
public uint Count { get; set; }
public bool IsAdded { get; set; }
public BotGroupReaction(string faceId, uint type, uint count, bool isAdded)
{
FaceId = faceId;
Type = type;
Count = count;
IsAdded = isAdded;
}
}

View File

@@ -0,0 +1,75 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotGroupRequest
{
internal BotGroupRequest(
uint groupUin,
uint? invitorMemberUin,
string? invitorMemberCard,
uint targetMemberUin,
string targetMemberCard,
uint? operatorUin,
string? operatorName,
uint state,
ulong sequence,
uint type,
string? comment,
bool isFiltered)
{
GroupUin = groupUin;
InvitorMemberUin = invitorMemberUin;
InvitorMemberCard = invitorMemberCard;
TargetMemberUin = targetMemberUin;
TargetMemberCard = targetMemberCard;
OperatorUin = operatorUin;
OperatorName = operatorName;
EventState = (State)state;
Sequence = sequence;
EventType = (Type)type;
Comment = comment;
IsFiltered = isFiltered;
}
public uint GroupUin { get; set; }
public uint? InvitorMemberUin { get; set; }
public string? InvitorMemberCard { get; set; }
public uint TargetMemberUin { get; set; }
public string TargetMemberCard { get; set; }
public uint? OperatorUin { get; set; }
public string? OperatorName { get; set; }
public Type EventType { get; set; }
public State EventState { get; set; }
internal ulong Sequence { get; set; } // for internal use of Approving Requests
public string? Comment { get; }
public bool IsFiltered { get; set; }
public enum State
{
Default = 0,
Pending = 1,
Approved = 2,
Disapproved = 3,
}
public enum Type
{
GroupRequest = 1,
SelfInvitation = 2,
KickMember = 6,
KickSelf =7,
ExitGroup = 13,
GroupInvitation = 22,
}
}

View File

@@ -0,0 +1,63 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class BotUserInfo
{
internal BotUserInfo(uint uin, string nickname, string avatar, DateTime birthday, string city, string country, string school, uint age, DateTime registerTime, GenderInfo gender, string? qid, uint level, string sign, BotStatus status, List<BusinessCustom> business)
{
Uin = uin;
Avatar = avatar;
// Avatar = $"https://q1.qlogo.cn/g?b=qq&nk={Uin}&s=640";
Nickname = nickname;
Birthday = birthday;
City = city;
Country = country;
School = school;
Age = age;
RegisterTime = registerTime;
Gender = gender;
Qid = qid;
Level = level;
Sign = sign;
Status = status;
Business = business;
}
public uint Uin { get; set; }
public string Avatar { get; set; }
public string Nickname { get; set; }
public DateTime Birthday { get; set; }
public string City { get; set; }
public string Country { get; set; }
public string School { get; set; }
public uint Age { get; set; }
public DateTime RegisterTime { get; set; }
public GenderInfo Gender { get; set; }
public string? Qid { get; set; }
public uint Level { get; set; }
public string Sign { get; set; }
public BotStatus Status { get; set; }
public List<BusinessCustom> Business { get; set; }
public enum GenderInfo
{
Unset = 0,
Male = 1,
Female = 2,
Unknown = 255
}
}

View File

@@ -0,0 +1,6 @@
namespace Lagrange.Core.Common.Entity;
/// <summary>
/// Indicate that it is an element in BotFileSystem
/// </summary>
public interface IBotFSEntry { }

View File

@@ -0,0 +1,49 @@
using System.Text.Json.Serialization;
namespace Lagrange.Core.Common.Entity
{
[Serializable]
public class ImageOcrResult
{
[JsonPropertyName("texts")] public List<TextDetection> Texts { get; set; }
[JsonPropertyName("language")] public string Language { get; set; }
public ImageOcrResult(List<TextDetection> texts, string language)
{
Texts = texts;
Language = language;
}
}
[Serializable]
public class TextDetection
{
[JsonPropertyName("text")] public string Text { get; set; }
[JsonPropertyName("confidence")] public int Confidence { get; set; }
[JsonPropertyName("coordinates")] public List<Coordinate> Coordinates { get; set; }
public TextDetection(string text, int confidence, List<Coordinate> coordinates)
{
Text = text;
Confidence = confidence;
Coordinates = coordinates;
}
}
[Serializable]
public class Coordinate
{
[JsonPropertyName("x")] public int X { get; set; }
[JsonPropertyName("y")] public int Y { get; set; }
public Coordinate(int x, int y)
{
X = x;
Y = y;
}
}
}

View File

@@ -0,0 +1,94 @@
namespace Lagrange.Core.Common.Entity;
public enum PokeFaceType
{
Poke = 1,
Heart = 2,
Like = 3,
HeartBroken = 4,
SixSixSix = 5,
Kamehameha = 6
}
public enum SpecialPokeFaceType
{
Like = 1,
Hear = 2,
Haha = 3,
Pig = 4,
Bomb = 5,
Poop = 6,
Mua = 7,
Pill = 8,
Durian = 9,
Lololo = 10,
Pan = 11,
Cash = 12,
}
public static class PokeFaceTypeExt
{
private static readonly string[] nameMapping =
{
string.Empty, "戳一戳", "比心", "点赞", "心碎", "666", "放大招"
};
private static readonly string[] specialNameMapping =
{
string.Empty, "点赞", "爱心", "哈哈", "猪头", "炸弹", "便便", "亲亲", "药丸", "榴莲", "略略略", "平底锅", "钞票"
};
/// <summary>
/// Return the name of the shake face type.
/// A <see cref="ArgumentOutOfRangeException"/> will be thrown if the type is not valid.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the type is not valid.</exception>
public static string ToName(this PokeFaceType type)
{
int v = (int)type;
if (v <= 0 || v >= nameMapping.Length)
throw new ArgumentOutOfRangeException(nameof(type), type, null);
return nameMapping[v];
}
/// <summary>
/// Tries to get the name of the shake face type.
/// Returns null if the type is not valid.
/// </summary>
/// <param name="type">The shake face type.</param>
/// <returns>The name of the shake face type, or null if the type is not valid.</returns>
public static string? TryGetName(this PokeFaceType type)
{
int v = (int)type;
if (v <= 0 || v >= nameMapping.Length)
return null;
return nameMapping[v];
}
/// <summary>
/// Return the name of the shake face type.
/// A <see cref="ArgumentOutOfRangeException"/> will be thrown if the type is not valid.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Thrown when the type is not valid.</exception>
public static string ToName(this SpecialPokeFaceType type)
{
int v = (int)type;
if (v <= 0 || v >= specialNameMapping.Length)
throw new ArgumentOutOfRangeException(nameof(type), type, null);
return specialNameMapping[v];
}
/// <summary>
/// Tries to get the name of the shake face type.
/// Returns null if the type is not valid.
/// </summary>
/// <param name="type">The shake face type.</param>
/// <returns>The name of the shake face type, or null if the type is not valid.</returns>
public static string? TryGetName(this SpecialPokeFaceType type)
{
int v = (int)type;
if (v <= 0 || v >= specialNameMapping.Length)
return null;
return specialNameMapping[v];
}
}

View File

@@ -0,0 +1,67 @@
namespace Lagrange.Core.Common.Entity;
[Serializable]
public class SysFaceEntry
{
public string QSid { get; set; }
public string? QDes { get; set; }
public string? EMCode { get; set; }
public int? QCid { get; set; }
public int? AniStickerType { get; set; }
public int? AniStickerPackId { get; set; }
public int? AniStickerId { get; set; }
public string? Url { get; set; }
public string[]? EmojiNameAlias { get; set; }
public int? AniStickerWidth { get; set; }
public int? AniStickerHeight { get; set; }
public SysFaceEntry(string qSid, string? qDes, string? emCode, int? qCid, int? aniStickerType,
int? aniStickerPackId, int? aniStickerId, string? url, string[]? emojiNameAlias, int? aniStickerWidth,
int? aniStickerHeight)
{
QSid = qSid;
QDes = qDes;
EMCode = emCode;
QCid = qCid;
AniStickerType = aniStickerType;
AniStickerPackId = aniStickerPackId;
AniStickerId = aniStickerId;
Url = url;
EmojiNameAlias = emojiNameAlias;
AniStickerWidth = aniStickerWidth;
AniStickerHeight = aniStickerHeight;
}
}
[Serializable]
public class SysFacePackEntry
{
public string EmojiPackName { get; set; }
public SysFaceEntry[] Emojis { get; set; }
public SysFacePackEntry(string emojiPackName, SysFaceEntry[] emojis)
{
EmojiPackName = emojiPackName;
Emojis = emojis;
}
public uint[] GetUniqueSuperQSids((int AniStickerType, int AniStickerPackId)[] excludeAniStickerTypesAndPackIds)
=> Emojis
.Where(e => e.AniStickerType is not null
&& e.AniStickerPackId is not null
&& !excludeAniStickerTypesAndPackIds.Contains((e.AniStickerType.Value, e.AniStickerPackId.Value)))
.Select(e => uint.Parse(e.QSid))
.Distinct()
.ToArray();
}

View File

@@ -0,0 +1,55 @@
using Lagrange.Core.Event;
namespace Lagrange.Core.Common.Interface.Api;
public static class BotExt
{
/// <summary>
/// Fetch the qrcode for QRCode Login
/// </summary>
/// <returns>return url and qrcode image in PNG format</returns>
public static Task<(string Url, byte[] QrCode)?> FetchQrCode(this BotContext bot)
=> bot.ContextCollection.Business.WtExchangeLogic.FetchQrCode();
/// <summary>
/// Use this method to login by QrCode, you should call <see cref="FetchQrCode"/> first
/// </summary>
public static Task LoginByQrCode(this BotContext bot, CancellationToken cancellationToken = default)
=> bot.ContextCollection.Business.WtExchangeLogic.LoginByQrCode(cancellationToken);
/// <summary>
/// Use this method to login by password, EasyLogin may be preformed if there is sig in <see cref="BotKeystore"/>
/// </summary>
public static Task<bool> LoginByPassword(this BotContext bot, CancellationToken cancellationToken = default)
=> bot.ContextCollection.Business.WtExchangeLogic.LoginByEasy(true, cancellationToken);
/// <summary>
/// Use this method to login by easy, no fallback to password login/>
/// </summary>
public static Task<bool> LoginByEasy(this BotContext bot, CancellationToken cancellationToken = default)
=> bot.ContextCollection.Business.WtExchangeLogic.LoginByEasy(false, cancellationToken);
/// <summary>
/// Submit the captcha of the url given by the <see cref="EventInvoker.OnBotCaptchaEvent"/>
/// </summary>
/// <returns>Whether the captcha is submitted successfully</returns>
public static bool SubmitCaptcha(this BotContext bot, string ticket, string randStr)
=> bot.ContextCollection.Business.WtExchangeLogic.SubmitCaptcha(ticket, randStr);
public static Task<bool> SetNeedToConfirmSwitch(this BotContext bot, bool needToConfirm)
=> bot.ContextCollection.Business.OperationLogic.SetNeedToConfirmSwitch(needToConfirm);
/// <summary>
/// Use this method to update keystore, so EasyLogin may be preformed next time by using this keystore
/// </summary>
/// <returns>BotKeystore instance</returns>
public static BotKeystore UpdateKeystore(this BotContext bot)
=> bot.ContextCollection.Keystore;
/// <summary>
/// Use this method to update device info
/// </summary>
/// <param name="bot"></param>
public static BotDeviceInfo UpdateDeviceInfo(this BotContext bot)
=> bot.ContextCollection.Device;
}

View File

@@ -0,0 +1,151 @@
using Lagrange.Core.Common.Entity;
using Lagrange.Core.Event.EventArg;
using Lagrange.Core.Message;
using Lagrange.Core.Message.Entity;
namespace Lagrange.Core.Common.Interface.Api;
public static class GroupExt
{
/// <summary>
/// Mute the member in the group, Bot must be admin
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="groupUin">The uin for target group</param>
/// <param name="targetUin">The uin for target member in such group</param>
/// <param name="duration">The duration in seconds, 0 for unmute member</param>
/// <returns>Successfully muted or not</returns>
public static Task<bool> MuteGroupMember(this BotContext bot, uint groupUin, uint targetUin, uint duration)
=> bot.ContextCollection.Business.OperationLogic.MuteGroupMember(groupUin, targetUin, duration);
/// <summary>
/// Mute the group
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="groupUin">The uin for target group</param>
/// <param name="isMute">true for mute and false for unmute</param>
/// <returns>Successfully muted or not</returns>
public static Task<bool> MuteGroupGlobal(this BotContext bot, uint groupUin, bool isMute)
=> bot.ContextCollection.Business.OperationLogic.MuteGroupGlobal(groupUin, isMute);
/// <summary>
///
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="groupUin">The uin for target group</param>
/// <param name="targetUin">The uin for target member in such group</param>
/// <param name="rejectAddRequest">whether the kicked member can request</param>
/// <returns>Successfully kicked or not</returns>
public static Task<bool> KickGroupMember(this BotContext bot, uint groupUin, uint targetUin, bool rejectAddRequest)
=> bot.ContextCollection.Business.OperationLogic.KickGroupMember(groupUin, targetUin, rejectAddRequest, "");
public static Task<bool> KickGroupMember(this BotContext bot, uint groupUin, uint targetUin, bool rejectAddRequest, string reason)
=> bot.ContextCollection.Business.OperationLogic.KickGroupMember(groupUin, targetUin, rejectAddRequest, reason);
public static Task<bool> SetGroupAdmin(this BotContext bot, uint groupUin, uint targetUin, bool isAdmin)
=> bot.ContextCollection.Business.OperationLogic.SetGroupAdmin(groupUin, targetUin, isAdmin);
// 300204 Check group manager:Not an administrator
public static Task<(int, string?)> SetGroupTodo(this BotContext bot, uint groupUin, uint sequence)
=> bot.ContextCollection.Business.OperationLogic.SetGroupTodo(groupUin, sequence);
public static Task<(int, string?)> RemoveGroupTodo(this BotContext bot, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.RemoveGroupTodo(groupUin);
public static Task<(int, string?)> FinishGroupTodo(this BotContext bot, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.FinishGroupTodo(groupUin);
public static Task<BotGetGroupTodoResult> GetGroupTodo(this BotContext bot, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.GetGroupTodo(groupUin);
public static Task<bool> SetGroupBot(this BotContext bot, uint targetUin, uint On, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.SetGroupBot(targetUin, On, groupUin);
[Obsolete("Cosider using SetGroupBotHD(BotContext, uint, uint, string?, string?) instead")]
public static Task<bool> SetGroupBotHD(this BotContext bot, uint targetUin, uint groupUin)
=> bot.SetGroupBotHD(targetUin, groupUin, null, null);
public static Task<bool> SetGroupBotHD(this BotContext bot, uint targetUin, uint groupUin, string? data_1, string? data_2)
=> bot.ContextCollection.Business.OperationLogic.SetGroupBotHD(targetUin, groupUin, data_1, data_2);
public static Task<bool> RenameGroupMember(this BotContext bot, uint groupUin, uint targetUin, string targetName)
=> bot.ContextCollection.Business.OperationLogic.RenameGroupMember(groupUin, targetUin, targetName);
public static Task<bool> RenameGroup(this BotContext bot, uint groupUin, string targetName)
=> bot.ContextCollection.Business.OperationLogic.RenameGroup(groupUin, targetName);
public static Task<bool> RemarkGroup(this BotContext bot, uint groupUin, string targetRemark)
=> bot.ContextCollection.Business.OperationLogic.RemarkGroup(groupUin, targetRemark);
public static Task<bool> LeaveGroup(this BotContext bot, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.LeaveGroup(groupUin);
public static Task<bool> InviteGroup(this BotContext bot, uint groupUin, Dictionary<uint, uint?> invitedUins)
=> bot.ContextCollection.Business.OperationLogic.InviteGroup(groupUin, invitedUins);
public static Task<bool> SetGroupRequest(this BotContext bot, BotGroupRequest request, bool accept = true, string reason = "")
=> bot.ContextCollection.Business.OperationLogic.SetGroupRequest(request.GroupUin, request.Sequence, (uint)request.EventType, accept, reason);
public static Task<bool> SetGroupFilteredRequest(this BotContext bot, BotGroupRequest request, bool accept = true, string reason = "")
=> bot.ContextCollection.Business.OperationLogic.SetGroupFilteredRequest(request.GroupUin, request.Sequence, (uint)request.EventType, accept, reason);
public static Task<bool> SetFriendRequest(this BotContext bot, BotFriendRequest request, bool accept = true)
=> bot.ContextCollection.Business.OperationLogic.SetFriendRequest(request.SourceUid, accept);
public static Task<bool> GroupPoke(this BotContext bot, uint groupUin, uint friendUin)
=> bot.ContextCollection.Business.OperationLogic.GroupPoke(groupUin, friendUin);
public static Task<bool> SetEssenceMessage(this BotContext bot, MessageChain chain)
=> bot.ContextCollection.Business.OperationLogic.SetEssenceMessage(chain.GroupUin ?? 0, chain.Sequence, (uint)(chain.MessageId & 0xFFFFFFFF));
public static Task<bool> RemoveEssenceMessage(this BotContext bot, MessageChain chain)
=> bot.ContextCollection.Business.OperationLogic.RemoveEssenceMessage(chain.GroupUin ?? 0, chain.Sequence, (uint)(chain.MessageId & 0xFFFFFFFF));
public static Task<bool> GroupSetSpecialTitle(this BotContext bot, uint groupUin, uint targetUin, string title)
=> bot.ContextCollection.Business.OperationLogic.GroupSetSpecialTitle(groupUin, targetUin, title);
public static Task<bool> GroupSetMessageReaction(this BotContext bot, uint groupUin, uint sequence, string code)
=> bot.ContextCollection.Business.OperationLogic.SetMessageReaction(groupUin, sequence, code, true);
public static Task<bool> GroupSetMessageReaction(this BotContext bot, uint groupUin, uint sequence, string code, bool isSet)
=> bot.ContextCollection.Business.OperationLogic.SetMessageReaction(groupUin, sequence, code, isSet);
public static Task<bool> GroupSetAvatar(this BotContext bot, uint groupUin, ImageEntity imageEntity)
=> bot.ContextCollection.Business.OperationLogic.GroupSetAvatar(groupUin, imageEntity);
public static Task<(uint, uint)> GroupRemainAtAll(this BotContext bot, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.GroupRemainAtAll(groupUin);
#region Group File System
public static Task<ulong> FetchGroupFSSpace(this BotContext bot, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.FetchGroupFSSpace(groupUin);
public static Task<uint> FetchGroupFSCount(this BotContext bot, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.FetchGroupFSCount(groupUin);
public static Task<List<IBotFSEntry>> FetchGroupFSList(this BotContext bot, uint groupUin, string targetDirectory = "/")
=> bot.ContextCollection.Business.OperationLogic.FetchGroupFSList(groupUin, targetDirectory);
public static Task<string> FetchGroupFSDownload(this BotContext bot, uint groupUin, string fileId)
=> bot.ContextCollection.Business.OperationLogic.FetchGroupFSDownload(groupUin, fileId);
public static Task<(int RetCode, string RetMsg)> GroupFSMove(this BotContext bot, uint groupUin, string fileId, string parentDirectory, string targetDirectory)
=> bot.ContextCollection.Business.OperationLogic.GroupFSMove(groupUin, fileId, parentDirectory, targetDirectory);
public static Task<(int RetCode, string RetMsg)> GroupFSDelete(this BotContext bot, uint groupUin, string fileId)
=> bot.ContextCollection.Business.OperationLogic.GroupFSDelete(groupUin, fileId);
public static Task<(int RetCode, string RetMsg)> GroupFSCreateFolder(this BotContext bot, uint groupUin, string name)
=> bot.ContextCollection.Business.OperationLogic.GroupFSCreateFolder(groupUin, name);
public static Task<(int RetCode, string RetMsg)> GroupFSDeleteFolder(this BotContext bot, uint groupUin, string folderId)
=> bot.ContextCollection.Business.OperationLogic.GroupFSDeleteFolder(groupUin, folderId);
public static Task<(int RetCode, string RetMsg)> GroupFSRenameFolder(this BotContext bot, uint groupUin, string folderId, string newFolderName)
=> bot.ContextCollection.Business.OperationLogic.GroupFSRenameFolder(groupUin, folderId, newFolderName);
public static Task<bool> GroupFSUpload(this BotContext bot, uint groupUin, FileEntity fileEntity, string targetDirectory = "/")
=> bot.ContextCollection.Business.OperationLogic.GroupFSUpload(groupUin, fileEntity, targetDirectory);
#endregion
}

View File

@@ -0,0 +1,302 @@
using Lagrange.Core.Common.Entity;
using Lagrange.Core.Message;
using Lagrange.Core.Message.Entity;
namespace Lagrange.Core.Common.Interface.Api;
public static class OperationExt
{
/// <summary>
/// Fetch the friend list of account from server or cache
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="refreshCache">force the cache to be refreshed</param>
/// <returns></returns>
public static Task<List<BotFriend>> FetchFriends(this BotContext bot, bool refreshCache = false)
=> bot.ContextCollection.Business.OperationLogic.FetchFriends(refreshCache);
/// <summary>
/// Fetch the member list of the group from server or cache
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="groupUin"></param>
/// <param name="refreshCache">force the cache to be refreshed</param>
/// <returns></returns>
public static Task<List<BotGroupMember>> FetchMembers(this BotContext bot, uint groupUin, bool refreshCache = false)
=> bot.ContextCollection.Business.OperationLogic.FetchMembers(groupUin, refreshCache);
/// <summary>
/// Fetch the group list of the account from server or cache
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="refreshCache">force the cache to be refreshed</param>
/// <returns></returns>
public static Task<List<BotGroup>> FetchGroups(this BotContext bot, bool refreshCache = false)
=> bot.ContextCollection.Business.OperationLogic.FetchGroups(refreshCache);
/// <summary>
/// Fetch the cookies/pskey for accessing other site
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="domains">the domain for the cookie to be valid</param>
/// <returns>the list of cookies</returns>
public static Task<List<string>> FetchCookies(this BotContext bot, List<string> domains)
=> bot.ContextCollection.Business.OperationLogic.GetCookies(domains);
/// <summary>
/// Send the message
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="chain">the chain constructed by <see cref="MessageBuilder"/></param>
public static Task<MessageResult> SendMessage(this BotContext bot, MessageChain chain)
=> bot.ContextCollection.Business.OperationLogic.SendMessage(chain);
/// <summary>
/// Recall the group message from Bot itself by <see cref="MessageResult"/>
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="groupUin">The uin for target group of the message</param>
/// <param name="result">The return value for <see cref="SendMessage"/></param>
/// <returns>Successfully recalled or not</returns>
public static Task<bool> RecallGroupMessage(this BotContext bot, uint groupUin, MessageResult result)
=> bot.ContextCollection.Business.OperationLogic.RecallGroupMessage(groupUin, result);
/// <summary>
/// Recall the group message by <see cref="MessageChain"/>
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="chain">target MessageChain, must be Group</param>
/// <returns>Successfully recalled or not</returns>
public static Task<bool> RecallGroupMessage(this BotContext bot, MessageChain chain)
=> bot.ContextCollection.Business.OperationLogic.RecallGroupMessage(chain);
/// <summary>
/// Recall the group message by sequence
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="groupUin">The uin for target group of the message</param>
/// <param name="sequence">The sequence for target message</param>
/// <returns>Successfully recalled or not</returns>
public static Task<bool> RecallGroupMessage(this BotContext bot, uint groupUin, uint sequence)
=> bot.ContextCollection.Business.OperationLogic.RecallGroupMessage(groupUin, sequence);
/// <summary>
/// Recall the group message from Bot itself by <see cref="MessageResult"/>
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="friendUin">The uin for target friend of the message</param>
/// <param name="result">The return value for <see cref="SendMessage"/></param>
/// <returns>Successfully recalled or not</returns>
public static Task<bool> RecallFriendMessage(this BotContext bot, uint friendUin, MessageResult result)
=> bot.ContextCollection.Business.OperationLogic.RecallFriendMessage(friendUin, result);
/// <summary>
/// Recall the group message by <see cref="MessageChain"/>
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="chain">target MessageChain, must be Friend</param>
/// <returns>Successfully recalled or not</returns>
public static Task<bool> RecallFriendMessage(this BotContext bot, MessageChain chain)
=> bot.ContextCollection.Business.OperationLogic.RecallFriendMessage(chain);
/// <summary>
/// Fetch Notifications and requests such as friend requests and Group Join Requests
/// </summary>
/// <param name="bot">target BotContext</param>
/// <returns></returns>
public static Task<List<BotGroupRequest>?> FetchGroupRequests(this BotContext bot)
=> bot.ContextCollection.Business.OperationLogic.FetchGroupRequests();
/// <summary>
///
/// </summary>
/// <param name="bot"></param>
/// <returns></returns>
public static Task<List<BotFriendRequest>?> FetchFriendRequests(this BotContext bot)
=> bot.ContextCollection.Business.OperationLogic.FetchFriendRequests();
/// <summary>
/// set status
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="status">The status code</param>
/// <returns></returns>
public static Task<bool> SetStatus(this BotContext bot, uint status)
=> bot.ContextCollection.Business.OperationLogic.SetStatus(status);
/// <summary>
/// set custom status
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="faceId">faceId that is same as the <see cref="Lagrange.Core.Message.Entity.FaceEntity"/></param>
/// <param name="text">text that would shown</param>
/// <returns></returns>
public static Task<bool> SetCustomStatus(this BotContext bot, uint faceId, string text)
=> bot.ContextCollection.Business.OperationLogic.SetCustomStatus(faceId, text);
public static Task<bool> GroupTransfer(this BotContext bot, uint groupUin, uint targetUin)
=> bot.ContextCollection.Business.OperationLogic.GroupTransfer(groupUin, targetUin);
public static Task<bool> DeleteFriend(this BotContext bot, uint friendUin, bool block)
=> bot.ContextCollection.Business.OperationLogic.DeleteFriend(friendUin, block);
public static Task<bool> RequestFriend(this BotContext bot, uint targetUin, string question = "", string message = "")
=> bot.ContextCollection.Business.OperationLogic.RequestFriend(targetUin, question, message);
public static Task<bool> Like(this BotContext bot, uint targetUin, uint count = 1)
=> bot.ContextCollection.Business.OperationLogic.Like(targetUin, count);
/// <summary>
/// Get the client key for all sites
/// </summary>
public static Task<string?> GetClientKey(this BotContext bot)
=> bot.ContextCollection.Business.OperationLogic.GetClientKey();
/// <summary>
/// Get the history message record, max 30 seqs
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="groupUin">target GroupUin</param>
/// <param name="startSequence">Start Sequence of the message</param>
/// <param name="endSequence">End Sequence of the message</param>
public static Task<List<MessageChain>?> GetGroupMessage(this BotContext bot, uint groupUin, uint startSequence, uint endSequence)
=> bot.ContextCollection.Business.OperationLogic.GetGroupMessage(groupUin, startSequence, endSequence);
/// <summary>
/// Get the history message record for private message
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="friendUin">target FriendUin</param>
/// <param name="timestamp">timestamp of the message chain</param>
/// <param name="count">number of message to be fetched before timestamp</param>
public static Task<List<MessageChain>?> GetRoamMessage(this BotContext bot, uint friendUin, uint timestamp, uint count)
=> bot.ContextCollection.Business.OperationLogic.GetRoamMessage(friendUin, timestamp, count);
/// <summary>
/// Get the history message record for private message
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="targetChain">target chain</param>
/// <param name="count">number of message to be fetched before timestamp</param>
public static Task<List<MessageChain>?> GetRoamMessage(this BotContext bot, MessageChain targetChain, uint count)
{
uint timestamp = (uint)new DateTimeOffset(targetChain.Time).ToUnixTimeSeconds();
return bot.ContextCollection.Business.OperationLogic.GetRoamMessage(targetChain.FriendUin, timestamp, count);
}
public static Task<List<MessageChain>?> GetC2cMessage(this BotContext bot, uint friendUin, uint startSequence, uint endSequence)
{
return bot.ContextCollection.Business.OperationLogic.GetC2cMessage(friendUin, startSequence, endSequence);
}
public static Task<(int code, List<MessageChain>? chains)> GetMessagesByResId(this BotContext bot, string resId)
{
return bot.ContextCollection.Business.OperationLogic.GetMessagesByResId(resId);
}
/// <summary>
/// Do group clock in (群打卡)
/// </summary>
/// <param name="bot">target BotContext</param>
/// <param name="groupUin">target groupUin</param>
/// <returns></returns>
public static Task<BotGroupClockInResult> GroupClockIn(this BotContext bot, uint groupUin)
=> bot.ContextCollection.Business.OperationLogic.GroupClockIn(groupUin);
public static Task<BotUserInfo?> FetchUserInfo(this BotContext bot, uint uin, bool refreshCache = false)
=> bot.ContextCollection.Business.OperationLogic.FetchUserInfo(uin, refreshCache);
public static Task<(int code, string? message, BotGroupInfo info)> FetchGroupInfo(this BotContext bot, ulong uin)
=> bot.ContextCollection.Business.OperationLogic.FetchGroupInfo(uin);
public static Task<List<string>?> FetchCustomFace(this BotContext bot)
=> bot.ContextCollection.Business.OperationLogic.FetchCustomFace();
public static Task<string?> UploadLongMessage(this BotContext bot, List<MessageChain> chains)
=> bot.ContextCollection.Business.OperationLogic.UploadLongMessage(chains);
public static Task<bool> MarkAsRead(this BotContext bot, MessageChain targetChain)
{
uint timestamp = (uint)(targetChain.Time - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc)).TotalSeconds;
return bot.ContextCollection.Business.OperationLogic.MarkAsRead(targetChain.GroupUin ?? 0, targetChain.Uid,
targetChain.Sequence, timestamp);
}
public static Task<bool> UploadFriendFile(this BotContext bot, uint targetUin, FileEntity fileEntity)
=> bot.ContextCollection.Business.OperationLogic.UploadFriendFile(targetUin, fileEntity);
public static Task<bool> FriendPoke(this BotContext bot, uint friendUin)
=> bot.ContextCollection.Business.OperationLogic.FriendPoke(friendUin);
/// <summary>
/// Send a special window shake to friend
/// </summary>
/// <param name="friendUin">target friend uin</param>
/// <param name="type">face type</param>
/// <param name="count">count of face</param>
public static Task<MessageResult> FriendSpecialShake(this BotContext bot, uint friendUin, SpecialPokeFaceType type, uint count)
=> bot.ContextCollection.Business.OperationLogic.FriendSpecialShake(friendUin, type, count);
/// <summary>
/// Send a window shake to friend
/// </summary>
/// <param name="friendUin">target friend uin</param>
/// <param name="type">face type</param>
/// <param name="strength">How big the face will be displayed ([0,3] is valid)</param>
public static Task<MessageResult> FriendShake(this BotContext bot, uint friendUin, PokeFaceType type, ushort strength)
=> bot.ContextCollection.Business.OperationLogic.FriendShake(friendUin, type, strength);
public static Task<List<string>?> FetchMarketFaceKey(this BotContext bot, List<string> faceIds)
=> bot.ContextCollection.Business.OperationLogic.FetchMarketFaceKey(faceIds);
/// <summary>
/// Set the avatar of the bot itself
/// </summary>
/// <param name="bot">target <see cref="BotContext"/></param>
/// <param name="avatar">The avatar object, <see cref="ImageEntity"/></param>
public static Task<bool> SetAvatar(this BotContext bot, ImageEntity avatar)
=> bot.ContextCollection.Business.OperationLogic.SetAvatar(avatar);
public static Task<bool> FetchSuperFaceId(this BotContext bot, uint id)
=> bot.ContextCollection.Business.OperationLogic.FetchSuperFaceId(id);
public static Task<SysFaceEntry?> FetchFaceEntity(this BotContext bot, uint id)
=> bot.ContextCollection.Business.OperationLogic.FetchFaceEntity(id);
public static Task<bool> GroupJoinEmojiChain(this BotContext bot, uint groupUin, uint emojiId, uint targetMessageSeq)
=> bot.ContextCollection.Business.OperationLogic.GroupJoinEmojiChain(groupUin, emojiId, targetMessageSeq);
public static Task<bool> FriendJoinEmojiChain(this BotContext bot, uint friendUin, uint emojiId,
uint targetMessageSeq)
=> bot.ContextCollection.Business.OperationLogic.FriendJoinEmojiChain(friendUin, emojiId, targetMessageSeq);
public static Task<string> UploadImage(this BotContext bot, ImageEntity entity)
=> bot.ContextCollection.Business.OperationLogic.UploadImage(entity);
public static Task<ImageOcrResult?> OcrImage(this BotContext bot, string url)
=> bot.ContextCollection.Business.OperationLogic.ImageOcr(url);
public static Task<ImageOcrResult?> OcrImage(this BotContext bot, ImageEntity entity)
=> bot.ContextCollection.Business.OperationLogic.ImageOcr(entity);
public static Task<(int Code, string ErrMsg, string? Url)> GetGroupGenerateAiRecordUrl(this BotContext bot, uint groupUin, string character, string text, uint chatType)
=> bot.ContextCollection.Business.OperationLogic.GetGroupGenerateAiRecordUrl(groupUin, character, text, chatType);
public static Task<(int Code, string ErrMsg, RecordEntity? RecordEntity)> GetGroupGenerateAiRecord(this BotContext bot, uint groupUin, string character, string text, uint chatType)
=> bot.ContextCollection.Business.OperationLogic.GetGroupGenerateAiRecord(groupUin, character, text, chatType);
public static Task<(int Code, string ErrMsg, List<AiCharacterList>? Result)> GetAiCharacters(this BotContext bot, uint chatType, uint groupUin = 42)
=> bot.ContextCollection.Business.OperationLogic.GetAiCharacters(chatType, groupUin);
public static Task<(int Retcode, string Message, List<uint> FriendUins, List<uint> GroupUins)> GetPins(this BotContext bot)
=> bot.ContextCollection.Business.OperationLogic.GetPins();
public static Task<(int Retcode, string Message)> SetPinFriend(this BotContext bot, uint uin, bool isPin)
=> bot.ContextCollection.Business.OperationLogic.SetPinFriend(uin, isPin);
public static Task<(int Retcode, string Message)> SetPinGroup(this BotContext bot, uint uin, bool isPin)
=> bot.ContextCollection.Business.OperationLogic.SetPinGroup(uin, isPin);
public static Task<string> FetchPrivateFSDownload(this BotContext bot, string fileId, string fileHash, uint userId)
=> bot.ContextCollection.Business.OperationLogic.FetchPrivateFSDownload(fileId, fileHash, userId);
}

View File

@@ -0,0 +1,48 @@
namespace Lagrange.Core.Common.Interface;
public static class BotFactory
{
/// <summary>
/// Create new Bot from existing <see cref="BotKeystore"/> and <see cref="BotDeviceInfo"/>
/// </summary>
/// <param name="config">The config for Bot</param>
/// <param name="deviceInfo">Existing DeviceInfo from deserialization</param>
/// <param name="keystore">Existing Keystore from deserialization</param>
/// <returns>Created BotContext Instance</returns>
public static BotContext Create(BotConfig config, BotDeviceInfo deviceInfo, BotKeystore keystore) =>
new(config, deviceInfo, keystore, BotAppInfo.ProtocolToAppInfo[config.Protocol]);
/// <summary>
/// Create new Bot from existing <see cref="BotKeystore"/> and <see cref="BotDeviceInfo"/> with custom <see cref="BotAppInfo"/>
/// </summary>
/// <param name="config">The config for Bot</param>
/// <param name="deviceInfo">Existing DeviceInfo from deserialization</param>
/// <param name="keystore">Existing Keystore from deserialization</param>
/// <param name="appInfo">Custom BotAppInfo, including client version, app ID, etc</param>
/// <returns></returns>
public static BotContext Create(BotConfig config, BotDeviceInfo deviceInfo, BotKeystore keystore, BotAppInfo appInfo) =>
new(config, deviceInfo, keystore, appInfo);
/// <summary>
/// Create new Bot from Password and uin
/// </summary>
/// <param name="config">The config for Bot</param>
/// <param name="uin">Uin, if QrCode login is used, ensure the account that scans QrCode is consistent with this Uin</param>
/// <param name="password">The password of account, for Password Login</param>
/// <param name="device">Created device, should be serialized to files for next use</param>
/// <returns>Created BotContext Instance</returns>
public static BotContext Create(BotConfig config, uint uin, string password, out BotDeviceInfo device) =>
new(config, device = BotDeviceInfo.GenerateInfo(), new BotKeystore(uin, password), BotAppInfo.ProtocolToAppInfo[config.Protocol]);
/// <summary>
/// Create new Bot from Password and uin with custom <see cref="BotAppInfo"/>
/// </summary>
/// <param name="config">The config for Bot</param>
/// <param name="uin">Uin, if QrCode login is used, ensure the account that scans QrCode is consistent with this Uin</param>
/// <param name="password">The password of account, for Password Login</param>
/// <param name="appInfo">Custom BotAppInfo, including client version, app ID, etc</param>
/// <param name="device">Created device, should be serialized to files for next use</param>
/// <returns></returns>
public static BotContext Create(BotConfig config, uint uin, string password, BotAppInfo appInfo, out BotDeviceInfo device) =>
new(config, device = BotDeviceInfo.GenerateInfo(), new BotKeystore(uin, password), appInfo);
}

View File

@@ -0,0 +1,12 @@
namespace Lagrange.Core.Event.EventArg;
public class BotCaptchaEvent : EventBase
{
public string Url { get; }
public BotCaptchaEvent(string url)
{
Url = url;
EventMessage = $"[{nameof(BotCaptchaEvent)}]: Url: {url}";
}
}

View File

@@ -0,0 +1,30 @@
namespace Lagrange.Core.Event.EventArg;
public enum LogLevel
{
Debug,
Verbose,
Information,
Warning,
Exception,
Fatal
}
public class BotLogEvent : EventBase
{
private const string DateFormat = "yyyy-MM-dd HH:mm:ss";
public string Tag { get; }
public LogLevel Level { get; }
internal BotLogEvent(string tag, LogLevel level, string content)
{
Tag = tag;
Level = level;
EventMessage = content;
}
public override string ToString() =>
$"[{EventTime.ToString(DateFormat)}] [{Tag}] [{Level.ToString().ToUpper()}]: {EventMessage}";
}

View File

@@ -0,0 +1,15 @@
namespace Lagrange.Core.Event.EventArg;
public class BotNewDeviceVerifyEvent : EventBase
{
public string Url { get; }
public byte[] QrCode { get; }
public BotNewDeviceVerifyEvent(string url, byte[] qrCode)
{
Url = url;
EventMessage = $"[{nameof(BotNewDeviceVerifyEvent)}]: Url: {url}";
QrCode = qrCode;
}
}

View File

@@ -0,0 +1,14 @@
namespace Lagrange.Core.Event.EventArg;
public class BotOfflineEvent : EventBase
{
public string Tag { get; }
public string Message { get; }
public BotOfflineEvent(string tag, string message)
{
Tag = tag;
Message = message;
}
}

View File

@@ -0,0 +1,18 @@
namespace Lagrange.Core.Event.EventArg;
public class BotOnlineEvent : EventBase
{
public OnlineReason Reason { get; }
public BotOnlineEvent(OnlineReason reason)
{
Reason = reason;
EventMessage = $"[{nameof(BotOnlineEvent)}]: {Reason}";
}
public enum OnlineReason
{
Login,
Reconnect
}
}

View File

@@ -0,0 +1,23 @@
namespace Lagrange.Core.Event.EventArg;
public class DeviceLoginEvent : EventBase
{
public bool IsLogin { get; }
public uint AppId { get; }
public string Tag { get; }
public string Message { get; }
public DeviceLoginEvent(bool isLogin, uint appId, string tag, string message)
{
IsLogin = isLogin;
AppId = appId;
Tag = tag;
Message = message;
EventMessage = $"[{nameof(DeviceLoginEvent)}]: {Tag} | {Message}, AppID: {AppId}, IsLogin: {IsLogin}";
}
}

View File

@@ -0,0 +1,13 @@
using Lagrange.Core.Message;
namespace Lagrange.Core.Event.EventArg;
public class FriendMessageEvent : EventBase
{
public MessageChain Chain { get; set; }
public FriendMessageEvent(MessageChain chain)
{
Chain = chain;
}
}

View File

@@ -0,0 +1,25 @@
namespace Lagrange.Core.Event.EventArg;
public class FriendPokeEvent : EventBase
{
public uint OperatorUin { get; }
public uint TargetUin { get; }
public string Action { get; }
public string Suffix { get; }
public string ActionImgUrl { get; }
public FriendPokeEvent(uint operatorUin, uint targetUin, string action, string suffix, string actionImgUrl)
{
OperatorUin = operatorUin;
TargetUin = targetUin;
Action = action;
Suffix = suffix;
ActionImgUrl = actionImgUrl;
EventMessage = $"{nameof(FriendPokeEvent)}: OperatorUin: {OperatorUin} | TargetUin: {TargetUin} | Action: {Action} | Suffix: {Suffix} | ActionImgUrl: {ActionImgUrl}";
}
}

View File

@@ -0,0 +1,25 @@
namespace Lagrange.Core.Event.EventArg;
public class FriendRecallEvent : EventBase
{
public uint FriendUin { get; }
public uint ClientSequence { get; }
public uint Time { get; }
public uint Random { get; }
public string Tip { get; }
public FriendRecallEvent(uint friendUin, uint clientSequence, uint time, uint random, string tip)
{
FriendUin = friendUin;
ClientSequence = clientSequence;
Time = time;
Random = random;
Tip = tip;
EventMessage = $"{nameof(FriendRecallEvent)}: {FriendUin} | ({ClientSequence} | {Time} | {Random} | {Tip})";
}
}

View File

@@ -0,0 +1,22 @@
namespace Lagrange.Core.Event.EventArg;
public class FriendRequestEvent : EventBase
{
public uint SourceUin { get; }
internal string SourceUid { get; }
public string Source { get; }
public string Message { get; }
internal FriendRequestEvent(uint sourceUin, string sourceUid, string message, string source)
{
SourceUin = sourceUin;
SourceUid = sourceUid;
Message = message;
Source = source;
EventMessage = $"[{nameof(FriendRequestEvent)}]: {SourceUin}:{Source} {Message}";
}
}

View File

@@ -0,0 +1,18 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupAdminChangedEvent : EventBase
{
public uint GroupUin { get; }
public uint AdminUin { get; }
public bool IsPromote { get; }
public GroupAdminChangedEvent(uint groupUin, uint adminUin, bool isPromote)
{
GroupUin = groupUin;
AdminUin = adminUin;
IsPromote = isPromote;
EventMessage = $"{nameof(GroupAdminChangedEvent)} | {GroupUin} | {AdminUin} | {IsPromote}";
}
}

View File

@@ -0,0 +1,28 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupEssenceEvent : EventBase
{
public uint GroupUin { get; }
public uint Sequence { get; }
public uint Random { get; }
public bool IsSet { get; } // 1 设置精华消息, 2 移除精华消息
public uint FromUin { get; }
public uint OperatorUin { get; }
public GroupEssenceEvent(uint groupUin, uint sequence,uint random, uint setFlag, uint fromUin, uint operatorUin)
{
GroupUin = groupUin;
Sequence = sequence;
Random = random;
IsSet = setFlag == 1;
FromUin = fromUin;
OperatorUin = operatorUin;
EventMessage = $"{nameof(GroupEssenceEvent)}: {GroupUin} | {FromUin} | {OperatorUin} | ({Sequence}) | IsSet: {IsSet}";
}
}

View File

@@ -0,0 +1,15 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupInvitationEvent : EventBase
{
public uint GroupUin { get; }
public uint InvitorUin { get; }
internal GroupInvitationEvent(uint groupUin, uint invitorUin)
{
GroupUin = groupUin;
InvitorUin = invitorUin;
EventMessage = $"[{nameof(GroupInvitationEvent)}]: {GroupUin} from {InvitorUin}";
}
}

View File

@@ -0,0 +1,18 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupInvitationRequestEvent : EventBase
{
internal GroupInvitationRequestEvent(uint groupUin, uint targetUin, uint invitorUin)
{
GroupUin = groupUin;
TargetUin = targetUin;
InvitorUin = invitorUin;
EventMessage = $"[{nameof(GroupInvitationRequestEvent)}] {TargetUin} from {InvitorUin} at {GroupUin}";
}
public uint GroupUin { get; }
public uint TargetUin { get; }
public uint InvitorUin { get; }
}

View File

@@ -0,0 +1,15 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupJoinRequestEvent : EventBase
{
internal GroupJoinRequestEvent(uint groupUin, uint targetUin)
{
TargetUin = targetUin;
GroupUin = groupUin;
EventMessage = $"[{nameof(GroupJoinRequestEvent)}] {TargetUin} at {GroupUin}";
}
public uint TargetUin { get; }
public uint GroupUin { get; }
}

View File

@@ -0,0 +1,30 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupMemberDecreaseEvent : EventBase
{
public uint GroupUin { get; }
public uint MemberUin { get; }
public uint? OperatorUin { get; }
public EventType Type { get; }
public GroupMemberDecreaseEvent(uint groupUin, uint memberUin, uint? operatorUin, uint type)
{
GroupUin = groupUin;
MemberUin = memberUin;
OperatorUin = operatorUin;
Type = (EventType)type;
EventMessage = $"{nameof(GroupMemberDecreaseEvent)}: {GroupUin} | {MemberUin} | {OperatorUin} | {Type}";
}
public enum EventType : uint
{
KickMe = 3,
Disband = 129,
Leave = 130,
Kick = 131
}
}

View File

@@ -0,0 +1,19 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupMemberEnterEvent : EventBase
{
public uint GroupUin { get; }
public uint GroupMemberUin { get; }
public uint StyleId { get; }
internal GroupMemberEnterEvent(uint groupUin, uint groupMemberUin, uint styleId)
{
GroupUin = groupUin;
GroupMemberUin = groupMemberUin;
StyleId = styleId;
EventMessage = $"[{nameof(GroupMemberEnterEvent)}]: {GroupMemberUin} Enter {GroupUin} | {StyleId}";
}
}

View File

@@ -0,0 +1,28 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupMemberIncreaseEvent : EventBase
{
public uint GroupUin { get; }
public uint MemberUin { get; }
public uint? InvitorUin { get; }
public EventType Type { get; }
public GroupMemberIncreaseEvent(uint groupUin, uint memberUin, uint? invitorUin, uint type)
{
GroupUin = groupUin;
MemberUin = memberUin;
InvitorUin = invitorUin;
Type = (EventType)type;
EventMessage = $"{nameof(GroupMemberIncreaseEvent)}: {GroupUin} | {MemberUin} | {InvitorUin} | {Type}";
}
public enum EventType : uint
{
Approve = 130,
Invite = 131
}
}

View File

@@ -0,0 +1,22 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupMemberMuteEvent : EventBase
{
public uint GroupUin { get; }
public uint TargetUin { get; }
public uint? OperatorUin { get; }
public uint Duration { get; }
public GroupMemberMuteEvent(uint groupUin, uint targetUin, uint? operatorUin, uint duration)
{
GroupUin = groupUin;
TargetUin = targetUin;
OperatorUin = operatorUin;
Duration = duration;
EventMessage = $"{nameof(GroupMemberMuteEvent)}: {GroupUin} | {TargetUin} | {OperatorUin} | {Duration}";
}
}

View File

@@ -0,0 +1,13 @@
using Lagrange.Core.Message;
namespace Lagrange.Core.Event.EventArg;
public class GroupMessageEvent : EventBase
{
public MessageChain Chain { get; set; }
public GroupMessageEvent(MessageChain chain)
{
Chain = chain;
}
}

View File

@@ -0,0 +1,19 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupMuteEvent : EventBase
{
public uint GroupUin { get; }
public uint? OperatorUin { get; }
public bool IsMuted { get; }
public GroupMuteEvent(uint groupUin, uint? operatorUin, bool isMuted)
{
GroupUin = groupUin;
OperatorUin = operatorUin;
IsMuted = isMuted;
EventMessage = $"{nameof(GroupMuteEvent)}: {GroupUin} | {OperatorUin} | IsMuted: {IsMuted}";
}
}

View File

@@ -0,0 +1,16 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupNameChangeEvent : EventBase
{
public uint GroupUin { get; }
public string Name { get; }
public GroupNameChangeEvent(uint groupUin, string name)
{
GroupUin = groupUin;
Name = name;
EventMessage = $"{nameof(GroupNameChangeEvent)}: GroupUin: {GroupUin} | Name: {Name}";
}
}

View File

@@ -0,0 +1,28 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupPokeEvent : EventBase
{
public uint GroupUin { get; }
public uint OperatorUin { get; }
public uint TargetUin { get; }
public string Action { get; }
public string Suffix { get; }
public string ActionImgUrl { get; }
public GroupPokeEvent(uint groupUin, uint operatorUin, uint targetUin, string action, string suffix, string actionImgUrl)
{
GroupUin = groupUin;
OperatorUin = operatorUin;
TargetUin = targetUin;
Action = action;
Suffix = suffix;
ActionImgUrl = actionImgUrl;
EventMessage = $"{nameof(GroupPokeEvent)}: GroupUin: {GroupUin} | OperatorUin: {OperatorUin} | TargetUin: {TargetUin} | Action: {Action} | Suffix: {Suffix} | ActionImgUrl: {ActionImgUrl}";
}
}

View File

@@ -0,0 +1,28 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupReactionEvent : EventBase
{
public uint TargetGroupUin { get; }
public uint TargetSequence { get; }
public uint OperatorUin { get; }
public bool IsAdd { get; }
public string Code { get; }
public uint Count { get; }
public GroupReactionEvent(uint targetGroupUin, uint targetSequence, uint operatorUin, bool isAdd, string code, uint count)
{
TargetGroupUin = targetGroupUin;
TargetSequence = targetSequence;
OperatorUin = operatorUin;
IsAdd = isAdd;
Code = code;
Count = count;
EventMessage = $"{nameof(GroupReactionEvent)}: TargetGroupUin: {TargetGroupUin} | TargetSequence: {TargetSequence} | OperatorUin: {OperatorUin} | IsAdd: {IsAdd} | Code: {Code} | Count: {Count}";
}
}

View File

@@ -0,0 +1,31 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupRecallEvent : EventBase
{
public uint GroupUin { get; }
public uint AuthorUin { get; }
public uint OperatorUin { get; }
public uint Sequence { get; }
public uint Time { get; }
public uint Random { get; }
public string Tip { get; }
public GroupRecallEvent(uint groupUin, uint authorUin, uint operatorUin, uint sequence, uint time, uint random, string tip)
{
GroupUin = groupUin;
AuthorUin = authorUin;
OperatorUin = operatorUin;
Sequence = sequence;
Time = time;
Random = random;
Tip = tip;
EventMessage = $"{nameof(GroupRecallEvent)}: {GroupUin} | {AuthorUin} | {OperatorUin} | ({Sequence} | {Time} | {Random} | {Tip})";
}
}

View File

@@ -0,0 +1,16 @@
namespace Lagrange.Core.Event.EventArg;
public class GroupTodoEvent : EventBase
{
public uint GroupUin { get; }
public uint OperatorUin { get; }
public GroupTodoEvent(uint groupUin, uint operatorUin)
{
GroupUin = groupUin;
OperatorUin = operatorUin;
EventMessage = $"{nameof(GroupPokeEvent)}: GroupUin: {GroupUin} | OperatorUin: {OperatorUin}";
}
}

View File

@@ -0,0 +1,26 @@
namespace Lagrange.Core.Event.EventArg;
public class PinChangedEvent : EventBase
{
public ChatType Type { get; }
public uint Uin { get; }
public bool IsPin { get; }
public PinChangedEvent(ChatType type, uint uin, bool isPin)
{
Type = type;
Uin = uin;
IsPin = isPin;
EventMessage = $"{nameof(PinChangedEvent)} {{ChatType: {Type} | Uin: {Uin} | IsPin: {IsPin}}}";
}
public enum ChatType
{
Friend,
Group,
Service
}
}

View File

@@ -0,0 +1,13 @@
using Lagrange.Core.Message;
namespace Lagrange.Core.Event.EventArg;
public class TempMessageEvent : EventBase
{
public MessageChain Chain { get; set; }
public TempMessageEvent(MessageChain chain)
{
Chain = chain;
}
}

View File

@@ -0,0 +1,22 @@
namespace Lagrange.Core.Event;
/// <summary>
/// Event that exposed to user
/// </summary>
public abstract class EventBase : EventArgs
{
public DateTime EventTime { get; }
public string EventMessage { get; protected set; }
internal EventBase()
{
EventTime = DateTime.Now;
EventMessage = "[Empty Event Message]";
}
public override string ToString()
{
return $"[{EventTime:HH:mm:ss}] {EventMessage}";
}
}

View File

@@ -0,0 +1,62 @@
using Lagrange.Core.Event.EventArg;
namespace Lagrange.Core.Event;
public partial class EventInvoker
{
public event LagrangeEvent<BotOnlineEvent>? OnBotOnlineEvent;
public event LagrangeEvent<BotOfflineEvent>? OnBotOfflineEvent;
public event LagrangeEvent<BotLogEvent>? OnBotLogEvent;
public event LagrangeEvent<BotCaptchaEvent>? OnBotCaptchaEvent;
public event LagrangeEvent<BotNewDeviceVerifyEvent>? OnBotNewDeviceVerify;
public event LagrangeEvent<GroupInvitationEvent>? OnGroupInvitationReceived;
public event LagrangeEvent<FriendMessageEvent>? OnFriendMessageReceived;
public event LagrangeEvent<GroupMessageEvent>? OnGroupMessageReceived;
public event LagrangeEvent<TempMessageEvent>? OnTempMessageReceived;
public event LagrangeEvent<GroupAdminChangedEvent>? OnGroupAdminChangedEvent;
public event LagrangeEvent<GroupMemberIncreaseEvent>? OnGroupMemberIncreaseEvent;
public event LagrangeEvent<GroupMemberDecreaseEvent>? OnGroupMemberDecreaseEvent;
public event LagrangeEvent<FriendRequestEvent>? OnFriendRequestEvent;
public event LagrangeEvent<GroupInvitationRequestEvent>? OnGroupInvitationRequestEvent;
public event LagrangeEvent<GroupJoinRequestEvent>? OnGroupJoinRequestEvent;
public event LagrangeEvent<GroupMuteEvent>? OnGroupMuteEvent;
public event LagrangeEvent<GroupMemberMuteEvent>? OnGroupMemberMuteEvent;
public event LagrangeEvent<GroupRecallEvent>? OnGroupRecallEvent;
public event LagrangeEvent<FriendRecallEvent>? OnFriendRecallEvent;
public event LagrangeEvent<DeviceLoginEvent>? OnDeviceLoginEvent;
public event LagrangeEvent<FriendPokeEvent>? OnFriendPokeEvent;
public event LagrangeEvent<GroupPokeEvent>? OnGroupPokeEvent;
public event LagrangeEvent<GroupEssenceEvent>? OnGroupEssenceEvent;
public event LagrangeEvent<GroupReactionEvent>? OnGroupReactionEvent;
public event LagrangeEvent<GroupNameChangeEvent>? OnGroupNameChangeEvent;
public event LagrangeEvent<GroupTodoEvent>? OnGroupTodoEvent;
public event LagrangeEvent<GroupMemberEnterEvent>? OnGroupMemberEnterEvent;
public event LagrangeEvent<PinChangedEvent>? OnPinChangedEvent;
}

View File

@@ -0,0 +1,72 @@
using System.Runtime.CompilerServices;
using Lagrange.Core.Event.EventArg;
namespace Lagrange.Core.Event;
public partial class EventInvoker : IDisposable
{
private const string Tag = "EventInvoker";
private readonly Dictionary<Type, Action<EventBase>> _events;
public delegate void LagrangeEvent<in TEvent>(BotContext context, TEvent e) where TEvent : EventBase;
internal EventInvoker(BotContext context)
{
_events = new Dictionary<Type, Action<EventBase>>();
RegisterEvent((BotOnlineEvent e) => OnBotOnlineEvent?.Invoke(context, e));
RegisterEvent((BotOfflineEvent e) => OnBotOfflineEvent?.Invoke(context, e));
RegisterEvent((BotLogEvent e) => OnBotLogEvent?.Invoke(context, e));
RegisterEvent((BotCaptchaEvent e) => OnBotCaptchaEvent?.Invoke(context, e));
RegisterEvent((BotNewDeviceVerifyEvent e) => OnBotNewDeviceVerify?.Invoke(context, e));
RegisterEvent((GroupInvitationEvent e) => OnGroupInvitationReceived?.Invoke(context, e));
RegisterEvent((FriendMessageEvent e) => OnFriendMessageReceived?.Invoke(context, e));
RegisterEvent((GroupMessageEvent e) => OnGroupMessageReceived?.Invoke(context, e));
RegisterEvent((TempMessageEvent e) => OnTempMessageReceived?.Invoke(context, e));
RegisterEvent((GroupAdminChangedEvent e) => OnGroupAdminChangedEvent?.Invoke(context, e));
RegisterEvent((GroupMemberIncreaseEvent e) => OnGroupMemberIncreaseEvent?.Invoke(context, e));
RegisterEvent((GroupMemberDecreaseEvent e) => OnGroupMemberDecreaseEvent?.Invoke(context, e));
RegisterEvent((FriendRequestEvent e) => OnFriendRequestEvent?.Invoke(context, e));
RegisterEvent((GroupInvitationRequestEvent e) => OnGroupInvitationRequestEvent?.Invoke(context, e));
RegisterEvent((GroupJoinRequestEvent e) => OnGroupJoinRequestEvent?.Invoke(context, e));
RegisterEvent((GroupMuteEvent e) => OnGroupMuteEvent?.Invoke(context, e));
RegisterEvent((GroupMemberMuteEvent e) => OnGroupMemberMuteEvent?.Invoke(context, e));
RegisterEvent((GroupRecallEvent e) => OnGroupRecallEvent?.Invoke(context, e));
RegisterEvent((FriendRecallEvent e) => OnFriendRecallEvent?.Invoke(context, e));
RegisterEvent((DeviceLoginEvent e) => OnDeviceLoginEvent?.Invoke(context, e));
RegisterEvent((FriendPokeEvent e) => OnFriendPokeEvent?.Invoke(context, e));
RegisterEvent((GroupPokeEvent e) => OnGroupPokeEvent?.Invoke(context, e));
RegisterEvent((GroupEssenceEvent e) => OnGroupEssenceEvent?.Invoke(context, e));
RegisterEvent((GroupReactionEvent e) => OnGroupReactionEvent?.Invoke(context, e));
RegisterEvent((GroupNameChangeEvent e) => OnGroupNameChangeEvent?.Invoke(context, e));
RegisterEvent((GroupTodoEvent e) => OnGroupTodoEvent?.Invoke(context, e));
RegisterEvent((GroupMemberEnterEvent e) => OnGroupMemberEnterEvent?.Invoke(context, e));
RegisterEvent((PinChangedEvent e) => OnPinChangedEvent?.Invoke(context, e));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void RegisterEvent<TEvent>(Action<TEvent> action) where TEvent : EventBase => _events[typeof(TEvent)] = e => action((TEvent)e);
internal void PostEvent(EventBase e)
{
Task.Run(() =>
{
try
{
if (_events.TryGetValue(e.GetType(), out var action)) action(e);
else PostEvent(new BotLogEvent(Tag, LogLevel.Warning, $"Event {e.GetType().Name} is not registered but pushed to invoker"));
}
catch (Exception ex)
{
PostEvent(new BotLogEvent(Tag, LogLevel.Exception, $"{ex.StackTrace}\n{ex.Message}"));
}
});
}
public void Dispose()
{
GC.SuppressFinalize(this);
_events.Clear();
}
}

View File

@@ -0,0 +1,15 @@
namespace Lagrange.Core.Internal.Context.Attributes;
[AttributeUsage(AttributeTargets.Class)]
internal class BusinessLogicAttribute : Attribute
{
public string Name { get; }
public string Description { get; }
public BusinessLogicAttribute(string name, string description)
{
Name = name;
Description = description;
}
}

View File

@@ -0,0 +1,202 @@
using System.Reflection;
using Lagrange.Core.Common;
using Lagrange.Core.Internal.Context.Attributes;
using Lagrange.Core.Internal.Context.Logic;
using Lagrange.Core.Internal.Context.Logic.Implementation;
using Lagrange.Core.Internal.Event;
using Lagrange.Core.Internal.Packets;
using Lagrange.Core.Internal.Service;
using Lagrange.Core.Utility.Extension;
#pragma warning disable CS8618
namespace Lagrange.Core.Internal.Context;
internal class BusinessContext : ContextBase
{
private const string Tag = nameof(BusinessContext);
private readonly Dictionary<Type, List<LogicBase>> _businessLogics;
#region Business Logics
internal MessagingLogic MessagingLogic { get; private set; }
internal WtExchangeLogic WtExchangeLogic { get; private set; }
internal OperationLogic OperationLogic { get; private set; }
internal CachingLogic CachingLogic { get; private set; }
#endregion
public BusinessContext(ContextCollection collection, BotKeystore keystore, BotAppInfo appInfo, BotDeviceInfo device)
: base(collection, keystore, appInfo, device)
{
_businessLogics = new Dictionary<Type, List<LogicBase>>();
RegisterLogics();
}
private void RegisterLogics()
{
var assembly = Assembly.GetExecutingAssembly();
foreach (var logic in assembly.GetTypeByAttributes<BusinessLogicAttribute>(out _))
{
var constructor = logic.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
var instance = (LogicBase)constructor[0].Invoke(new object[] { Collection });
foreach (var @event in logic.GetCustomAttributes<EventSubscribeAttribute>())
{
if (!_businessLogics.TryGetValue(@event.EventType, out var list))
{
list = new List<LogicBase>();
_businessLogics.Add(@event.EventType, list);
}
list.Add(instance); // Append logics
}
switch (instance)
{
case WtExchangeLogic wtExchangeLogic:
WtExchangeLogic = wtExchangeLogic;
break;
case MessagingLogic messagingLogic:
MessagingLogic = messagingLogic;
break;
case OperationLogic operationLogic:
OperationLogic = operationLogic;
break;
case CachingLogic cachingLogic:
CachingLogic = cachingLogic;
break;
}
}
}
public async Task<bool> PushEvent(ProtocolEvent @event)
{
try
{
var packets = Collection.Service.ResolvePacketByEvent(@event);
foreach (var packet in packets) await Collection.Packet.PostPacket(packet);
}
catch
{
return false;
}
return true;
}
/// <summary>
/// Send Event to the Server, goes through the given context
/// </summary>
public async Task<List<ProtocolEvent>> SendEvent(ProtocolEvent @event)
{
await HandleOutgoingEvent(@event);
var result = new List<ProtocolEvent>();
try
{
var packets = Collection.Service.ResolvePacketByEvent(@event);
foreach (var packet in packets)
{
var returnVal = await Collection.Packet.SendPacket(packet);
var resolved = Collection.Service.ResolveEventByPacket(returnVal);
foreach (var protocol in resolved)
{
await HandleIncomingEvent(protocol);
result.Add(protocol);
}
}
}
catch (Exception e)
{
Collection.Log.LogWarning(Tag, $"Error when processing the event: {@event}");
Collection.Log.LogWarning(Tag, e.ToString());
}
return result;
}
public async Task<bool> HandleIncomingEvent(ProtocolEvent @event)
{
_businessLogics.TryGetValue(typeof(ProtocolEvent), out var baseLogics);
_businessLogics.TryGetValue(@event.GetType(), out var normalLogics);
var logics = new List<LogicBase>();
if (baseLogics != null) logics.AddRange(baseLogics);
if (normalLogics != null) logics.AddRange(normalLogics);
foreach (var logic in logics)
{
try
{
await logic.Incoming(@event);
}
catch (Exception e)
{
Collection.Log.LogFatal(Tag, $"Error occurred while handling event {@event.GetType().Name}");
Collection.Log.LogFatal(Tag, e.Message);
if (e.StackTrace != null) Collection.Log.LogFatal(Tag, e.StackTrace);
}
}
return true;
}
public async Task<bool> HandleOutgoingEvent(ProtocolEvent @event)
{
_businessLogics.TryGetValue(typeof(ProtocolEvent), out var baseLogics);
_businessLogics.TryGetValue(@event.GetType(), out var normalLogics);
var logics = new List<LogicBase>();
if (baseLogics != null) logics.AddRange(baseLogics);
if (normalLogics != null) logics.AddRange(normalLogics);
foreach (var logic in logics)
{
try
{
await logic.Outgoing(@event);
}
catch (Exception e)
{
Collection.Log.LogFatal(Tag, $"Error occurred while handling outgoing event {@event.GetType().Name}");
Collection.Log.LogFatal(Tag, e.Message);
if (e.StackTrace != null) Collection.Log.LogFatal(Tag, e.StackTrace);
}
}
return true;
}
/// <summary>
/// Handle the incoming packet with new sequence number.
/// </summary>
public async Task<bool> HandleServerPacket(SsoPacket packet)
{
bool success = false;
try
{
var events = Collection.Service.ResolveEventByPacket(packet);
foreach (var @event in events)
{
var isSuccessful = await Collection.Business.HandleIncomingEvent(@event);
if (!isSuccessful) break;
success = true;
}
}
catch (Exception e)
{
Collection.Log.LogWarning(Tag, $"Error while handling msf push: {packet.PacketType} {packet.Command}");
Collection.Log.LogWarning(Tag, e.Message);
if (e.StackTrace is { } stackTrace) Collection.Log.LogWarning(Tag, stackTrace);
Collection.Log.LogDebug(Tag, packet.Payload.ToArray().Hex());
}
return success;
}
}

View File

@@ -0,0 +1,22 @@
using Lagrange.Core.Common;
namespace Lagrange.Core.Internal.Context;
internal abstract class ContextBase
{
protected readonly ContextCollection Collection;
protected readonly BotKeystore Keystore;
protected readonly BotAppInfo AppInfo;
protected readonly BotDeviceInfo DeviceInfo;
protected ContextBase(ContextCollection collection, BotKeystore keystore, BotAppInfo appInfo, BotDeviceInfo device)
{
Collection = collection;
Keystore = keystore;
AppInfo = appInfo;
DeviceInfo = device;
}
}

View File

@@ -0,0 +1,48 @@
using Lagrange.Core.Common;
using Lagrange.Core.Event;
using TaskScheduler = Lagrange.Core.Utility.TaskScheduler;
namespace Lagrange.Core.Internal.Context;
internal class ContextCollection : IDisposable
{
public PacketContext Packet { get; }
public SocketContext Socket { get; }
public ServiceContext Service { get; }
public BusinessContext Business { get; }
public LogContext Log { get; }
public HighwayContext Highway { get; }
public BotKeystore Keystore { get; }
public BotAppInfo AppInfo { get; }
public BotDeviceInfo Device { get; }
public TaskScheduler Scheduler { get; }
public EventInvoker Invoker { get; }
public ContextCollection(BotKeystore keystore, BotAppInfo appInfo, BotDeviceInfo device, BotConfig config,
EventInvoker invoker, TaskScheduler scheduler)
{
Log = new LogContext(this, keystore, appInfo, device, invoker);
Packet = new PacketContext(this, keystore, appInfo, device, config);
Socket = new SocketContext(this, keystore, appInfo, device, config);
Service = new ServiceContext(this, keystore, appInfo, device);
Business = new BusinessContext(this, keystore, appInfo, device);
Highway = new HighwayContext(this, keystore, appInfo, device, config);
Keystore = keystore;
AppInfo = appInfo;
Device = device;
Scheduler = scheduler;
Invoker = invoker;
}
public void Dispose()
{
Socket.Dispose();
Highway.Dispose();
Invoker.Dispose();
Scheduler.Dispose();
}
}

View File

@@ -0,0 +1,245 @@
using System.Reflection;
using Lagrange.Core.Common;
using Lagrange.Core.Internal.Context.Uploader;
using Lagrange.Core.Internal.Event.System;
using Lagrange.Core.Internal.Packets.Service.Highway;
using Lagrange.Core.Message;
using Lagrange.Core.Utility.Binary;
using Lagrange.Core.Utility.Extension;
using ProtoBuf.Meta;
namespace Lagrange.Core.Internal.Context;
/// <summary>
/// <para>Provides BDH (Big-Data Highway) Operation</para>
/// </summary>
internal class HighwayContext : ContextBase, IDisposable
{
private const string Tag = nameof(HighwayContext);
private static readonly RuntimeTypeModel Serializer;
private uint _sequence;
private Uri? _uri;
private readonly Dictionary<Type, IHighwayUploader> _uploaders;
private readonly HttpClient _client;
private readonly int _chunkSize;
private readonly uint _concurrent;
static HighwayContext()
{
Serializer = RuntimeTypeModel.Create();
Serializer.UseImplicitZeroDefaults = false;
}
public HighwayContext(ContextCollection collection, BotKeystore keystore, BotAppInfo appInfo, BotDeviceInfo device, BotConfig config)
: base(collection, keystore, appInfo, device)
{
_uploaders = new Dictionary<Type, IHighwayUploader>();
foreach (var impl in Assembly.GetExecutingAssembly().GetImplementations<IHighwayUploader>())
{
var attribute = impl.GetCustomAttribute<HighwayUploaderAttribute>();
if (attribute != null) _uploaders[attribute.Entity] = (IHighwayUploader)impl.CreateInstance(false);
}
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, _, _, _) => true,
};
_client = new HttpClient(handler);
_client.DefaultRequestHeaders.Add("Accept-Encoding", "identity");
_client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2)");
_sequence = 0;
_chunkSize = (int)config.HighwayChunkSize;
_concurrent = config.HighwayConcurrent;
}
public async Task UploadResources(MessageChain chain)
{
foreach (var entity in chain)
{
if (_uploaders.TryGetValue(entity.GetType(), out var uploader))
{
try
{
if (chain.IsGroup) await uploader.UploadGroup(Collection, chain, entity);
else await uploader.UploadPrivate(Collection, chain, entity);
}
catch
{
Collection.Log.LogFatal(Tag, $"Upload resources for {entity.GetType().Name} failed");
}
}
}
}
public async Task ManualUploadEntity(IMessageEntity entity)
{
if (_uploaders.TryGetValue(entity.GetType(), out var uploader))
{
try
{
uint uin = Collection.Keystore.Uin;
string uid = Collection.Keystore.Uid ?? "";
var chain = new MessageChain(uin, uid, uid) { entity };
await uploader.UploadPrivate(Collection, chain, entity);
}
catch
{
Collection.Log.LogFatal(Tag, $"Upload resources for {entity.GetType().Name} failed");
}
}
}
public async Task<bool> UploadSrcByStreamAsync(int commonId, Stream data, byte[] ticket, byte[] md5, byte[]? extendInfo = null)
{
if (_uri == null)
{
var highwayUrlEvent = await Collection.Business.SendEvent(HighwayUrlEvent.Create());
var result = (HighwayUrlEvent)highwayUrlEvent[0];
_uri = result.HighwayUrls[1][0];
}
bool success = true;
var upBlocks = new List<UpBlock>();
long fileSize = data.Length;
int offset = 0;
data.Seek(0, SeekOrigin.Begin);
while (offset < fileSize)
{
var buffer = new byte[Math.Min(_chunkSize, fileSize - offset)];
int payload = await data.ReadAsync(buffer.AsMemory());
uint uin = Collection.Keystore.Uin;
uint sequence = Interlocked.Increment(ref _sequence);
var reqBody = new UpBlock(commonId, uin, sequence, (ulong)fileSize, (ulong)offset, ticket, md5, buffer, extendInfo);
upBlocks.Add(reqBody);
offset += payload;
if (upBlocks.Count >= _concurrent || data.Position == data.Length)
{
var tasks = upBlocks.Select(x => SendUpBlockAsync(x, _uri)).ToArray();
var results = await Task.WhenAll(tasks);
success &= results.All(x => x);
upBlocks.Clear();
}
}
return success;
}
private async Task<bool> SendUpBlockAsync(UpBlock upBlock, Uri server)
{
var head = new DataHighwayHead
{
Version = 1,
Uin = upBlock.Uin.ToString(),
Command = "PicUp.DataUp",
Seq = upBlock.Sequence,
AppId = (uint)AppInfo.SubAppId,
DataFlag = 16,
CommandId = (uint)upBlock.CommandId,
};
var segHead = new SegHead
{
Filesize = upBlock.FileSize,
DataOffset = upBlock.Offset,
DataLength = (uint)upBlock.Block.Length,
ServiceTicket = upBlock.Ticket,
Md5 = (await upBlock.Block.Md5Async()).UnHex(),
FileMd5 = upBlock.FileMd5,
};
var loginHead = new LoginSigHead
{
Uint32LoginSigType = 8,
BytesLoginSig = Collection.Keystore.Session.Tgt,
AppId = (uint)Collection.AppInfo.AppId
};
var highwayHead = new ReqDataHighwayHead
{
MsgBaseHead = head,
MsgSegHead = segHead,
BytesReqExtendInfo = upBlock.ExtendInfo,
Timestamp = upBlock.Timestamp,
MsgLoginSigHead = loginHead
};
bool isEnd = upBlock.Offset + (ulong)upBlock.Block.Length == upBlock.FileSize;
var payload = await SendPacketAsync(highwayHead, new BinaryPacket(upBlock.Block), server, isEnd);
var (respHead, resp) = ParsePacket(payload);
Collection.Log.LogDebug(Tag, $"Highway Block Result: {respHead.ErrorCode} | {respHead.MsgSegHead?.RetCode} | {respHead.BytesRspExtendInfo?.Hex()} | {resp.ToArray().Hex()}");
return respHead.ErrorCode == 0;
}
private Task<BinaryPacket> SendPacketAsync(ReqDataHighwayHead head, BinaryPacket buffer, Uri server, bool end = true)
{
using var stream = new MemoryStream();
Serializer.Serialize(stream, head);
var writer = new BinaryPacket()
.WriteByte(0x28) // packet start
.WriteInt((int)stream.Length)
.WriteInt((int)buffer.Length)
.WriteBytes(stream.ToArray())
.WritePacket(buffer)
.WriteByte(0x29); // packet end
return SendDataAsync(writer.ToArray(), server, end);
}
private static (RespDataHighwayHead, BinaryPacket) ParsePacket(BinaryPacket packet)
{
if (packet.ReadByte() == 0x28)
{
int headLength = packet.ReadInt();
int bodyLength = packet.ReadInt();
var head = Serializer.Deserialize<RespDataHighwayHead>(packet.ReadBytes(headLength));
var body = packet.ReadPacket(bodyLength);
if (packet.ReadByte() == 0x29) return (head, body);
}
throw new InvalidOperationException("Invalid packet");
}
private async Task<BinaryPacket> SendDataAsync(byte[] packet, Uri server, bool end)
{
var content = new ByteArrayContent(packet);
var request = new HttpRequestMessage(HttpMethod.Post, server)
{
Content = content,
Headers =
{
{ "Connection" , end ? "close" : "keep-alive" },
}
};
var response = await _client.SendAsync(request);
var data = await response.Content.ReadAsByteArrayAsync();
return new BinaryPacket(data);
}
private record struct UpBlock(
int CommandId,
uint Uin,
uint Sequence,
ulong FileSize,
ulong Offset,
byte[] Ticket,
byte[] FileMd5,
byte[] Block,
byte[]? ExtendInfo = null,
ulong Timestamp = 0);
public void Dispose()
{
_client.Dispose();
}
}

View File

@@ -0,0 +1,34 @@
using Lagrange.Core.Common;
using Lagrange.Core.Event;
using Lagrange.Core.Event.EventArg;
namespace Lagrange.Core.Internal.Context;
/// <summary>
/// Log context, all the logs will be dispatched to this context and then to the <see cref="BotLogEvent"/>.
/// </summary>
internal class LogContext : ContextBase
{
private readonly EventInvoker _invoker;
public LogContext(ContextCollection collection, BotKeystore keystore, BotAppInfo appInfo, BotDeviceInfo device, EventInvoker invoker)
: base(collection, keystore, appInfo, device) => _invoker = invoker;
public void LogDebug(string tag, string message) =>
_invoker.PostEvent(new BotLogEvent(tag, LogLevel.Debug, message));
public void LogVerbose(string tag, string message) =>
_invoker.PostEvent(new BotLogEvent(tag, LogLevel.Verbose, message));
public void LogInfo(string tag, string message) =>
_invoker.PostEvent(new BotLogEvent(tag, LogLevel.Information, message));
public void LogWarning(string tag, string message) =>
_invoker.PostEvent(new BotLogEvent(tag, LogLevel.Warning, message));
public void LogFatal(string tag, string message) =>
_invoker.PostEvent(new BotLogEvent(tag, LogLevel.Fatal, message));
public void Log(string tag, LogLevel level, string message) =>
_invoker.PostEvent(new BotLogEvent(tag, level, message));
}

View File

@@ -0,0 +1,229 @@
using System.Collections.Concurrent;
using Lagrange.Core.Common.Entity;
using Lagrange.Core.Internal.Context.Attributes;
using Lagrange.Core.Internal.Event;
using Lagrange.Core.Internal.Event.Notify;
using Lagrange.Core.Internal.Event.System;
using Lagrange.Core.Internal.Service;
namespace Lagrange.Core.Internal.Context.Logic.Implementation;
[EventSubscribe(typeof(GroupSysDecreaseEvent))]
[EventSubscribe(typeof(GroupSysIncreaseEvent))]
[EventSubscribe(typeof(GroupSysAdminEvent))]
[BusinessLogic("CachingLogic", "Cache Uin to Uid")]
internal class CachingLogic : LogicBase
{
private const string Tag = nameof(CachingLogic);
private readonly Dictionary<uint, string> _uinToUid;
private readonly List<uint> _cachedGroups;
private readonly List<BotGroup> _cachedGroupEntities;
private readonly List<BotFriend> _cachedFriends;
private readonly Dictionary<uint, List<BotGroupMember>> _cachedGroupMembers;
private readonly ConcurrentDictionary<uint, BotUserInfo> _cacheUsers;
private readonly Dictionary<uint, SysFaceEntry> _cacheFaceEntities;
private readonly List<uint> _cacheSuperFaceId;
internal CachingLogic(ContextCollection collection) : base(collection)
{
_uinToUid = new Dictionary<uint, string>();
_cachedGroups = new List<uint>();
_cachedGroupEntities = new List<BotGroup>();
_cachedFriends = new List<BotFriend>();
_cachedGroupMembers = new Dictionary<uint, List<BotGroupMember>>();
_cacheUsers = new ConcurrentDictionary<uint, BotUserInfo>();
_cacheFaceEntities = new Dictionary<uint, SysFaceEntry>();
_cacheSuperFaceId = new List<uint>();
}
public override Task Incoming(ProtocolEvent e)
{
return e switch
{
GroupSysDecreaseEvent groupSysDecreaseEvent when groupSysDecreaseEvent.MemberUid != Collection.Keystore.Uid => CacheUid(groupSysDecreaseEvent.GroupUin, true),
GroupSysIncreaseEvent groupSysIncreaseEvent => CacheUid(groupSysIncreaseEvent.GroupUin, true),
GroupSysAdminEvent groupSysAdminEvent => CacheUid(groupSysAdminEvent.GroupUin, true),
_ => Task.CompletedTask,
};
}
public async Task<List<BotGroup>> GetCachedGroups(bool refreshCache)
{
if (_cachedGroupEntities.Count == 0 || refreshCache)
{
_cachedGroupEntities.Clear();
var fetchGroupsEvent = FetchGroupsEvent.Create();
var events = await Collection.Business.SendEvent(fetchGroupsEvent);
var groups = ((FetchGroupsEvent)events[0]).Groups;
_cachedGroupEntities.AddRange(groups);
return groups;
}
return _cachedGroupEntities;
}
public async Task<string?> ResolveUid(uint? groupUin, uint friendUin)
{
if (_uinToUid.Count == 0) await ResolveFriendsUidAndFriendGroups();
if (groupUin == null) return _uinToUid.GetValueOrDefault(friendUin);
await CacheUid(groupUin.Value);
return _uinToUid.GetValueOrDefault(friendUin);
}
public async Task<uint?> ResolveUin(uint? groupUin, string friendUid, bool force = false)
{
if (_uinToUid.Count == 0) await ResolveFriendsUidAndFriendGroups();
if (groupUin == null) return _uinToUid.FirstOrDefault(x => x.Value == friendUid).Key;
await CacheUid(groupUin.Value, force);
return _uinToUid.FirstOrDefault(x => x.Value == friendUid).Key;
}
public async Task<List<BotGroupMember>> GetCachedMembers(uint groupUin, bool refreshCache)
{
if (!_cachedGroupMembers.TryGetValue(groupUin, out var members) || refreshCache)
{
await ResolveMembersUid(groupUin);
return _cachedGroupMembers.TryGetValue(groupUin, out members) ? members : new List<BotGroupMember>();
}
return members;
}
public async Task<List<BotFriend>> GetCachedFriends(bool refreshCache)
{
if (_cachedFriends.Count == 0 || refreshCache) await ResolveFriendsUidAndFriendGroups();
return _cachedFriends;
}
public async Task<BotUserInfo?> GetCachedUsers(uint uin, bool refreshCache)
{
if (!_cacheUsers.ContainsKey(uin) || refreshCache) await ResolveUser(uin);
if (!_cacheUsers.TryGetValue(uin, out BotUserInfo? info)) return null;
return info;
}
private async Task CacheUid(uint groupUin, bool force = false)
{
if (!_cachedGroups.Contains(groupUin) || force)
{
Collection.Log.LogVerbose(Tag, $"Caching group members: {groupUin}");
await ResolveMembersUid(groupUin);
_cachedGroups.Add(groupUin);
}
}
private async Task ResolveFriendsUidAndFriendGroups()
{
uint? next = null;
var friends = new List<BotFriend>();
var friendGroups = new Dictionary<uint, string>();
do
{
var @event = FetchFriendsEvent.Create(next);
var results = await Collection.Business.SendEvent(@event);
if (results.Count == 0)
{
Collection.Log.LogWarning(Tag, "Failed to resolve friends uid and cache.");
return;
}
var result = (FetchFriendsEvent)results[0];
foreach ((uint id, string name) in result.FriendGroups) friendGroups[id] = name;
foreach (var friend in result.Friends)
{
friend.Group = new(friend.Group.GroupId, friendGroups[friend.Group.GroupId]);
}
friends.AddRange(result.Friends);
next = result.NextUin;
} while (next.HasValue);
foreach (var friend in friends) _uinToUid.TryAdd(friend.Uin, friend.Uid);
_cachedFriends.Clear();
_cachedFriends.AddRange(friends);
}
private async Task ResolveMembersUid(uint groupUin)
{
var fetchFriendsEvent = FetchMembersEvent.Create(groupUin);
var events = await Collection.Business.SendEvent(fetchFriendsEvent);
if (events.Count != 0)
{
var @event = (FetchMembersEvent)events[0];
string? token = @event.Token;
while (token != null)
{
var next = FetchMembersEvent.Create(groupUin, token);
var results = await Collection.Business.SendEvent(next);
@event.Members.AddRange(((FetchMembersEvent)results[0]).Members);
token = ((FetchMembersEvent)results[0]).Token;
}
foreach (var member in @event.Members) _uinToUid.TryAdd(member.Uin, member.Uid);
_cachedGroupMembers[groupUin] = @event.Members;
}
else
{
_cachedGroupMembers[groupUin] = new List<BotGroupMember>();
Collection.Log.LogWarning(Tag, $"Failed to resolve group {groupUin} members uid and cache.");
}
}
private async Task ResolveUser(uint uin)
{
var events = await Collection.Business.SendEvent(FetchUserInfoEvent.Create(uin));
if (events.Count != 0 && events[0] is FetchUserInfoEvent { } @event)
{
_cacheUsers.AddOrUpdate(uin, @event.UserInfo, (_key, _value) => @event.UserInfo);
}
}
private async Task ResolveEmojiCache()
{
var fetchSysEmojisEvent = FetchFullSysFacesEvent.Create();
var events = await Collection.Business.SendEvent(fetchSysEmojisEvent);
var emojiPacks = ((FetchFullSysFacesEvent)events[0]).FacePacks;
emojiPacks
.SelectMany(pack => pack.Emojis)
.Where(emoji => uint.TryParse(emoji.QSid, out _))
.ToList()
.ForEach(emoji => _cacheFaceEntities[uint.Parse(emoji.QSid)] = emoji);
_cacheSuperFaceId.AddRange(emojiPacks
.SelectMany(emojiPack => emojiPack.GetUniqueSuperQSids(new[] { (1, 1) })));
}
public async Task<bool> GetCachedIsSuperFaceId(uint id)
{
if (!_cacheSuperFaceId.Any()) await ResolveEmojiCache();
return _cacheSuperFaceId.Contains(id);
}
public async Task<SysFaceEntry?> GetCachedFaceEntity(uint faceId)
{
if (!_cacheFaceEntities.ContainsKey(faceId)) await ResolveEmojiCache();
return _cacheFaceEntities.GetValueOrDefault(faceId);
}
}

View File

@@ -0,0 +1,527 @@
using Lagrange.Core.Event;
using Lagrange.Core.Event.EventArg;
using Lagrange.Core.Internal.Context.Attributes;
using Lagrange.Core.Internal.Event;
using Lagrange.Core.Internal.Event.Action;
using Lagrange.Core.Internal.Event.Message;
using Lagrange.Core.Internal.Event.Notify;
using Lagrange.Core.Internal.Event.System;
using Lagrange.Core.Internal.Service;
using Lagrange.Core.Message;
using Lagrange.Core.Message.Entity;
using Lagrange.Core.Message.Filter;
using FriendPokeEvent = Lagrange.Core.Event.EventArg.FriendPokeEvent;
using GroupPokeEvent = Lagrange.Core.Event.EventArg.GroupPokeEvent;
namespace Lagrange.Core.Internal.Context.Logic.Implementation;
[EventSubscribe(typeof(PushMessageEvent))]
[EventSubscribe(typeof(SendMessageEvent))]
[EventSubscribe(typeof(MultiMsgUploadEvent))]
[EventSubscribe(typeof(GetRoamMessageEvent))]
[EventSubscribe(typeof(GetGroupMessageEvent))]
[EventSubscribe(typeof(GroupSysInviteEvent))]
[EventSubscribe(typeof(GroupSysAdminEvent))]
[EventSubscribe(typeof(GroupSysIncreaseEvent))]
[EventSubscribe(typeof(GroupSysDecreaseEvent))]
[EventSubscribe(typeof(GroupSysMuteEvent))]
[EventSubscribe(typeof(GroupSysMemberMuteEvent))]
[EventSubscribe(typeof(GroupSysRecallEvent))]
[EventSubscribe(typeof(GroupSysRequestJoinEvent))]
[EventSubscribe(typeof(GroupSysRequestInvitationEvent))]
[EventSubscribe(typeof(GroupSysEssenceEvent))]
[EventSubscribe(typeof(GroupSysPokeEvent))]
[EventSubscribe(typeof(GroupSysReactionEvent))]
[EventSubscribe(typeof(GroupSysNameChangeEvent))]
[EventSubscribe(typeof(FriendSysRecallEvent))]
[EventSubscribe(typeof(FriendSysRequestEvent))]
[EventSubscribe(typeof(GroupSysMemberEnterEvent))]
[EventSubscribe(typeof(FriendSysPokeEvent))]
[EventSubscribe(typeof(LoginNotifyEvent))]
[EventSubscribe(typeof(MultiMsgDownloadEvent))]
[EventSubscribe(typeof(GroupSysTodoEvent))]
[EventSubscribe(typeof(SysPinChangedEvent))]
[EventSubscribe(typeof(FetchPinsEvent))]
[EventSubscribe(typeof(SetPinFriendEvent))]
[BusinessLogic("MessagingLogic", "Manage the receiving and sending of messages and notifications")]
internal class MessagingLogic : LogicBase
{
private const string Tag = nameof(MessagingLogic);
internal MessagingLogic(ContextCollection collection) : base(collection) { }
public override async Task Incoming(ProtocolEvent e)
{
switch (e)
{
case PushMessageEvent push:
{
if (push.Chain.Count == 0) return;
await ResolveIncomingChain(push.Chain);
await ResolveChainMetadata(push.Chain);
MessageFilter.Filter(push.Chain);
var chain = push.Chain;
Collection.Log.LogVerbose(Tag, chain.ToPreviewString());
EventBase args = push.Chain.Type switch
{
MessageChain.MessageType.Friend => new FriendMessageEvent(chain),
MessageChain.MessageType.Group => new GroupMessageEvent(chain),
MessageChain.MessageType.Temp => new TempMessageEvent(chain),
_ => throw new ArgumentOutOfRangeException()
};
Collection.Invoker.PostEvent(args);
break;
}
case GetRoamMessageEvent get:
{
foreach (var chain in get.Chains)
{
if (chain.Count == 0) return;
await ResolveIncomingChain(chain);
await ResolveChainMetadata(chain);
MessageFilter.Filter(chain);
}
break;
}
case GetGroupMessageEvent get:
{
foreach (var chain in get.Chains)
{
if (chain.Count == 0) return;
await ResolveIncomingChain(chain);
await ResolveChainMetadata(chain);
MessageFilter.Filter(chain);
}
break;
}
case GroupSysInviteEvent invite:
{
uint invitorUin = await Collection.Business.CachingLogic.ResolveUin(null, invite.InvitorUid) ?? 0;
var inviteArgs = new GroupInvitationEvent(invite.GroupUin, invitorUin);
Collection.Invoker.PostEvent(inviteArgs);
break;
}
case GroupSysAdminEvent admin:
{
uint adminUin = await Collection.Business.CachingLogic.ResolveUin(admin.GroupUin, admin.Uid) ?? 0;
var adminArgs = new GroupAdminChangedEvent(admin.GroupUin, adminUin, admin.IsPromoted);
Collection.Invoker.PostEvent(adminArgs);
break;
}
case GroupSysIncreaseEvent increase:
{
uint memberUin = await Collection.Business.CachingLogic.ResolveUin(increase.GroupUin, increase.MemberUid, true) ?? 0;
uint? invitorUin = null;
if (increase.InvitorUid != null) invitorUin = await Collection.Business.CachingLogic.ResolveUin(increase.GroupUin, increase.InvitorUid);
var increaseArgs = new GroupMemberIncreaseEvent(increase.GroupUin, memberUin, invitorUin, increase.Type);
Collection.Invoker.PostEvent(increaseArgs);
break;
}
case GroupSysDecreaseEvent decrease:
{
uint memberUin = await Collection.Business.CachingLogic.ResolveUin(decrease.GroupUin, decrease.MemberUid) ?? 0;
uint? operatorUin = null;
if (decrease.OperatorUid != null) operatorUin = await Collection.Business.CachingLogic.ResolveUin(decrease.GroupUin, decrease.OperatorUid);
var decreaseArgs = new GroupMemberDecreaseEvent(decrease.GroupUin, memberUin, operatorUin, decrease.Type);
Collection.Invoker.PostEvent(decreaseArgs);
break;
}
case GroupSysEssenceEvent essence:
{
var essenceArgs = new GroupEssenceEvent(essence.GroupUin, essence.Sequence, essence.Random, essence.SetFlag, essence.FromUin, essence.OperatorUin);
Collection.Invoker.PostEvent(essenceArgs);
break;
}
case GroupSysPokeEvent poke:
{
var pokeArgs = new GroupPokeEvent(poke.GroupUin, poke.OperatorUin, poke.TargetUin, poke.Action, poke.Suffix, poke.ActionImgUrl);
Collection.Invoker.PostEvent(pokeArgs);
break;
}
case GroupSysReactionEvent reaction:
{
uint operatorUin = await Collection.Business.CachingLogic.ResolveUin(reaction.TargetGroupUin, reaction.OperatorUid) ?? 0;
var pokeArgs = new GroupReactionEvent(reaction.TargetGroupUin, reaction.TargetSequence, operatorUin, reaction.IsAdd, reaction.Code, reaction.Count);
Collection.Invoker.PostEvent(pokeArgs);
break;
}
case GroupSysNameChangeEvent nameChange:
{
var pokeArgs = new GroupNameChangeEvent(nameChange.GroupUin, nameChange.Name);
Collection.Invoker.PostEvent(pokeArgs);
break;
}
case FriendSysRequestEvent info:
{
var requestArgs = new FriendRequestEvent(info.SourceUin, info.SourceUid, info.Message, info.Source);
Collection.Invoker.PostEvent(requestArgs);
break;
}
case GroupSysMemberEnterEvent info:
{
var @event = new GroupMemberEnterEvent(info.GroupUin, info.GroupMemberUin, info.StyleId);
Collection.Invoker.PostEvent(@event);
break;
}
case GroupSysMuteEvent groupMute:
{
uint? operatorUin = null;
if (groupMute.OperatorUid != null) operatorUin = await Collection.Business.CachingLogic.ResolveUin(groupMute.GroupUin, groupMute.OperatorUid);
var muteArgs = new GroupMuteEvent(groupMute.GroupUin, operatorUin, groupMute.IsMuted);
Collection.Invoker.PostEvent(muteArgs);
break;
}
case GroupSysMemberMuteEvent memberMute:
{
uint memberUin = await Collection.Business.CachingLogic.ResolveUin(memberMute.GroupUin, memberMute.TargetUid) ?? 0;
uint? operatorUin = null;
if (memberMute.OperatorUid != null) operatorUin = await Collection.Business.CachingLogic.ResolveUin(memberMute.GroupUin, memberMute.OperatorUid);
var muteArgs = new GroupMemberMuteEvent(memberMute.GroupUin, memberUin, operatorUin, memberMute.Duration);
Collection.Invoker.PostEvent(muteArgs);
break;
}
case GroupSysRecallEvent recall:
{
uint authorUin = await Collection.Business.CachingLogic.ResolveUin(recall.GroupUin, recall.AuthorUid) ?? 0;
uint operatorUin = 0;
if (recall.OperatorUid != null) operatorUin = await Collection.Business.CachingLogic.ResolveUin(recall.GroupUin, recall.OperatorUid) ?? 0;
var recallArgs = new GroupRecallEvent(recall.GroupUin, authorUin, operatorUin, recall.Sequence, recall.Time, recall.Random, recall.Tip);
Collection.Invoker.PostEvent(recallArgs);
break;
}
case GroupSysRequestJoinEvent join:
{
var fetchUidEvent = FetchUserInfoEvent.Create(join.TargetUid);
var results = await Collection.Business.SendEvent(fetchUidEvent);
uint targetUin = results.Count == 0 ? 0 : ((FetchUserInfoEvent)results[0]).UserInfo.Uin;
var joinArgs = new GroupJoinRequestEvent(join.GroupUin, targetUin);
Collection.Invoker.PostEvent(joinArgs);
break;
}
case GroupSysRequestInvitationEvent invitation:
{
uint invitorUin = await Collection.Business.CachingLogic.ResolveUin(invitation.GroupUin, invitation.InvitorUid) ?? 0;
var fetchUidEvent = FetchUserInfoEvent.Create(invitation.TargetUid);
var results = await Collection.Business.SendEvent(fetchUidEvent);
uint targetUin = results.Count == 0 ? 0 : ((FetchUserInfoEvent)results[0]).UserInfo.Uin;
var invitationArgs = new GroupInvitationRequestEvent(invitation.GroupUin, targetUin, invitorUin);
Collection.Invoker.PostEvent(invitationArgs);
break;
}
case FriendSysRecallEvent recall:
{
uint fromUin = await Collection.Business.CachingLogic.ResolveUin(null, recall.FromUid) ?? 0;
var recallArgs = new FriendRecallEvent(fromUin, recall.ClientSequence, recall.Time, recall.Random, recall.Tip);
Collection.Invoker.PostEvent(recallArgs);
break;
}
case FriendSysPokeEvent poke:
{
var pokeArgs = new FriendPokeEvent(poke.OperatorUin, poke.TargetUin, poke.Action, poke.Suffix, poke.ActionImgUrl);
Collection.Invoker.PostEvent(pokeArgs);
break;
}
case LoginNotifyEvent login:
{
var deviceArgs = new DeviceLoginEvent(login.IsLogin, login.AppId, login.Tag, login.Message);
Collection.Invoker.PostEvent(deviceArgs);
break;
}
case MultiMsgDownloadEvent multi:
{
if (multi.Chains != null)
{
foreach (var chain in multi.Chains)
{
if (chain.Count == 0) continue;
await ResolveIncomingChain(chain);
MessageFilter.Filter(chain);
}
}
break;
}
case GroupSysTodoEvent todo:
{
uint uin = await Collection.Business.CachingLogic.ResolveUin(todo.GroupUin, todo.OperatorUid) ?? 0;
Collection.Invoker.PostEvent(new GroupTodoEvent(todo.GroupUin, uin));
break;
}
case SysPinChangedEvent pin:
{
uint uin = pin.GroupUin ?? await Collection.Business.CachingLogic.ResolveUin(null, pin.Uid) ?? 0;
Collection.Invoker.PostEvent(new PinChangedEvent(
pin.GroupUin == null ? PinChangedEvent.ChatType.Friend : PinChangedEvent.ChatType.Group,
uin,
pin.IsPin
));
break;
}
case FetchPinsEvent pins:
{
foreach (var friendUid in pins.FriendUids)
{
pins.FriendUins.Add(await Collection.Business.CachingLogic.ResolveUin(null, friendUid) ?? 0);
}
break;
}
}
}
public override async Task Outgoing(ProtocolEvent e)
{
switch (e)
{
case MultiMsgUploadEvent { Chains: { } chains }:
{
foreach (var chain in chains)
{
await ResolveChainMetadata(chain);
await ResolveOutgoingChain(chain);
await Collection.Highway.UploadResources(chain);
}
break;
}
case SendMessageEvent send: // resolve Uin to Uid
{
await ResolveChainMetadata(send.Chain);
await ResolveOutgoingChain(send.Chain);
await Collection.Highway.UploadResources(send.Chain);
break;
}
case SetPinFriendEvent pinFriend: // resolve Uin to Uid
{
pinFriend.Uid = await Collection.Business.CachingLogic.ResolveUid(null, pinFriend.Uin)
?? throw new Exception();
break;
}
}
}
private async Task ResolveIncomingChain(MessageChain chain)
{
foreach (var entity in chain)
{
switch (entity)
{
case FileEntity { FileHash: not null, FileUuid: not null } file: // private
{
var @event = FileDownloadEvent.Create(file.FileUuid, file.FileHash, chain.Uid, chain.SelfUid);
var results = await Collection.Business.SendEvent(@event);
if (results.Count != 0)
{
var result = (FileDownloadEvent)results[0];
file.FileUrl = result.FileUrl;
}
break;
}
case FileEntity { FileId: not null } file when chain.GroupUin is not null: // group
{
var @event = GroupFSDownloadEvent.Create(chain.GroupUin.Value, file.FileId);
var results = await Collection.Business.SendEvent(@event);
if (results.Count != 0)
{
var result = (GroupFSDownloadEvent)results[0];
file.FileUrl = result.FileUrl;
}
break;
}
case MultiMsgEntity { ResId: not null } multi:
{
var @event = MultiMsgDownloadEvent.Create(chain.Uid ?? "", multi.ResId);
var results = await Collection.Business.SendEvent(@event);
if (results.Count != 0)
{
var result = (MultiMsgDownloadEvent)results[0];
multi.Chains.AddRange((IEnumerable<MessageChain>?)result.Chains ?? Array.Empty<MessageChain>());
}
break;
}
case RecordEntity { MsgInfo: not null } record:
{
var @event = chain.IsGroup
? RecordGroupDownloadEvent.Create(chain.GroupUin ?? 0, record.MsgInfo)
: RecordDownloadEvent.Create(chain.Uid ?? string.Empty, record.MsgInfo);
var results = await Collection.Business.SendEvent(@event);
if (results.Count != 0)
{
var result = (RecordDownloadEvent)results[0];
record.AudioUrl = result.AudioUrl;
}
break;
}
case RecordEntity { AudioUuid: not null } record:
{
var @event = chain.IsGroup
? RecordGroupDownloadEvent.Create(chain.GroupUin ?? 0, record.AudioUuid)
: RecordDownloadEvent.Create(chain.Uid ?? string.Empty, record.AudioUuid);
var results = await Collection.Business.SendEvent(@event);
if (results.Count != 0)
{
var result = (RecordDownloadEvent)results[0];
record.AudioUrl = result.AudioUrl;
}
break;
}
case VideoEntity { VideoUuid: not null } video:
{
string uid = (chain.IsGroup
? await Collection.Business.CachingLogic.ResolveUid(chain.GroupUin, chain.FriendUin)
: chain.Uid) ?? "";
var @event = VideoDownloadEvent.Create(video.VideoUuid, uid, video.FilePath, "", "", chain.IsGroup);
var results = await Collection.Business.SendEvent(@event);
if (results.Count != 0)
{
var result = (VideoDownloadEvent)results[0];
video.VideoUrl = result.AudioUrl;
}
break;
}
case ImageEntity image when !image.ImageUrl.Contains("&rkey=") && image.MsgInfo is not null:
{
var @event = image.IsGroup
? ImageGroupDownloadEvent.Create(chain.GroupUin ?? 0, image.MsgInfo)
: ImageDownloadEvent.Create(chain.Uid ?? string.Empty, image.MsgInfo);
var results = await Collection.Business.SendEvent(@event);
if (results.Count != 0)
{
var result = (ImageDownloadEvent)results[0];
image.ImageUrl = result.ImageUrl;
}
break;
}
}
}
}
private async Task ResolveOutgoingChain(MessageChain chain)
{
foreach (var entity in chain)
{
switch (entity)
{
case FaceEntity face:
{
var cache = Collection.Business.CachingLogic;
face.SysFaceEntry ??= await cache.GetCachedFaceEntity(face.FaceId);
break;
}
case BounceFaceEntity bounceFace:
{
var cache = Collection.Business.CachingLogic;
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (bounceFace.Name != null)
break;
string name = (await cache.GetCachedFaceEntity(bounceFace.FaceId))?.QDes ?? string.Empty;
// Because the name is used as a preview text, it should not start with '/'
// But the QDes of the face may start with '/', so remove it
if (name.StartsWith('/'))
name = name[1..];
bounceFace.Name = name;
break;
}
case ForwardEntity forward when forward.TargetUin != 0:
{
var cache = Collection.Business.CachingLogic;
forward.Uid = await cache.ResolveUid(chain.GroupUin, forward.TargetUin) ?? throw new Exception($"Failed to resolve Uid for Uin {forward.TargetUin}");
break;
}
case MentionEntity mention when mention.Uin != 0:
{
var cache = Collection.Business.CachingLogic;
mention.Uid = await cache.ResolveUid(chain.GroupUin, mention.Uin) ?? throw new Exception($"Failed to resolve Uid for Uin {mention.Uin}");
if (chain is { IsGroup: true, GroupUin: not null } && mention.Name is null)
{
var members = await Collection.Business.CachingLogic.GetCachedMembers(chain.GroupUin.Value, false);
var member = members.FirstOrDefault(x => x.Uin == mention.Uin);
if (member != null) mention.Name = $"@{member.MemberCard ?? member.MemberName}";
}
else if (chain is { IsGroup: false } && mention.Name is null)
{
var friends = await Collection.Business.CachingLogic.GetCachedFriends(false);
string? friend = friends.FirstOrDefault(x => x.Uin == mention.Uin)?.Nickname;
if (friend != null) mention.Name = $"@{friend}";
}
break;
}
case MultiMsgEntity { ResId: null } multiMsg:
{
if (chain.GroupUin != null) foreach (var c in multiMsg.Chains) c.GroupUin = chain.GroupUin;
var multiMsgEvent = MultiMsgUploadEvent.Create(chain.GroupUin, multiMsg.Chains);
var results = await Collection.Business.SendEvent(multiMsgEvent);
if (results.Count != 0)
{
var result = (MultiMsgUploadEvent)results[0];
multiMsg.ResId = result.ResId;
}
break;
}
case MultiMsgEntity { ResId: not null, Chains.Count: 0 } multiMsg:
{
var @event = MultiMsgDownloadEvent.Create(chain.Uid ?? "", multiMsg.ResId);
var results = await Collection.Business.SendEvent(@event);
if (results.Count != 0)
{
var result = (MultiMsgDownloadEvent)results[0];
multiMsg.Chains.AddRange((IEnumerable<MessageChain>?)result.Chains ?? Array.Empty<MessageChain>());
}
break;
}
}
}
}
/// <summary>
/// <para>Resolve the <see cref="MessageChain.GroupMemberInfo"/> or <see cref="MessageChain.FriendInfo"/> for the <see cref="MessageChain"/></para>
/// <para>for both Incoming and Outgoing MessageChain</para>
/// </summary>
/// <param name="chain">The target chain</param>
private async Task ResolveChainMetadata(MessageChain chain)
{
if (chain is { IsGroup: true, GroupUin: not null })
{
var groups = await Collection.Business.CachingLogic.GetCachedMembers(chain.GroupUin.Value, false);
chain.GroupMemberInfo = chain.FriendUin == 0
? groups.FirstOrDefault(x => x.Uin == Collection.Keystore.Uin)
: groups.FirstOrDefault(x => x.Uin == chain.FriendUin);
chain.Uid ??= chain.GroupMemberInfo?.Uid;
}
else
{
var friends = await Collection.Business.CachingLogic.GetCachedFriends(false);
if (friends.FirstOrDefault(x => x.Uin == chain.FriendUin) is { } friend)
{
chain.FriendInfo = friend;
chain.Uid ??= friend.Uid;
}
}
}
}

View File

@@ -0,0 +1,868 @@
using Lagrange.Core.Common.Entity;
using Lagrange.Core.Internal.Context.Attributes;
using Lagrange.Core.Internal.Context.Uploader;
using Lagrange.Core.Internal.Event.Action;
using Lagrange.Core.Internal.Event.Message;
using Lagrange.Core.Internal.Event.System;
using Lagrange.Core.Internal.Packets.Service.Highway;
using Lagrange.Core.Message;
using Lagrange.Core.Message.Entity;
using Lagrange.Core.Utility.Extension;
namespace Lagrange.Core.Internal.Context.Logic.Implementation;
[BusinessLogic("OperationLogic", "Manage the user operation of the bot")]
internal class OperationLogic : LogicBase
{
private const string Tag = nameof(OperationLogic);
internal OperationLogic(ContextCollection collection) : base(collection) { }
public async Task<List<string>> GetCookies(List<string> domains)
{
var fetchCookieEvent = FetchCookieEvent.Create(domains);
var events = await Collection.Business.SendEvent(fetchCookieEvent);
return events.Count != 0 ? ((FetchCookieEvent)events[0]).Cookies : new List<string>();
}
public Task<List<BotFriend>> FetchFriends(bool refreshCache = false) =>
Collection.Business.CachingLogic.GetCachedFriends(refreshCache);
public Task<List<BotGroupMember>> FetchMembers(uint groupUin, bool refreshCache = false) =>
Collection.Business.CachingLogic.GetCachedMembers(groupUin, refreshCache);
public Task<List<BotGroup>> FetchGroups(bool refreshCache) =>
Collection.Business.CachingLogic.GetCachedGroups(refreshCache);
public async Task<MessageResult> SendMessage(MessageChain chain)
{
uint clientSeq = chain.ClientSequence;
ulong messageId = chain.MessageId;
var sendMessageEvent = SendMessageEvent.Create(chain);
var events = await Collection.Business.SendEvent(sendMessageEvent);
if (events.Count == 0) return new MessageResult { Result = 9057 };
var result = ((SendMessageEvent)events[0]).MsgResult;
result.ClientSequence = clientSeq;
result.MessageId = messageId;
return result;
}
public async Task<bool> MuteGroupMember(uint groupUin, uint targetUin, uint duration)
{
string? uid = await Collection.Business.CachingLogic.ResolveUid(groupUin, targetUin);
if (uid == null) return false;
var muteGroupMemberEvent = GroupMuteMemberEvent.Create(groupUin, duration, uid);
var events = await Collection.Business.SendEvent(muteGroupMemberEvent);
return events.Count != 0 && ((GroupMuteMemberEvent)events[0]).ResultCode == 0;
}
public async Task<bool> MuteGroupGlobal(uint groupUin, bool isMute)
{
var muteGroupMemberEvent = GroupMuteGlobalEvent.Create(groupUin, isMute);
var events = await Collection.Business.SendEvent(muteGroupMemberEvent);
return events.Count != 0 && ((GroupMuteGlobalEvent)events[0]).ResultCode == 0;
}
public async Task<bool> KickGroupMember(uint groupUin, uint targetUin, bool rejectAddRequest, string reason)
{
string? uid = await Collection.Business.CachingLogic.ResolveUid(groupUin, targetUin);
if (uid == null) return false;
var muteGroupMemberEvent = GroupKickMemberEvent.Create(groupUin, uid, rejectAddRequest, reason);
var events = await Collection.Business.SendEvent(muteGroupMemberEvent);
return events.Count != 0 && ((GroupKickMemberEvent)events[0]).ResultCode == 0;
}
public async Task<bool> SetGroupAdmin(uint groupUin, uint targetUin, bool isAdmin)
{
string? uid = await Collection.Business.CachingLogic.ResolveUid(groupUin, targetUin);
if (uid == null) return false;
var setGroupAdminEvent = GroupSetAdminEvent.Create(groupUin, uid, isAdmin);
var events = await Collection.Business.SendEvent(setGroupAdminEvent);
return events.Count != 0 && ((GroupSetAdminEvent)events[0]).ResultCode == 0;
}
public async Task<(int, string?)> SetGroupTodo(uint groupUin, uint sequence)
{
var setGroupTodoEvent = GroupSetTodoEvent.Create(groupUin, sequence);
var events = await Collection.Business.SendEvent(setGroupTodoEvent);
if (events.Count == 0) return (-1, "No Events");
var @event = (GroupSetTodoEvent)events[0];
return (@event.ResultCode, @event.ResultMessage);
}
public async Task<(int, string?)> RemoveGroupTodo(uint groupUin)
{
var setGroupTodoEvent = GroupRemoveTodoEvent.Create(groupUin);
var events = await Collection.Business.SendEvent(setGroupTodoEvent);
if (events.Count == 0) return (-1, "No Event");
var @event = (GroupRemoveTodoEvent)events[0];
return (@event.ResultCode, @event.ResultMessage);
}
public async Task<(int, string?)> FinishGroupTodo(uint groupUin)
{
var setGroupTodoEvent = GroupFinishTodoEvent.Create(groupUin);
var events = await Collection.Business.SendEvent(setGroupTodoEvent);
if (events.Count == 0) return (-1, "No Event");
var @event = (GroupFinishTodoEvent)events[0];
return (@event.ResultCode, @event.ResultMessage);
}
public async Task<BotGetGroupTodoResult> GetGroupTodo(uint groupUin)
{
var setGroupTodoEvent = GroupGetTodoEvent.Create(groupUin);
var events = await Collection.Business.SendEvent(setGroupTodoEvent);
if (events.Count == 0) return new(-1, "No Event", 0, 0, string.Empty);
var @event = (GroupGetTodoEvent)events[0];
return new(
@event.ResultCode,
@event.ResultMessage,
@event.GroupUin,
@event.Sequence,
@event.Preview
);
}
public async Task<bool> SetGroupBot(uint BotId, uint On, uint groupUin)
{
var muteBotEvent = GroupSetBotEvent.Create(BotId, On, groupUin);
var events = await Collection.Business.SendEvent(muteBotEvent);
return events.Count != 0 && ((GroupSetBotEvent)events[0]).ResultCode == 0;
}
public async Task<bool> SetGroupBotHD(uint BotId, uint groupUin, string? data_1, string? data_2)
{
var muteBotEvent = GroupSetBothdEvent.Create(BotId, groupUin, data_1, data_2);
var events = await Collection.Business.SendEvent(muteBotEvent);
return events.Count != 0 && ((GroupSetBothdEvent)events[0]).ResultCode == 0;
}
public async Task<bool> RenameGroupMember(uint groupUin, uint targetUin, string targetName)
{
string? uid = await Collection.Business.CachingLogic.ResolveUid(groupUin, targetUin);
if (uid == null) return false;
var renameGroupEvent = RenameMemberEvent.Create(groupUin, uid, targetName);
var events = await Collection.Business.SendEvent(renameGroupEvent);
return events.Count != 0 && ((RenameMemberEvent)events[0]).ResultCode == 0;
}
public async Task<bool> RenameGroup(uint groupUin, string targetName)
{
var renameGroupEvent = GroupRenameEvent.Create(groupUin, targetName);
var events = await Collection.Business.SendEvent(renameGroupEvent);
return events.Count != 0 && ((GroupRenameEvent)events[0]).ResultCode == 0;
}
public async Task<bool> RemarkGroup(uint groupUin, string targetRemark)
{
var renameGroupEvent = GroupRemarkEvent.Create(groupUin, targetRemark);
var events = await Collection.Business.SendEvent(renameGroupEvent);
return events.Count != 0 && ((GroupRemarkEvent)events[0]).ResultCode == 0;
}
public async Task<bool> LeaveGroup(uint groupUin)
{
var leaveGroupEvent = GroupLeaveEvent.Create(groupUin);
var events = await Collection.Business.SendEvent(leaveGroupEvent);
return events.Count != 0 && ((GroupLeaveEvent)events[0]).ResultCode == 0;
}
public async Task<ulong> FetchGroupFSSpace(uint groupUin)
{
var groupFSSpaceEvent = GroupFSSpaceEvent.Create(groupUin);
var events = await Collection.Business.SendEvent(groupFSSpaceEvent);
return ((GroupFSSpaceEvent)events[0]).TotalSpace - ((GroupFSSpaceEvent)events[0]).UsedSpace;
}
public async Task<uint> FetchGroupFSCount(uint groupUin)
{
var groupFSSpaceEvent = GroupFSCountEvent.Create(groupUin);
var events = await Collection.Business.SendEvent(groupFSSpaceEvent);
return ((GroupFSCountEvent)events[0]).FileCount;
}
public async Task<List<IBotFSEntry>> FetchGroupFSList(uint groupUin, string targetDirectory)
{
uint startIndex = 0;
var entries = new List<IBotFSEntry>();
while (true)
{
var groupFSListEvent = GroupFSListEvent.Create(groupUin, targetDirectory, startIndex, 20);
var events = await Collection.Business.SendEvent(groupFSListEvent);
if (events.Count == 0) break;
entries.AddRange(((GroupFSListEvent)events[0]).FileEntries);
if (((GroupFSListEvent)events[0]).IsEnd) break;
startIndex += 20;
}
return entries;
}
public async Task<string> FetchGroupFSDownload(uint groupUin, string fileId)
{
var groupFSDownloadEvent = GroupFSDownloadEvent.Create(groupUin, fileId);
var events = await Collection.Business.SendEvent(groupFSDownloadEvent);
return $"{((GroupFSDownloadEvent)events[0]).FileUrl}{fileId}";
}
public async Task<string> FetchPrivateFSDownload(string fileId, string fileHash, uint userId)
{
var uid = await Collection.Business.CachingLogic.ResolveUid(null, userId);
if (uid == null) return "false";
var privateFSDownloadEvent = FileDownloadEvent.Create(fileId, fileHash, uid, uid);
var events = await Collection.Business.SendEvent(privateFSDownloadEvent);
return $"{((FileDownloadEvent)events[0]).FileUrl}";
}
public async Task<(int, string)> GroupFSMove(uint groupUin, string fileId, string parentDirectory, string targetDirectory)
{
var groupFSMoveEvent = GroupFSMoveEvent.Create(groupUin, fileId, parentDirectory, targetDirectory);
var events = await Collection.Business.SendEvent(groupFSMoveEvent);
int retCode = events.Count > 0 ? ((GroupFSMoveEvent)events[0]).ResultCode : -1;
string retMsg = events.Count > 0 ? ((GroupFSMoveEvent)events[0]).RetMsg : string.Empty;
return (retCode, retMsg);
}
public async Task<(int, string)> GroupFSDelete(uint groupUin, string fileId)
{
var groupFSDeleteEvent = GroupFSDeleteEvent.Create(groupUin, fileId);
var events = await Collection.Business.SendEvent(groupFSDeleteEvent);
int retCode = events.Count > 0 ? ((GroupFSDeleteEvent)events[0]).ResultCode : -1;
string retMsg = events.Count > 0 ? ((GroupFSDeleteEvent)events[0]).RetMsg : string.Empty;
return (retCode, retMsg);
}
public async Task<(int, string)> GroupFSCreateFolder(uint groupUin, string name)
{
var groupFSCreateFolderEvent = GroupFSCreateFolderEvent.Create(groupUin, name);
var events = await Collection.Business.SendEvent(groupFSCreateFolderEvent);
int retCode = events.Count > 0 ? ((GroupFSCreateFolderEvent)events[0]).ResultCode : -1;
string retMsg = events.Count > 0 ? ((GroupFSCreateFolderEvent)events[0]).RetMsg : string.Empty;
return (retCode, retMsg);
}
public async Task<(int, string)> GroupFSDeleteFolder(uint groupUin, string folderId)
{
var groupFSDeleteFolderEvent = GroupFSDeleteFolderEvent.Create(groupUin, folderId);
var events = await Collection.Business.SendEvent(groupFSDeleteFolderEvent);
int retCode = events.Count > 0 ? ((GroupFSDeleteFolderEvent)events[0]).ResultCode : -1;
string retMsg = events.Count > 0 ? ((GroupFSDeleteFolderEvent)events[0]).RetMsg : string.Empty;
return (retCode, retMsg);
}
public async Task<(int, string)> GroupFSRenameFolder(uint groupUin, string folderId, string newFolderName)
{
var groupFSDeleteFolderEvent = GroupFSRenameFolderEvent.Create(groupUin, folderId, newFolderName);
var events = await Collection.Business.SendEvent(groupFSDeleteFolderEvent);
int retCode = events.Count > 0 ? ((GroupFSRenameFolderEvent)events[0]).ResultCode : -1;
string retMsg = events.Count > 0 ? ((GroupFSRenameFolderEvent)events[0]).RetMsg : "";
return (retCode, retMsg);
}
public Task<bool> GroupFSUpload(uint groupUin, FileEntity fileEntity, string targetDirectory)
{
try
{
return FileUploader.UploadGroup(Collection, MessageBuilder.Group(groupUin).Build(), fileEntity, targetDirectory);
}
catch
{
return Task.FromResult(false);
}
}
public async Task<bool> UploadFriendFile(uint targetUin, FileEntity fileEntity)
{
string? uid = await Collection.Business.CachingLogic.ResolveUid(null, targetUin);
var chain = new MessageChain(targetUin, Collection.Keystore.Uid ?? "", uid ?? "") { fileEntity };
try
{
return await FileUploader.UploadPrivate(Collection, chain, fileEntity);
}
catch
{
return false;
}
}
public async Task<bool> RecallGroupMessage(uint groupUin, MessageResult result)
{
if (result.Sequence == null) return false;
var recallMessageEvent = RecallGroupMessageEvent.Create(groupUin, result.Sequence.Value);
var events = await Collection.Business.SendEvent(recallMessageEvent);
return events.Count != 0 && ((RecallGroupMessageEvent)events[0]).ResultCode == 0;
}
public async Task<bool> RecallGroupMessage(MessageChain chain)
{
if (chain.GroupUin == null) return false;
var recallMessageEvent = RecallGroupMessageEvent.Create(chain.GroupUin.Value, chain.Sequence);
var events = await Collection.Business.SendEvent(recallMessageEvent);
return events.Count != 0 && ((RecallGroupMessageEvent)events[0]).ResultCode == 0;
}
public async Task<bool> RecallGroupMessage(uint groupUin, uint sequence)
{
var recallMessageEvent = RecallGroupMessageEvent.Create(groupUin, sequence);
var events = await Collection.Business.SendEvent(recallMessageEvent);
return events.Count != 0 && ((RecallGroupMessageEvent)events[0]).ResultCode == 0;
}
public async Task<bool> RecallFriendMessage(uint friendUin, MessageResult result)
{
if (result.Sequence == null) return false;
if (await Collection.Business.CachingLogic.ResolveUid(null, friendUin) is not { } uid) return false;
var recallMessageEvent = RecallFriendMessageEvent.Create(uid, result.ClientSequence, result.Sequence ?? 0, (uint)(result.MessageId & uint.MaxValue), result.Timestamp);
var events = await Collection.Business.SendEvent(recallMessageEvent);
return events.Count != 0 && ((RecallFriendMessageEvent)events[0]).ResultCode == 0;
}
public async Task<bool> RecallFriendMessage(MessageChain chain)
{
if (await Collection.Business.CachingLogic.ResolveUid(null, chain.TargetUin) is not { } uid) return false;
uint timestamp = (uint)new DateTimeOffset(chain.Time).ToUnixTimeSeconds();
var recallMessageEvent = RecallFriendMessageEvent.Create(uid, chain.ClientSequence, chain.Sequence, (uint)(chain.MessageId & uint.MaxValue), timestamp);
var events = await Collection.Business.SendEvent(recallMessageEvent);
return events.Count != 0 && ((RecallFriendMessageEvent)events[0]).ResultCode == 0;
}
public async Task<List<BotGroupRequest>?> FetchGroupRequests()
{
var fetchRequestsEvent = FetchGroupRequestsEvent.Create();
var events = await Collection.Business.SendEvent(fetchRequestsEvent);
if (events.Count == 0) return null;
var resolved = events.Cast<FetchGroupRequestsEvent>().SelectMany(e => e.Events).ToList();
var results = new List<BotGroupRequest>();
foreach (var result in resolved)
{
var uins = await Task.WhenAll(ResolveUid(result.InvitorMemberUid), ResolveUid(result.TargetMemberUid),
ResolveUid(result.OperatorUid));
uint invitorUin = uins[0];
uint targetUin = uins[1];
uint operatorUin = uins[2];
results.Add(new BotGroupRequest(
result.GroupUin,
invitorUin,
result.InvitorMemberCard,
targetUin,
result.TargetMemberCard,
operatorUin,
result.OperatorName,
result.State,
result.Sequence,
result.EventType,
result.Comment,
result.IsFiltered
));
}
return results;
async Task<uint> ResolveUid(string? uid)
{
if (uid == null) return 0;
var fetchUidEvent = FetchUserInfoEvent.Create(uid);
var e = await Collection.Business.SendEvent(fetchUidEvent);
return e.Count == 0 ? 0 : ((FetchUserInfoEvent)e[0]).UserInfo.Uin;
}
}
public async Task<List<BotFriendRequest>?> FetchFriendRequests()
{
var fetchRequestsEvent = FetchFriendsRequestsEvent.Create();
var events = await Collection.Business.SendEvent(fetchRequestsEvent);
if (events.Count == 0) return null;
var resolved = ((FetchFriendsRequestsEvent)events[0]).Requests;
foreach (var result in resolved)
{
var uins = await Task.WhenAll(ResolveUid(result.TargetUid), ResolveUid(result.SourceUid));
result.TargetUin = uins[0];
result.SourceUin = uins[1];
}
return resolved;
async Task<uint> ResolveUid(string? uid)
{
if (uid == null) return 0;
var fetchUidEvent = FetchUserInfoEvent.Create(uid);
var e = await Collection.Business.SendEvent(fetchUidEvent);
return e.Count == 0 ? 0 : ((FetchUserInfoEvent)e[0]).UserInfo.Uin;
}
}
public async Task<bool> GroupTransfer(uint groupUin, uint targetUin)
{
string? uid = await Collection.Business.CachingLogic.ResolveUid(groupUin, targetUin);
if (uid == null || Collection.Keystore.Uid is not { } source) return false;
var transferEvent = GroupTransferEvent.Create(groupUin, source, uid);
var results = await Collection.Business.SendEvent(transferEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<bool> SetStatus(uint status)
{
var setStatusEvent = SetStatusEvent.Create(status, 0);
var results = await Collection.Business.SendEvent(setStatusEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<bool> SetCustomStatus(uint faceId, string text)
{
var setCustomStatusEvent = SetCustomStatusEvent.Create(faceId, text);
var results = await Collection.Business.SendEvent(setCustomStatusEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<bool> DeleteFriend(uint targetUin, bool block)
{
var uid = await Collection.Business.CachingLogic.ResolveUid(null, targetUin);
var deleteFriendEvent = DeleteFriendEvent.Create(uid, block);
var results = await Collection.Business.SendEvent(deleteFriendEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<bool> RequestFriend(uint targetUin, string question, string message)
{
var requestFriendSearchEvent = RequestFriendSearchEvent.Create(targetUin);
var searchEvents = await Collection.Business.SendEvent(requestFriendSearchEvent);
if (searchEvents.Count == 0) return false;
await Task.Delay(5000);
var requestFriendSettingEvent = RequestFriendSettingEvent.Create(targetUin);
var settingEvents = await Collection.Business.SendEvent(requestFriendSettingEvent);
if (settingEvents.Count == 0) return false;
var requestFriendEvent = RequestFriendEvent.Create(targetUin, message, question);
var events = await Collection.Business.SendEvent(requestFriendEvent);
return events.Count != 0 && ((RequestFriendEvent)events[0]).ResultCode == 0;
}
public async Task<bool> Like(uint targetUin, uint count)
{
var uid = await Collection.Business.CachingLogic.ResolveUid(null, targetUin);
if (uid == null) return false;
var friendLikeEvent = FriendLikeEvent.Create(uid, count);
var results = await Collection.Business.SendEvent(friendLikeEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<bool> InviteGroup(uint targetGroupUin, Dictionary<uint, uint?> invitedUins)
{
var invitedUids = new Dictionary<string, uint?>(invitedUins.Count);
foreach (var (friendUin, groupUin) in invitedUins)
{
string? uid = await Collection.Business.CachingLogic.ResolveUid(groupUin, friendUin);
if (uid != null) invitedUids[uid] = groupUin;
}
var @event = GroupInviteEvent.Create(targetGroupUin, invitedUids);
var results = await Collection.Business.SendEvent(@event);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<string?> GetClientKey()
{
var clientKeyEvent = FetchClientKeyEvent.Create();
var events = await Collection.Business.SendEvent(clientKeyEvent);
return events.Count == 0 ? null : ((FetchClientKeyEvent)events[0]).ClientKey;
}
public async Task<bool> SetGroupRequest(uint groupUin, ulong sequence, uint type, bool accept, string reason)
{
var inviteEvent = SetGroupRequestEvent.Create(accept, groupUin, sequence, type, reason);
var results = await Collection.Business.SendEvent(inviteEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<bool> SetGroupFilteredRequest(uint groupUin, ulong sequence, uint type, bool accept, string reason)
{
var inviteEvent = SetGroupFilteredRequestEvent.Create(accept, groupUin, sequence, type, reason);
var results = await Collection.Business.SendEvent(inviteEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<bool> SetFriendRequest(string targetUid, bool accept)
{
var inviteEvent = SetFriendRequestEvent.Create(targetUid, accept);
var results = await Collection.Business.SendEvent(inviteEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<List<MessageChain>?> GetGroupMessage(uint groupUin, uint startSequence, uint endSequence)
{
var getMsgEvent = GetGroupMessageEvent.Create(groupUin, startSequence, endSequence);
var results = await Collection.Business.SendEvent(getMsgEvent);
return results.Count != 0 ? ((GetGroupMessageEvent)results[0]).Chains : null;
}
public async Task<List<MessageChain>?> GetRoamMessage(uint friendUin, uint time, uint count)
{
if (await Collection.Business.CachingLogic.ResolveUid(null, friendUin) is not { } uid) return null;
var roamEvent = GetRoamMessageEvent.Create(uid, time, count);
var results = await Collection.Business.SendEvent(roamEvent);
return results.Count != 0 ? ((GetRoamMessageEvent)results[0]).Chains : null;
}
public async Task<List<MessageChain>?> GetC2cMessage(uint friendUin, uint startSequence, uint endSequence)
{
if (await Collection.Business.CachingLogic.ResolveUid(null, friendUin) is not { } uid) return null;
var c2cEvent = GetC2cMessageEvent.Create(uid, startSequence, endSequence);
var results = await Collection.Business.SendEvent(c2cEvent);
return results.Count != 0 ? ((GetC2cMessageEvent)results[0]).Chains : null;
}
public async Task<(int code, List<MessageChain>? chains)> GetMessagesByResId(string resId)
{
var @event = MultiMsgDownloadEvent.Create(Collection.Keystore.Uid ?? "", resId);
var results = await Collection.Business.SendEvent(@event);
if (results.Count == 0) return (-9999, null);
var result = (MultiMsgDownloadEvent)results[0];
return (result.ResultCode, result.Chains);
}
public async Task<List<string>?> FetchCustomFace()
{
var fetchCustomFaceEvent = FetchCustomFaceEvent.Create();
var results = await Collection.Business.SendEvent(fetchCustomFaceEvent);
return results.Count != 0 ? ((FetchCustomFaceEvent)results[0]).Urls : null;
}
public async Task<string?> UploadLongMessage(List<MessageChain> chains)
{
var multiMsgUploadEvent = MultiMsgUploadEvent.Create(null, chains);
var results = await Collection.Business.SendEvent(multiMsgUploadEvent);
return results.Count != 0 ? ((MultiMsgUploadEvent)results[0]).ResId : null;
}
public async Task<bool> MarkAsRead(uint groupUin, string? targetUid, uint startSequence, uint time)
{
var markAsReadEvent = MarkReadedEvent.Create(groupUin, targetUid, startSequence, time);
var results = await Collection.Business.SendEvent(markAsReadEvent);
return results.Count != 0 && ((MarkReadedEvent)results[0]).ResultCode == 0;
}
public async Task<bool> FriendPoke(uint friendUin)
{
var friendPokeEvent = FriendPokeEvent.Create(friendUin);
var results = await Collection.Business.SendEvent(friendPokeEvent);
return results.Count != 0 && ((FriendPokeEvent)results[0]).ResultCode == 0;
}
public async Task<bool> GroupPoke(uint groupUin, uint friendUin)
{
var friendPokeEvent = GroupPokeEvent.Create(friendUin, groupUin);
var results = await Collection.Business.SendEvent(friendPokeEvent);
return results.Count != 0 && ((FriendPokeEvent)results[0]).ResultCode == 0;
}
public async Task<bool> SetEssenceMessage(uint groupUin, uint sequence, uint random)
{
var setEssenceMessageEvent = SetEssenceMessageEvent.Create(groupUin, sequence, random);
var results = await Collection.Business.SendEvent(setEssenceMessageEvent);
return results.Count != 0 && ((SetEssenceMessageEvent)results[0]).ResultCode == 0;
}
public async Task<bool> RemoveEssenceMessage(uint groupUin, uint sequence, uint random)
{
var removeEssenceMessageEvent = RemoveEssenceMessageEvent.Create(groupUin, sequence, random);
var results = await Collection.Business.SendEvent(removeEssenceMessageEvent);
return results.Count != 0 && ((RemoveEssenceMessageEvent)results[0]).ResultCode == 0;
}
public async Task<bool> GroupSetSpecialTitle(uint groupUin, uint targetUin, string title)
{
string? uid = await Collection.Business.CachingLogic.ResolveUid(groupUin, targetUin);
if (uid == null) return false;
var groupSetSpecialTitleEvent = GroupSetSpecialTitleEvent.Create(groupUin, uid, title);
var events = await Collection.Business.SendEvent(groupSetSpecialTitleEvent);
return events.Count != 0 && ((GroupSetSpecialTitleEvent)events[0]).ResultCode == 0;
}
public async Task<BotUserInfo?> FetchUserInfo(uint uin, bool refreshCache = false)
{
return await Collection.Business.CachingLogic.GetCachedUsers(uin, refreshCache);
}
public async Task<(int code, string? message, BotGroupInfo info)> FetchGroupInfo(ulong uin)
{
var events = await Collection.Business.SendEvent(GetGroupInfoEvent.Create(uin));
if (events.Count == 0) return (-1, "No Result", new());
var @event = (GetGroupInfoEvent)events[0];
return (@event.ResultCode, @event.Message, @event.Info);
}
public async Task<bool> SetMessageReaction(uint groupUin, uint sequence, string code, bool isAdd)
{
if (isAdd)
{
var addReactionEvent = GroupAddReactionEvent.Create(groupUin, sequence, code);
var results = await Collection.Business.SendEvent(addReactionEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
else
{
var reduceReactionEvent = GroupReduceReactionEvent.Create(groupUin, sequence, code);
var results = await Collection.Business.SendEvent(reduceReactionEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
}
public async Task<bool> SetNeedToConfirmSwitch(bool enableNoNeed)
{
var setNeedToConfirmSwitchEvent = SetNeedToConfirmSwitchEvent.Create(enableNoNeed);
var results = await Collection.Business.SendEvent(setNeedToConfirmSwitchEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<List<string>?> FetchMarketFaceKey(List<string> faceIds)
{
var fetchMarketFaceKeyEvent = FetchMarketFaceKeyEvent.Create(faceIds);
var results = await Collection.Business.SendEvent(fetchMarketFaceKeyEvent);
return results.Count != 0 ? ((FetchMarketFaceKeyEvent)results[0]).Keys : null;
}
public async Task<BotGroupClockInResult> GroupClockIn(uint groupUin)
{
var groupClockInEvent = GroupClockInEvent.Create(groupUin);
var results = await Collection.Business.SendEvent(groupClockInEvent);
return ((GroupClockInEvent)results[0]).ResultInfo ?? new BotGroupClockInResult(false);
}
public Task<MessageResult> FriendSpecialShake(uint friendUin, SpecialPokeFaceType type, uint count)
{
var chain = MessageBuilder.Friend(friendUin)
.SpecialPoke(type, count)
.Build();
return SendMessage(chain);
}
public Task<MessageResult> FriendShake(uint friendUin, PokeFaceType type, uint strength)
{
var chain = MessageBuilder.Friend(friendUin)
.Poke(type, strength)
.Build();
return SendMessage(chain);
}
public async Task<bool> SetAvatar(ImageEntity avatar)
{
if (avatar.ImageStream == null) return false;
var highwayUrlEvent = HighwayUrlEvent.Create();
var highwayUrlResults = await Collection.Business.SendEvent(highwayUrlEvent);
if (highwayUrlResults.Count == 0) return false;
var ticket = ((HighwayUrlEvent)highwayUrlResults[0]).SigSession;
var md5 = avatar.ImageStream.Value.Md5().UnHex();
return await Collection.Highway.UploadSrcByStreamAsync(90, avatar.ImageStream.Value, ticket, md5,
Array.Empty<byte>());
}
public async Task<bool> GroupSetAvatar(uint groupUin, ImageEntity avatar)
{
if (avatar.ImageStream == null) return false;
var highwayUrlEvent = HighwayUrlEvent.Create();
var highwayUrlResults = await Collection.Business.SendEvent(highwayUrlEvent);
if (highwayUrlResults.Count == 0) return false;
var ticket = ((HighwayUrlEvent)highwayUrlResults[0]).SigSession;
var md5 = avatar.ImageStream.Value.Md5().UnHex();
var extra = new GroupAvatarExtra
{
Type = 101,
GroupUin = groupUin,
Field3 = new GroupAvatarExtraField3 { Field1 = 1 },
Field5 = 3,
Field6 = 1
}.Serialize().ToArray();
return await Collection.Highway.UploadSrcByStreamAsync(3000, avatar.ImageStream.Value, ticket, md5, extra);
}
public async Task<(uint, uint)> GroupRemainAtAll(uint groupUin)
{
var groupRemainAtAllEvent = FetchGroupAtAllRemainEvent.Create(groupUin);
var results = await Collection.Business.SendEvent(groupRemainAtAllEvent);
if (results.Count == 0) return (0, 0);
var ret = (FetchGroupAtAllRemainEvent)results[0];
return (ret.RemainAtAllCountForUin, ret.RemainAtAllCountForGroup);
}
public async Task<bool> FetchSuperFaceId(uint id) =>
await Collection.Business.CachingLogic.GetCachedIsSuperFaceId(id);
public async Task<SysFaceEntry?> FetchFaceEntity(uint id) =>
await Collection.Business.CachingLogic.GetCachedFaceEntity(id);
public async Task<bool> GroupJoinEmojiChain(uint groupUin, uint emojiId, uint targetMessageSeq)
{
var groupJoinEmojiChainEvent = GroupJoinEmojiChainEvent.Create(targetMessageSeq, emojiId, groupUin);
var results = await Collection.Business.SendEvent(groupJoinEmojiChainEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<bool> FriendJoinEmojiChain(uint friendUin, uint emojiId, uint targetMessageSeq)
{
string? friendUid = await Collection.Business.CachingLogic.ResolveUid(null, friendUin);
if (friendUid == null) return false;
var friendJoinEmojiChainEvent = FriendJoinEmojiChainEvent.Create(targetMessageSeq, emojiId, friendUid);
var results = await Collection.Business.SendEvent(friendJoinEmojiChainEvent);
return results.Count != 0 && results[0].ResultCode == 0;
}
public async Task<(int Code, string ErrMsg, string? Url)> GetGroupGenerateAiRecordUrl(uint groupUin, string character, string text, uint chatType)
{
var (code, errMsg, record) = await GetGroupGenerateAiRecord(groupUin, character, text, chatType);
if (code != 0)
return (code, errMsg, null);
var recordGroupDownloadEvent = RecordGroupDownloadEvent.Create(groupUin, record!.MsgInfo!);
var @event = await Collection.Business.SendEvent(recordGroupDownloadEvent);
if (@event.Count == 0) return (-1, "running event missing!", null);
var finalResult = (RecordGroupDownloadEvent)@event[0];
return finalResult.ResultCode == 0
? (finalResult.ResultCode, string.Empty, finalResult.AudioUrl)
: (finalResult.ResultCode, "Failed to get group ai record", null);
}
public async Task<(int Code, string ErrMsg, RecordEntity? Record)> GetGroupGenerateAiRecord(uint groupUin, string character, string text, uint chatType)
{
var groupAiRecordEvent = GroupAiRecordEvent.Create(groupUin, character, text, chatType, (uint)Random.Shared.Next());
while (true)
{
var results = await Collection.Business.SendEvent(groupAiRecordEvent);
if (results.Count == 0) return (-1, "running event missing!", null);
var aiRecordResult = (GroupAiRecordEvent)results[0];
if (aiRecordResult.ResultCode != 0)
return (aiRecordResult.ResultCode, aiRecordResult.ErrorMessage, null);
if (aiRecordResult.RecordInfo is not null)
{
var index = aiRecordResult.RecordInfo!.MsgInfoBody[0].Index;
return (aiRecordResult.ResultCode, string.Empty, new RecordEntity(index.FileUuid, index.Info.FileName, index.Info.FileHash.UnHex())
{
AudioLength = (int)index.Info.Time,
FileSha1 = index.Info.FileSha1,
MsgInfo = aiRecordResult.RecordInfo
});
}
}
}
public async Task<(int Code, string ErrMsg, List<AiCharacterList>? Result)> GetAiCharacters(uint chatType, uint groupUin)
{
var fetchAiRecordListEvent = FetchAiCharacterListEvent.Create(chatType, groupUin);
var results = await Collection.Business.SendEvent(fetchAiRecordListEvent);
if (results.Count == 0) return (-1, "Event missing!", null);
var result = (FetchAiCharacterListEvent)results[0];
return (result.ResultCode, result.ErrorMessage, result.AiCharacters);
}
public async Task<string> UploadImage(ImageEntity image)
{
await Collection.Highway.ManualUploadEntity(image);
var msgInfo = image.MsgInfo;
if (msgInfo is null) throw new Exception();
var downloadEvent = ImageDownloadEvent.Create(Collection.Keystore.Uid ?? "", msgInfo);
var result = await Collection.Business.SendEvent(downloadEvent);
var ret = (ImageDownloadEvent)result[0];
return ret.ImageUrl;
}
public async Task<ImageOcrResult?> ImageOcr(string imageUrl)
{
var imageOcrEvent = ImageOcrEvent.Create(imageUrl);
var results = await Collection.Business.SendEvent(imageOcrEvent);
return results.Count != 0 ? ((ImageOcrEvent)results[0]).ImageOcrResult : null;
}
public async Task<ImageOcrResult?> ImageOcr(ImageEntity image)
{
var imageUrl = await UploadImage(image);
return await ImageOcr(imageUrl);
}
public async Task<(int Retcode, string Message, List<uint> FriendUins, List<uint> GroupUins)> GetPins()
{
var @event = FetchPinsEvent.Create();
var results = await Collection.Business.SendEvent(@event);
if (results.Count == 0)
{
return (-1, "No Result", new(), new());
}
var result = (FetchPinsEvent)results[0];
return (result.ResultCode, result.Message, result.FriendUins, result.GroupUins);
}
public async Task<(int Retcode, string Message)> SetPinFriend(uint uin, bool isPin)
{
var @event = SetPinFriendEvent.Create(uin, isPin);
var results = await Collection.Business.SendEvent(@event);
if (results.Count == 0)
{
return (-1, "No Result");
}
var result = (SetPinFriendEvent)results[0];
return (result.ResultCode, result.Message);
}
public async Task<(int Retcode, string Message)> SetPinGroup(uint uin, bool isPin)
{
var @event = SetPinGroupEvent.Create(uin, isPin);
var results = await Collection.Business.SendEvent(@event);
if (results.Count == 0)
{
return (-1, "No Result");
}
var result = (SetPinGroupEvent)results[0];
return (result.ResultCode, result.Message);
}
}

View File

@@ -0,0 +1,532 @@
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Web;
using Lagrange.Core.Common;
using Lagrange.Core.Event.EventArg;
using Lagrange.Core.Internal.Context.Attributes;
using Lagrange.Core.Internal.Event;
using Lagrange.Core.Internal.Event.Login;
using Lagrange.Core.Internal.Event.System;
using Lagrange.Core.Internal.Packets.Login.NTLogin;
using Lagrange.Core.Internal.Packets.Login.WtLogin.Entity;
using Lagrange.Core.Internal.Service;
using Lagrange.Core.Utility.Crypto;
using Lagrange.Core.Utility.Network;
// ReSharper disable AsyncVoidLambda
namespace Lagrange.Core.Internal.Context.Logic.Implementation;
[EventSubscribe(typeof(TransEmpEvent))]
[EventSubscribe(typeof(LoginEvent))]
[EventSubscribe(typeof(KickNTEvent))]
[BusinessLogic("WtExchangeLogic", "Manage the online task of the Bot")]
internal class WtExchangeLogic : LogicBase
{
private const string Tag = nameof(WtExchangeLogic);
private readonly Timer _reLoginTimer;
private TaskCompletionSource<bool> _transEmpTask = new();
private TaskCompletionSource<(string, string)> _captchaTask = new();
private const string Interface = "https://ntlogin.qq.com/qr/getFace";
private const string QueryEvent = "wtlogin.trans_emp CMD0x12";
private const string HeartbeatEvent = "Heartbeat.Alive";
private const string SsoHeartbeatEvent = "SsoHeartBeat";
internal WtExchangeLogic(ContextCollection collection) : base(collection)
{
_reLoginTimer = new Timer(async _ => await ReLogin(), null, Timeout.Infinite, Timeout.Infinite);
}
public override async Task Incoming(ProtocolEvent e)
{
switch (e)
{
case KickNTEvent kick:
Collection.Log.LogFatal(Tag, $"KickNTEvent: {kick.Tag}: {kick.Message}");
Collection.Log.LogFatal(Tag, "Bot will be offline in 5 seconds...");
await Task.Delay(5000);
Collection.Invoker.PostEvent(new BotOfflineEvent(kick.Tag, kick.Message)); // TODO: Fill in the reason of offline
Collection.Scheduler.Dispose();
break;
}
}
private void Reset()
{
_transEmpTask = new TaskCompletionSource<bool>();
_captchaTask = new TaskCompletionSource<(string, string)>();
}
private void OnCancellation()
{
Collection.Scheduler.Cancel(QueryEvent);
Collection.Scheduler.Cancel(HeartbeatEvent);
_transEmpTask.TrySetException(new TaskCanceledException());
_captchaTask.TrySetException(new TaskCanceledException());
}
/// <summary>
/// <para>1. resolve wtlogin.trans_emp CMD0x31 packet</para>
/// <para>2. Schedule wtlogin.trans_emp CMD0x12 Task</para>
/// </summary>
public async Task<(string, byte[])?> FetchQrCode()
{
Collection.Log.LogInfo(Tag, "Connecting Servers...");
if (!await Collection.Socket.Connect()) return null;
Collection.Scheduler.Interval(HeartbeatEvent, 10 * 1000, async () => await Collection.Business.PushEvent(AliveEvent.Create()));
if (Collection.Keystore.Session.D2.Length != 0)
{
Collection.Log.LogWarning(Tag, "Invalid Session found, try to clean D2Key, D2 and TGT Token");
Collection.Keystore.ClearSession();
}
var transEmp = TransEmpEvent.Create(TransEmpEvent.State.FetchQrCode);
var result = await Collection.Business.SendEvent(transEmp);
if (result.Count != 0)
{
var @event = (TransEmpEvent)result[0];
Collection.Keystore.Session.QrString = @event.QrSig;
Collection.Keystore.Session.QrSign = @event.Signature;
Collection.Keystore.Session.QrUrl = @event.Url;
Collection.Log.LogInfo(Tag, $"QrCode Fetched, Expiration: {@event.Expiration} seconds");
return (@event.Url, @event.QrCode);
}
return null;
}
public Task LoginByQrCode(CancellationToken cancellationToken)
{
Reset();
cancellationToken.Register(OnCancellation);
Collection.Scheduler.Interval(QueryEvent, 2 * 1000, async () => await QueryTransEmpState(async @event =>
{
if (@event.TgtgtKey != null)
{
Collection.Keystore.Stub.TgtgtKey = @event.TgtgtKey;
Collection.Keystore.Session.TempPassword = @event.TempPassword;
Collection.Keystore.Session.NoPicSig = @event.NoPicSig;
}
return await DoWtLogin();
}));
return _transEmpTask.Task;
}
public Task<bool> LoginByPassword(CancellationToken token) => LoginByEasy(true, token);
public async Task<bool> LoginByEasy(bool easyFallbackToPassword, CancellationToken cancellationToken)
{
Reset();
cancellationToken.Register(OnCancellation);
if (!Collection.Socket.Connected) // if socket not connected, try to connect
{
if (!await Collection.Socket.Connect()) return false;
Collection.Scheduler.Interval(HeartbeatEvent, 10 * 1000, async () => await Collection.Business.PushEvent(AliveEvent.Create()));
}
if (Collection.Keystore.Session.D2.Length > 0 && Collection.Keystore.Session.Tgt.Length > 0 &&
DateTime.Now - Collection.Keystore.Session.SessionDate < TimeSpan.FromDays(15))
{
Collection.Log.LogInfo(Tag, "Session has not expired, using session to login and register status");
try
{
if (await BotOnline()) return true;
Collection.Log.LogWarning(Tag, "Register by session failed, try to login by EasyLogin");
}
catch
{
Collection.Log.LogWarning(Tag, "Register by session failed, try to login by EasyLogin");
}
}
if (Collection.Keystore.Session.ExchangeKey == null)
{
Collection.Keystore.ClearSession();
if (!await KeyExchange())
{
Collection.Log.LogInfo(Tag, "Key Exchange Failed, please try again later");
return false;
}
}
if (Collection.Keystore.Session.TempPassword != null) // try EasyLogin
{
Collection.Log.LogInfo(Tag, "Trying to Login by EasyLogin...");
var easyLoginEvent = EasyLoginEvent.Create();
var easyLoginResult = await Collection.Business.SendEvent(easyLoginEvent);
if (easyLoginResult.Count != 0)
{
switch ((LoginCommon.Error)easyLoginResult[0].ResultCode)
{
case LoginCommon.Error.Success:
{
Collection.Log.LogInfo(Tag, "Login Success, try to register services");
return await BotOnline();
}
case LoginCommon.Error.UnusualVerify:
{
Collection.Log.LogInfo(Tag, "Verification needed");
if (!await FetchUnusual())
{
Collection.Log.LogInfo(Tag, "Fetch unusual state failed");
return false;
}
Collection.Scheduler.Interval(QueryEvent, 2 * 1000, async () => await QueryTransEmpState(async e =>
{
if (e.TempPassword != null)
{
Collection.Keystore.Session.TempPassword = e.TempPassword;
return await DoUnusualEasyLogin();
}
return false;
}));
bool result = await _transEmpTask.Task;
return result && await BotOnline();
}
default:
{
Collection.Log.LogWarning(Tag, $"Fast Login Failed with code {easyLoginResult[0].ResultCode}, trying to Login by Password...");
Collection.Keystore.Session.TempPassword = null; // clear temp password
return easyFallbackToPassword && await LoginByPassword(cancellationToken); // fallback to password
}
}
}
}
else if (easyFallbackToPassword)
{
Collection.Log.LogInfo(Tag, "Trying to Login by Password...");
var passwordLoginEvent = PasswordLoginEvent.Create();
var passwordLoginResult = await Collection.Business.SendEvent(passwordLoginEvent);
if (passwordLoginResult.Count != 0)
{
var @event = (PasswordLoginEvent)passwordLoginResult[0];
switch ((LoginCommon.Error)@event.ResultCode)
{
case LoginCommon.Error.Success:
{
Collection.Log.LogInfo(Tag, "Login Success");
await BotOnline();
return true;
}
case LoginCommon.Error.UnusualVerify:
{
Collection.Log.LogInfo(Tag, "Unusual Verify is not currently supported for PasswordLogin");
return false;
}
case LoginCommon.Error.CaptchaVerify:
{
Collection.Log.LogInfo(Tag, "Login captcha is required, please follow the link from event");
if (Collection.Keystore.Session.CaptchaUrl != null)
{
var captchaEvent = new BotCaptchaEvent(Collection.Keystore.Session.CaptchaUrl);
Collection.Invoker.PostEvent(captchaEvent);
string aid = Collection.Keystore.Session.CaptchaUrl.Split("&sid=")[1].Split("&")[0];
var (ticket, randStr) = await _captchaTask.Task;
Collection.Keystore.Session.Captcha = new ValueTuple<string, string, string>(ticket, randStr, aid);
return await LoginByPassword(cancellationToken);
}
Collection.Log.LogInfo(Tag, "Captcha Url is null, please try again later");
return false;
}
case LoginCommon.Error.NewDeviceVerify:
{
Collection.Log.LogInfo(Tag, $"NewDeviceVerify required, please notice the {nameof(BotNewDeviceVerifyEvent)} and encode into QRCode");
string? parameters = Collection.Keystore.Session.NewDeviceVerifyUrl;
if (parameters == null) return false;
var parsed = HttpUtility.ParseQueryString(parameters);
uint uin = Collection.Keystore.Uin;
string url = $"https://oidb.tim.qq.com/v3/oidbinterface/oidb_0xc9e_8?uid={uin}&getqrcode=1&sdkappid=39998&actype=2";
var request = new NTNewDeviceQrCodeRequest
{
StrDevAuthToken = parsed["sig"] ?? "",
Uint32Flag = 1,
Uint32UrlType = 0,
StrUinToken = parsed["uin-token"] ?? "",
StrDevType = Collection.AppInfo.Os,
StrDevName = Collection.Device.DeviceName
};
var client = new HttpClient();
var response = await client.PostAsJsonAsync(url, request, cancellationToken);
var json = await response.Content.ReadFromJsonAsync<NTNewDeviceQrCodeResponse>(cancellationToken: cancellationToken);
if (json == null) return false;
var newDeviceEvent = new BotNewDeviceVerifyEvent(json.StrUrl, Array.Empty<byte>());
Collection.Invoker.PostEvent(newDeviceEvent);
Collection.Log.LogInfo(Tag, $"NewDeviceLogin Url: {json.StrUrl}");
string? original = HttpUtility.ParseQueryString(json.StrUrl.Split("?")[1])["str_url"];
if (original == null) return false;
Collection.Scheduler.Interval(QueryEvent, 2 * 1000, async () =>
{
var query = new NTNewDeviceQrCodeQuery
{
Uint32Flag = 0,
Token = Convert.ToBase64String(Encoding.UTF8.GetBytes(original))
};
var resp = await client.PostAsJsonAsync(url, query, cancellationToken);
var responseJson = await resp.Content.ReadFromJsonAsync<NTNewDeviceQrCodeResponse>(cancellationToken: cancellationToken);
if (!string.IsNullOrEmpty(responseJson?.StrNtSuccToken))
{
Collection.Scheduler.Cancel(QueryEvent); // cancel the event
Collection.Keystore.Session.TempPassword = Encoding.UTF8.GetBytes(responseJson.StrNtSuccToken);
_transEmpTask.SetResult(true);
client.Dispose();
}
else
{
Collection.Log.LogInfo(Tag, "NewDeviceLogin is waiting for scanning");
}
});
if (await _transEmpTask.Task)
{
Collection.Log.LogInfo(Tag, "Trying to Login by NewDeviceLogin...");
var newDeviceLogin = NewDeviceLoginEvent.Create();
_ = await Collection.Business.SendEvent(newDeviceLogin);
return await BotOnline();
}
return false;
}
default:
{
Collection.Log.LogWarning(Tag, @event is { Message: not null, Tag: not null }
? $"Login Failed: {(LoginCommon.Error)@event.ResultCode} | {@event.Tag}: {@event.Message}"
: $"Login Failed: {(LoginCommon.Error)@event.ResultCode}");
return false;
}
}
}
}
return false;
}
private async Task<bool> KeyExchange()
{
var keyExchangeEvent = KeyExchangeEvent.Create();
var exchangeResult = await Collection.Business.SendEvent(keyExchangeEvent);
if (exchangeResult.Count != 0)
{
Collection.Log.LogInfo(Tag, "Key Exchange successfully!");
return true;
}
return false;
}
private async Task<bool> DoWtLogin()
{
Collection.Log.LogInfo(Tag, "Doing Login...");
Collection.Keystore.Session.Sequence = 0;
Collection.Keystore.SecpImpl = new EcdhImpl(EcdhImpl.CryptMethod.Secp192K1);
var loginEvent = LoginEvent.Create();
var result = await Collection.Business.SendEvent(loginEvent);
if (result.Count != 0)
{
var @event = (LoginEvent)result[0];
if (@event.ResultCode == 0)
{
Collection.Log.LogInfo(Tag, "Login Success");
Collection.Keystore.Info = new BotKeystore.BotInfo(@event.Age, @event.Sex, @event.Name);
Collection.Log.LogInfo(Tag, Collection.Keystore.Info.ToString());
return await BotOnline();
}
Collection.Log.LogFatal(Tag, $"Login failed: {@event.ResultCode}");
Collection.Log.LogFatal(Tag, $"Tag: {@event.Tag}\nState: {@event.Message}");
}
return false;
}
private async Task QueryTransEmpState(Func<TransEmpEvent, Task<bool>> callback)
{
if (Collection.Keystore.Session.QrString != null)
{
var request = new NTLoginHttpRequest
{
Appid = Collection.AppInfo.AppId,
Qrsig = Collection.Keystore.Session.QrString,
FaceUpdateTime = 0
};
var payload = JsonSerializer.SerializeToUtf8Bytes(request);
var response = await Http.PostAsync(Interface, payload, "application/json");
var info = JsonSerializer.Deserialize<NTLoginHttpResponse>(response);
if (info != null) Collection.Keystore.Uin = info.Uin;
}
var transEmp = TransEmpEvent.Create(TransEmpEvent.State.QueryResult);
var result = await Collection.Business.SendEvent(transEmp);
if (result.Count != 0)
{
var @event = (TransEmpEvent)result[0];
var state = (TransEmp12.State)@event.ResultCode;
Collection.Log.LogInfo(Tag, $"QrCode State Queried: {state} Uin: {Collection.Keystore.Uin}");
switch (state)
{
case TransEmp12.State.Confirmed:
{
Collection.Log.LogInfo(Tag, "QrCode Confirmed, Logging in with A1 sig...");
Collection.Scheduler.Cancel(QueryEvent); // cancel query task
_transEmpTask.SetResult(await callback.Invoke(@event));
break;
}
case TransEmp12.State.CodeExpired:
{
Collection.Log.LogWarning(Tag, "QrCode Expired, Please Fetch QrCode Again");
Collection.Scheduler.Cancel(QueryEvent);
_transEmpTask.SetResult(false);
return;
}
case TransEmp12.State.Canceled:
{
Collection.Log.LogWarning(Tag, "QrCode Canceled, Please Fetch QrCode Again");
Collection.Scheduler.Cancel(QueryEvent);
_transEmpTask.SetResult(false);
return;
}
case TransEmp12.State.WaitingForConfirm:
case TransEmp12.State.WaitingForScan:
default:
break;
}
}
}
public async Task<bool> BotOnline(BotOnlineEvent.OnlineReason reason = BotOnlineEvent.OnlineReason.Login)
{
var registerEvent = InfoSyncEvent.Create();
var registerResponse = await Collection.Business.SendEvent(registerEvent);
var heartbeatDelegate = new Action(async () => await Collection.Business.PushEvent(SsoAliveEvent.Create()));
if (registerResponse.Count != 0)
{
var resp = (InfoSyncEvent)registerResponse[0];
Collection.Log.LogInfo(Tag, $"Register Status: {resp.Message}");
bool result = resp.Message.Contains("register success");
if (result)
{
Collection.Scheduler.Interval(SsoHeartbeatEvent, (int)(4.5 * 60 * 1000), heartbeatDelegate);
var onlineEvent = new BotOnlineEvent(reason);
Collection.Invoker.PostEvent(onlineEvent);
_reLoginTimer.Change(TimeSpan.FromDays(15), TimeSpan.FromDays(15));
Collection.Log.LogInfo(Tag, "AutoReLogin Enabled, session would be refreshed in 15 days period");
}
return result;
}
return false;
}
private async Task<bool> FetchUnusual()
{
var transEmp = TransEmpEvent.Create(TransEmpEvent.State.FetchQrCode);
var result = await Collection.Business.SendEvent(transEmp);
if (result.Count != 0)
{
Collection.Log.LogInfo(Tag, "Confirmation Request Send");
return true;
}
return false;
}
private async Task<bool> DoUnusualEasyLogin()
{
Collection.Log.LogInfo(Tag, "Trying to Login by EasyLogin...");
var unusualEvent = UnusualEasyLoginEvent.Create();
var result = await Collection.Business.SendEvent(unusualEvent);
return result.Count != 0 && ((UnusualEasyLoginEvent)result[0]).Success;
}
private async Task ReLogin()
{
Collection.Log.LogInfo(Tag, "Session is about to expire, try to relogin and refresh");
if (Collection.Keystore.Session.TempPassword == null)
{
Collection.Log.LogInfo(Tag, "A2 is null, abort");
return;
}
var d2 = Collection.Keystore.Session.D2;
var d2Key = Collection.Keystore.Session.D2Key;
var tgt = Collection.Keystore.Session.Tgt; // save the original state
Collection.Socket.Disconnect();
Collection.Keystore.ClearSession();
await Collection.Socket.Connect();
if (await KeyExchange())
{
var easyLoginEvent = EasyLoginEvent.Create();
var easyLoginResult = await Collection.Business.SendEvent(easyLoginEvent);
if (easyLoginResult.Count != 0)
{
var result = (EasyLoginEvent)easyLoginResult[0];
if ((LoginCommon.Error)result.ResultCode == LoginCommon.Error.Success)
{
Collection.Log.LogInfo(Tag, "Login Success, try to register services");
if (await BotOnline(BotOnlineEvent.OnlineReason.Reconnect)) return;
Collection.Log.LogInfo(Tag, "Re-login failed, please refresh manually");
}
}
}
else
{
Collection.Log.LogInfo(Tag, "Key Exchange Failed, trying to online, please refresh manually");
}
Collection.Keystore.Session.D2 = d2;
Collection.Keystore.Session.D2Key = d2Key;
Collection.Keystore.Session.Tgt = tgt;
await BotOnline(BotOnlineEvent.OnlineReason.Reconnect);
}
public bool SubmitCaptcha(string ticket, string randStr) => _captchaTask.TrySetResult((ticket, randStr));
}

View File

@@ -0,0 +1,14 @@
using Lagrange.Core.Internal.Event;
namespace Lagrange.Core.Internal.Context.Logic;
internal abstract class LogicBase
{
protected readonly ContextCollection Collection;
protected LogicBase(ContextCollection collection) => Collection = collection;
public virtual Task Incoming(ProtocolEvent e) => Task.CompletedTask;
public virtual Task Outgoing(ProtocolEvent e) => Task.CompletedTask;
}

View File

@@ -0,0 +1,110 @@
using System.Collections.Concurrent;
using Lagrange.Core.Common;
using Lagrange.Core.Internal.Packets;
using Lagrange.Core.Utility.Binary;
using Lagrange.Core.Utility.Sign;
#pragma warning disable CS4014
namespace Lagrange.Core.Internal.Context;
/// <summary>
/// <para>Translate the protocol event into SSOPacket and further ServiceMessage</para>
/// <para>And Dispatch the packet from <see cref="SocketContext"/> by managing the sequence from Tencent's server</para>
/// <para>Every Packet should be sent and received from this context instead of being directly send to <see cref="SocketContext"/></para>
/// </summary>
internal class PacketContext : ContextBase
{
internal SignProvider SignProvider { private get; set; }
private readonly ConcurrentDictionary<uint, TaskCompletionSource<SsoPacket>> _pendingTasks;
public PacketContext(ContextCollection collection, BotKeystore keystore, BotAppInfo appInfo, BotDeviceInfo device, BotConfig config)
: base(collection, keystore, appInfo, device)
{
SignProvider = config.CustomSignProvider ?? appInfo.Os switch
{
"Windows" => new WindowsSigner(),
"Mac" => new MacSigner(),
"Linux" => new LinuxSigner(),
_ => throw new Exception("Unknown System Found")
};
_pendingTasks = new ConcurrentDictionary<uint, TaskCompletionSource<SsoPacket>>();
}
/// <summary>
/// Send the packet and wait for the corresponding response by the packet's sequence number.
/// </summary>
public Task<SsoPacket> SendPacket(SsoPacket packet)
{
var task = new TaskCompletionSource<SsoPacket>();
_pendingTasks.TryAdd(packet.Sequence, task);
switch (packet.PacketType)
{
case 12:
{
var sso = SsoPacker.Build(packet, AppInfo, DeviceInfo, Keystore, SignProvider);
var service = ServicePacker.BuildProtocol12(sso, Keystore);
bool _ = Collection.Socket.Send(service.ToArray()).GetAwaiter().GetResult();
break;
}
case 13:
{
var service = ServicePacker.BuildProtocol13(packet.Payload, Keystore, packet.Command, packet.Sequence);
bool _ = Collection.Socket.Send(service.ToArray()).GetAwaiter().GetResult();
break;
}
}
return task.Task;
}
/// <summary>
/// Send the packet and don't wait for the corresponding response by the packet's sequence number.
/// </summary>
public async Task<bool> PostPacket(SsoPacket packet)
{
switch (packet.PacketType)
{
case 12:
{
var sso = SsoPacker.Build(packet, AppInfo, DeviceInfo, Keystore, SignProvider);
var service = ServicePacker.BuildProtocol12(sso, Keystore);
return await Collection.Socket.Send(service.ToArray());
}
case 13:
{
var service = ServicePacker.BuildProtocol13(packet.Payload, Keystore, packet.Command, packet.Sequence);
return await Collection.Socket.Send(service.ToArray());
}
default:
return false;
}
}
public void DispatchPacket(BinaryPacket packet)
{
var service = ServicePacker.Parse(packet, Keystore);
if (service.Length == 0) return;
var sso = SsoPacker.Parse(service);
if (_pendingTasks.TryRemove(sso.Sequence, out var task))
{
if (sso is { RetCode: not 0, Extra: { } extra})
{
string msg = $"Packet '{sso.Command}' returns {sso.RetCode} with seq: {sso.Sequence}, extra: {extra}";
task.SetException(new InvalidOperationException(msg));
}
else
{
task.SetResult(sso);
}
}
else
{
Collection.Business.HandleServerPacket(sso);
}
}
}

View File

@@ -0,0 +1,131 @@
using System.Collections.Concurrent;
using System.Reflection;
using Lagrange.Core.Common;
using Lagrange.Core.Internal.Event;
using Lagrange.Core.Internal.Packets;
using Lagrange.Core.Internal.Service;
using Lagrange.Core.Utility.Extension;
namespace Lagrange.Core.Internal.Context;
/// <summary>
/// <para>Manage the service and packet translation of the Bot</para>
/// <para>Instantiate the Service by <see cref="System.Reflection"/> and store such</para>
/// <para>Translate the event into <see cref="ProtocolEvent"/>, you may manually dispatch the packet to <see cref="PacketContext"/></para>
/// </summary>
internal class ServiceContext : ContextBase
{
private const string Tag = nameof(ServiceContext);
private readonly SequenceProvider _sequenceProvider;
private readonly Dictionary<string, IService> _services;
private readonly Dictionary<Type, List<(ServiceAttribute Attribute, IService Instance)>> _servicesEventType;
public ServiceContext(ContextCollection collection, BotKeystore keystore, BotAppInfo appInfo, BotDeviceInfo device)
: base(collection, keystore, appInfo, device)
{
_sequenceProvider = new SequenceProvider();
_services = new Dictionary<string, IService>();
_servicesEventType = new Dictionary<Type, List<(ServiceAttribute, IService)>>();
RegisterServices();
}
private void RegisterServices()
{
var assembly = Assembly.GetExecutingAssembly();
foreach (var type in assembly.GetDerivedTypes<ProtocolEvent>())
{
_servicesEventType[type] = new List<(ServiceAttribute, IService)>();
}
foreach (var type in assembly.GetTypeByAttributes<ServiceAttribute>(out _))
{
var serviceAttribute = type.GetCustomAttribute<ServiceAttribute>();
if (serviceAttribute != null)
{
var service = (IService)type.CreateInstance();
_services[serviceAttribute.Command] = service;
foreach (var attribute in type.GetCustomAttributes<EventSubscribeAttribute>())
{
_servicesEventType[attribute.EventType].Add((serviceAttribute, service));
}
}
}
}
/// <summary>
/// Resolve the outgoing packet by the event
/// </summary>
public List<SsoPacket> ResolvePacketByEvent(ProtocolEvent protocolEvent)
{
var result = new List<SsoPacket>();
if (!_servicesEventType.TryGetValue(protocolEvent.GetType(), out var serviceList)) return result; // 没找到 滚蛋吧
foreach (var (attribute, instance) in serviceList)
{
bool success = instance.Build(protocolEvent, Keystore, AppInfo, DeviceInfo, out var binary, out var extraPackets);
if (success && binary != null)
{
result.Add(new SsoPacket(attribute.PacketType, attribute.Command, (uint)_sequenceProvider.GetNewSequence(), binary.ToArray()));
if (extraPackets is { } extra)
{
var packets = extra.Select(e => new SsoPacket(attribute.PacketType, attribute.Command, (uint)_sequenceProvider.GetNewSequence(), e.ToArray()));
result.AddRange(packets);
}
Collection.Log.LogDebug(Tag, $"Outgoing SSOFrame: {attribute.Command}");
}
}
return result;
}
/// <summary>
/// Resolve the incoming event by the packet
/// </summary>
public List<ProtocolEvent> ResolveEventByPacket(SsoPacket packet)
{
var result = new List<ProtocolEvent>();
if (!_services.TryGetValue(packet.Command, out var service))
{
Collection.Log.LogWarning(Tag, $"Unsupported SSOFrame Received: {packet.Command}");
Collection.Log.LogDebug(Tag, $"Unsuccessful SSOFrame Payload: {packet.Payload.Hex()}");
return result; // 没找到 滚蛋吧
}
bool success = service.Parse(packet.Payload, Keystore, AppInfo, DeviceInfo, out var @event, out var extraEvents);
if (success)
{
if (@event != null) result.Add(@event);
if (extraEvents != null) result.AddRange(extraEvents);
Collection.Log.LogDebug(Tag, $"Incoming SSOFrame: {packet.Command}");
}
return result;
}
public int GetNewSequence() => _sequenceProvider.GetNewSequence();
private class SequenceProvider
{
private readonly ConcurrentDictionary<string, int> _sessionSequence = new();
private int _sequence = Random.Shared.Next(5000000, 9900000);
public int GetNewSequence()
{
Interlocked.CompareExchange(ref _sequence, 5000000, 9900000);
return Interlocked.Increment(ref _sequence);
}
public int RegisterSession(string sessionId) => _sessionSequence.GetOrAdd(sessionId, GetNewSequence());
}
}

View File

@@ -0,0 +1,140 @@
using System.Buffers.Binary;
using System.Net;
using Lagrange.Core.Common;
using Lagrange.Core.Event.EventArg;
using Lagrange.Core.Internal.Network;
using Lagrange.Core.Utility.Binary;
using Lagrange.Core.Utility.Extension;
using Lagrange.Core.Utility.Network;
namespace Lagrange.Core.Internal.Context;
/// <summary>
/// <para>Provide Low-Allocation Tcp Client which connects to the Tencent's SSO Server</para>
/// <para>Internal Implementation, Packet Received would be dispatched to <see cref="PacketContext"/> for decryption and unpack</para>
/// <para>MSF Service is also implemented here</para>
/// </summary>
internal class SocketContext : ContextBase, IClientListener
{
private const string Tag = nameof(SocketContext);
private readonly ClientListener _tcpClient;
private readonly BotConfig _config;
private Uri? ServerUri { get; set; }
public uint HeaderSize => 4;
public bool Connected => _tcpClient.Connected;
public SocketContext(ContextCollection collection, BotKeystore keystore, BotAppInfo appInfo, BotDeviceInfo device, BotConfig config)
: base(collection, keystore, appInfo, device)
{
_tcpClient = new CallbackClientListener(this);
_config = config;
}
public async Task<bool> Connect()
{
if (_tcpClient.Connected) return true;
var servers = await OptimumServer(_config.GetOptimumServer, _config.UseIPv6Network);
ServerUri = servers.First();
return await _tcpClient.Connect(ServerUri.Host, ServerUri.Port);
}
private async Task<bool> Reconnect()
{
if (ServerUri != null && !_tcpClient.Connected)
{
bool reconnect = await _tcpClient.Connect(ServerUri.Host, ServerUri.Port);
if (reconnect)
{
Collection.Log.LogInfo(Tag, $"Reconnect to {ServerUri}");
await Collection.Business.WtExchangeLogic.BotOnline(BotOnlineEvent.OnlineReason.Reconnect);
}
}
return false;
}
public void Disconnect() => _tcpClient.Disconnect();
public Task<bool> Send(ReadOnlyMemory<byte> packet) => _tcpClient.Send(packet);
public uint GetPacketLength(ReadOnlySpan<byte> header) => BinaryPrimitives.ReadUInt32BigEndian(header);
public void OnRecvPacket(ReadOnlySpan<byte> packet)
{
var binary = new BinaryPacket(packet.ToArray());
Collection.Packet.DispatchPacket(binary);
}
public void OnDisconnect()
{
Collection.Log.LogFatal(Tag, "Socket Disconnected, Scheduling Reconnect");
if (_config.AutoReconnect)
{
Collection.Scheduler.Interval("Reconnect", 10 * 1000, async () =>
{
if (await Reconnect()) Collection.Scheduler.Cancel("Reconnect");
});
}
}
public void OnSocketError(Exception e, ReadOnlyMemory<byte> data = default)
{
Collection.Log.LogFatal(Tag, $"Socket Error: {e.Message}");
if (e.StackTrace != null) Collection.Log.LogFatal(Tag, e.StackTrace);
if (data.Length > 0) Collection.Log.LogDebug(Tag, $"Data: {data.Span.Hex()}");
_tcpClient.Disconnect();
if (!_tcpClient.Connected) OnDisconnect();
}
private static readonly Uri[] HardCodeIPv6Uris =
{
new("http://msfwifiv6.3g.qq.com:14000")
};
/// <summary>
/// 好像这才是真货
/// </summary>
private static readonly Uri[] TestIPv4HardCodes =
{
new("http://183.47.102.193:8080"),
new("http://14.22.9.84:8080"),
new("http://119.147.190.138:8080")
};
private async Task<List<Uri>> OptimumServer(bool requestMsf, bool useIPv6Network = false)
{
var result = requestMsf ? await ResolveDns(useIPv6Network) : useIPv6Network ? HardCodeIPv6Uris : TestIPv4HardCodes;
var latencyTasks = result.Select(uri => Icmp.PingAsync(uri)).ToArray();
var latency = await Task.WhenAll(latencyTasks);
Array.Sort(latency, result);
var list = result.ToList();
for (int i = 0; i < list.Count; i++) Collection.Log.LogVerbose(Tag, $"Server: {list[i]} Latency: {latency[i]}");
return list;
}
private static async Task<Uri[]> ResolveDns(bool useIPv6Network = false)
{
string dns = useIPv6Network ? "msfwifiv6.3g.qq.com" : "msfwifi.3g.qq.com";
var addresses = await Dns.GetHostEntryAsync(dns);
var result = new Uri[addresses.AddressList.Length];
for (int i = 0; i < addresses.AddressList.Length; i++) result[i] = new Uri($"http://{addresses.AddressList[i]}:8080");
return result;
}
public void Dispose()
{
_tcpClient.Disconnect();
}
}

View File

@@ -0,0 +1,113 @@
using Lagrange.Core.Internal.Event.Message;
using Lagrange.Core.Internal.Event.System;
using Lagrange.Core.Internal.Packets.Service.Highway;
using Lagrange.Core.Internal.Packets.Service.Oidb.Common;
using Lagrange.Core.Utility.Crypto.Provider.Sha;
using Lagrange.Core.Utility.Extension;
namespace Lagrange.Core.Internal.Context.Uploader;
internal static class Common
{
private const int BlockSize = 1024 * 1024;
public static NTV2RichMediaHighwayExt? GenerateExt(NTV2RichMediaUploadEvent @event)
{
if (@event.UKey == null) return null;
var index = @event.MsgInfo.MsgInfoBody[0].Index;
return new NTV2RichMediaHighwayExt
{
FileUuid = index.FileUuid,
UKey = @event.UKey,
Network = Convert(@event.Network),
MsgInfoBody = @event.MsgInfo.MsgInfoBody,
BlockSize = BlockSize,
Hash = new NTHighwayHash
{
FileSha1 = new List<byte[]> { index.Info.FileSha1.UnHex() }
}
};
}
public static NTV2RichMediaHighwayExt? GenerateExt(NTV2RichMediaUploadEvent @event, SubFileInfo subFile)
{
if (subFile.UKey == null) return null;
var index = @event.MsgInfo.MsgInfoBody[1].Index;
return new NTV2RichMediaHighwayExt
{
FileUuid = index.FileUuid,
UKey = subFile.UKey,
Network = Convert(subFile.IPv4s),
MsgInfoBody = @event.MsgInfo.MsgInfoBody,
BlockSize = BlockSize,
Hash = new NTHighwayHash
{
FileSha1 = new List<byte[]> { index.Info.FileSha1.UnHex() }
}
};
}
public static async Task<byte[]> GetTicket(ContextCollection context)
{
var hwUrlEvent = HighwayUrlEvent.Create();
var highwayUrlResult = await context.Business.SendEvent(hwUrlEvent);
return ((HighwayUrlEvent)highwayUrlResult[0]).SigSession;
}
public static List<byte[]> CalculateStreamBytes(Stream inputStream)
{
const int blockSize = 1024 * 1024;
inputStream.Seek(0, SeekOrigin.Begin);
var byteArrayList = new List<byte[]>();
var sha1 = new Sha1Stream();
var buffer = new byte[Sha1Stream.Sha1BlockSize];
var digest = new byte[Sha1Stream.Sha1DigestSize];
int lastRead;
while (true)
{
int read = inputStream.Read(buffer);
if (read < Sha1Stream.Sha1BlockSize)
{
lastRead = read;
break;
}
sha1.Update(buffer, Sha1Stream.Sha1BlockSize);
if (inputStream.Position % blockSize == 0)
{
sha1.Hash(digest, false);
byteArrayList.Add((byte[])digest.Clone());
}
}
sha1.Update(buffer, lastRead);
sha1.Final(digest);
byteArrayList.Add((byte[])digest.Clone());
return byteArrayList;
}
private static NTHighwayNetwork Convert(List<IPv4> ipv4s) => new()
{
IPv4s = ipv4s.Select(x => new NTHighwayIPv4
{
Domain = new NTHighwayDomain
{
IsEnable = true,
IP = ConvertIP(x.OutIP)
},
Port = x.OutPort
}).ToList()
};
private static string ConvertIP(uint raw)
{
var ip = BitConverter.GetBytes(raw);
return $"{ip[0]}.{ip[1]}.{ip[2]}.{ip[3]}";
}
}

View File

@@ -0,0 +1,158 @@
using Lagrange.Core.Internal.Event.Message;
using Lagrange.Core.Internal.Packets.Service.Highway;
using Lagrange.Core.Message;
using Lagrange.Core.Message.Entity;
using Lagrange.Core.Utility.Extension;
namespace Lagrange.Core.Internal.Context.Uploader;
/// <summary>
/// This FileUploader should be called manually
/// </summary>
internal static class FileUploader
{
public static async Task<bool> UploadPrivate(ContextCollection context, MessageChain chain, IMessageEntity entity)
{
if (entity is not FileEntity { FileStream: not null } file) return false;
var uploadEvent = FileUploadEvent.Create(chain.Uid ?? "", file);
var result = await context.Business.SendEvent(uploadEvent);
var uploadResp = (FileUploadEvent)result[0];
if (!uploadResp.IsExist)
{
var ext = new FileUploadExt
{
Unknown1 = 100,
Unknown2 = 1,
Entry = new FileUploadEntry
{
BusiBuff = new ExcitingBusiInfo
{
SenderUin = context.Keystore.Uin,
},
FileEntry = new ExcitingFileEntry
{
FileSize = file.FileStream.Length,
Md5 = file.FileMd5,
CheckKey = file.FileSha1,
Md5S2 = file.FileMd5,
FileId = uploadResp.FileId,
UploadKey = uploadResp.UploadKey
},
ClientInfo = new ExcitingClientInfo
{
ClientType = 3,
AppId = "100",
TerminalType = 3,
ClientVer = "1.1.1",
Unknown = 4
},
FileNameInfo = new ExcitingFileNameInfo
{
FileName = file.FileName
},
Host = new ExcitingHostConfig
{
Hosts = new List<ExcitingHostInfo>
{
new()
{
Url = new ExcitingUrlInfo
{
Unknown = 1,
Host = uploadResp.Ip
},
Port = uploadResp.Port
}
}
}
},
Unknown200 = 1
};
file.FileHash = uploadResp.Addon;
file.FileUuid = uploadResp.FileId;
bool hwSuccess = await context.Highway.UploadSrcByStreamAsync(95, file.FileStream, await Common.GetTicket(context), file.FileMd5, ext.Serialize().ToArray());
if (!hwSuccess) return false;
}
await file.FileStream.DisposeAsync();
var sendEvent = SendMessageEvent.Create(chain);
var sendResult = await context.Business.SendEvent(sendEvent);
return sendResult.Count != 0 && ((SendMessageEvent)sendResult[0]).MsgResult.Result == 0;
}
public static async Task<bool> UploadGroup(ContextCollection context, MessageChain chain, IMessageEntity entity, string targetDirectory)
{
if (entity is not FileEntity { FileStream: not null } file) return false;
var uploadEvent = GroupFSUploadEvent.Create(chain.GroupUin ?? 0, targetDirectory, file);
var result = await context.Business.SendEvent(uploadEvent);
var uploadResp = (GroupFSUploadEvent)result[0];
if (!uploadResp.IsExist)
{
var ext = new FileUploadExt
{
Unknown1 = 100,
Unknown2 = 1,
Entry = new FileUploadEntry
{
BusiBuff = new ExcitingBusiInfo
{
SenderUin = context.Keystore.Uin,
ReceiverUin = chain.GroupUin ?? 0,
GroupCode = chain.GroupUin ?? 0
},
FileEntry = new ExcitingFileEntry
{
FileSize = file.FileStream.Length,
Md5 = file.FileMd5,
CheckKey = uploadResp.CheckKey,
Md5S2 = file.FileMd5,
FileId = uploadResp.FileId,
UploadKey = uploadResp.UploadKey
},
ClientInfo = new ExcitingClientInfo
{
ClientType = 3,
AppId = "100",
TerminalType = 3,
ClientVer = "1.1.1",
Unknown = 4
},
FileNameInfo = new ExcitingFileNameInfo
{
FileName = file.FileName
},
Host = new ExcitingHostConfig
{
Hosts = new List<ExcitingHostInfo>
{
new()
{
Url = new ExcitingUrlInfo
{
Unknown = 1,
Host = uploadResp.Ip
},
Port = uploadResp.Port
}
}
}
}
};
bool hwSuccess = await context.Highway.UploadSrcByStreamAsync(71, file.FileStream, await Common.GetTicket(context), file.FileMd5, ext.Serialize().ToArray());
if (!hwSuccess) return false;
}
await file.FileStream.DisposeAsync();
var sendEvent = GroupSendFileEvent.Create(chain.GroupUin ?? 0, uploadResp.FileId);
var sendResult = await context.Business.SendEvent(sendEvent);
return sendResult.Count != 0 && ((GroupSendFileEvent)sendResult[0]).ResultCode == 0;
}
}

Some files were not shown because too many files have changed in this diff Show More