├── .gitattributes ├── .github └── workflows │ └── dotnet-core.yml ├── .gitignore ├── CxSignHelper ├── CxSignClient.cs ├── CxSignHelper.csproj └── Models │ ├── LoginObject.cs │ ├── SignOptions.cs │ ├── SignType.cs │ └── TokenObject.cs ├── LICENSE ├── README.md ├── cx-auto-sign.WebApi ├── Controllers │ └── StatusController.cs ├── IntervalData.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── Status.cs ├── appsettings.Development.json ├── appsettings.json └── cx-auto-sign.WebApi.csproj ├── cx-auto-sign.sln ├── cx-auto-sign ├── AppDataConfig.cs ├── BaseConfig.cs ├── BaseDataConfig.cs ├── CommandBase.cs ├── CourseConfig.cs ├── CourseDataConfig.cs ├── Cxim.cs ├── InitCommand.cs ├── Notification.cs ├── Program.cs ├── UpdateCommand.cs ├── UserConfig.cs ├── UserDataConfig.cs ├── WorkCommand.cs └── cx-auto-sign.csproj └── global.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 5.0.x 20 | - name: Install dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --configuration Release --no-restore 24 | - name: Publish 25 | run: dotnet publish -c Release -o out 26 | - name: Upload Build Artifact 27 | uses: actions/upload-artifact@v2 28 | with: 29 | name: cx-auto-sign-publish 30 | path: "out" 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | [Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb 341 | /cx-auto-sign/Properties/launchSettings.json 342 | -------------------------------------------------------------------------------- /CxSignHelper/CxSignClient.cs: -------------------------------------------------------------------------------- 1 | using CxSignHelper.Models; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using RestSharp; 5 | using System; 6 | using System.Net; 7 | using System.Text.RegularExpressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace CxSignHelper 11 | { 12 | public class CxSignClient 13 | { 14 | private readonly CookieContainer _cookie; 15 | 16 | private string Fid { get; set; } 17 | 18 | private string PUid { get; set; } 19 | 20 | private CxSignClient(CookieContainer cookieContainer) 21 | { 22 | _cookie = cookieContainer; 23 | ParseCookies(); 24 | } 25 | 26 | public static async Task LoginAsync(string username, string password, string fid = null) 27 | { 28 | RestClient client; 29 | IRestResponse response; 30 | if (string.IsNullOrEmpty(fid)) 31 | { 32 | client = new RestClient("https://passport2-api.chaoxing.com") 33 | { 34 | CookieContainer = new CookieContainer() 35 | }; 36 | var request = new RestRequest("v11/loginregister"); 37 | request.AddParameter("uname", username); 38 | request.AddParameter("code", password); 39 | response = await client.ExecuteGetAsync(request); 40 | } 41 | else 42 | { 43 | client = new RestClient($"https://passport2-api.chaoxing.com/v6/idNumberLogin?fid={fid}&idNumber={username}") 44 | { 45 | CookieContainer = new CookieContainer() 46 | }; 47 | var request = new RestRequest(Method.POST); 48 | request.AddHeader("Content-Type", "application/x-www-form-urlencoded"); 49 | request.AddParameter("pwd", password); 50 | request.AddParameter("t", "0"); 51 | response = await client.ExecutePostAsync(request); 52 | } 53 | TestResponseCode(response); 54 | var loginObject = JsonConvert.DeserializeObject(response.Content); 55 | if (loginObject.Status != true) 56 | { 57 | throw new Exception(loginObject.Message); 58 | } 59 | return new CxSignClient(client.CookieContainer); 60 | } 61 | 62 | private async Task GetTokenAsync() 63 | { 64 | var client = new RestClient("https://pan-yz.chaoxing.com") 65 | { 66 | CookieContainer = _cookie 67 | }; 68 | var request = new RestRequest("api/token/uservalid"); 69 | var response = await client.ExecuteGetAsync(request); 70 | TestResponseCode(response); 71 | var tokenObject = JsonConvert.DeserializeObject(response.Content); 72 | if (tokenObject.Result != true) 73 | { 74 | throw new Exception("获取 token 失败"); 75 | } 76 | return tokenObject.Token; 77 | } 78 | 79 | public async Task GetSignTasksAsync(string courseId, string classId) 80 | { 81 | var client = new RestClient("https://mobilelearn.chaoxing.com") 82 | { 83 | CookieContainer = _cookie 84 | }; 85 | var request = new RestRequest("v2/apis/active/student/activelist"); 86 | request.AddParameter("fid", "0"); 87 | request.AddParameter("courseId", courseId); 88 | request.AddParameter("classId", classId); 89 | var response = await client.ExecuteGetAsync(request); 90 | TestResponseCode(response); 91 | var json = JObject.Parse(response.Content); 92 | if (json["result"]!.Value() != 1) 93 | { 94 | throw new Exception(json["msg"]?.Value()); 95 | } 96 | return (JArray)json["data"]!["activeList"]; 97 | } 98 | 99 | public async Task GetActiveDetailAsync(string activeId) 100 | { 101 | var client = new RestClient("https://mobilelearn.chaoxing.com") 102 | { 103 | CookieContainer = _cookie 104 | }; 105 | var request = new RestRequest("v2/apis/active/getPPTActiveInfo"); 106 | request.AddParameter("activeId", activeId); 107 | var response = await client.ExecuteGetAsync(request); 108 | TestResponseCode(response); 109 | var json = JObject.Parse(response.Content); 110 | if (json["result"]?.Value() != 1) 111 | { 112 | throw new Exception("Message: " + json["msg"]?.Value() + 113 | "\nError Message: " + json["errorMsg"]?.Value()); 114 | } 115 | return json["data"]; 116 | } 117 | 118 | public async Task SignAsync(string activeId, SignOptions signOptions) 119 | { 120 | var client = new RestClient("https://mobilelearn.chaoxing.com/pptSign/stuSignajax") 121 | { 122 | CookieContainer = _cookie 123 | }; 124 | var request = new RestRequest(Method.GET); 125 | // ?activeId=292002019&appType=15&ifTiJiao=1&latitude=-1&longitude=-1&clientip=1.1.1.1&address=中国&objectId=3194679e88dbc9c60a4c6e31da7fa905 126 | request.AddParameter("activeId", activeId); 127 | request.AddParameter("appType", "15"); 128 | request.AddParameter("ifTiJiao", "1"); 129 | request.AddParameter("latitude", signOptions.Latitude); 130 | request.AddParameter("longitude", signOptions.Longitude); 131 | request.AddParameter("clientip", signOptions.ClientIp); 132 | request.AddParameter("address", signOptions.Address); 133 | request.AddParameter("objectId", signOptions.ImageId); 134 | var response = await client.ExecuteGetAsync(request); 135 | return response.Content; 136 | } 137 | 138 | public async Task<(string ImToken, string TUid)> GetImTokenAsync() 139 | { 140 | var client = new RestClient("https://im.chaoxing.com/webim/me") 141 | { 142 | CookieContainer = _cookie 143 | }; 144 | var request = new RestRequest(Method.GET); 145 | var response = await client.ExecuteGetAsync(request); 146 | TestResponseCode(response); 147 | var regex = new Regex(@"loginByToken\('(\d+?)', '([^']+?)'\);"); 148 | var match = regex.Match(response.Content); 149 | if (!match.Success) 150 | { 151 | throw new Exception("获取 ImToken 失败"); 152 | } 153 | return (match.Groups[2].Value, match.Groups[1].Value); 154 | } 155 | 156 | public async Task GetCoursesAsync(JToken course) 157 | { 158 | var client = new RestClient("https://mooc2-ans.chaoxing.com/visit/courses/list?rss=1&start=0&size=500&catalogId=0&searchname=") 159 | { 160 | CookieContainer = _cookie 161 | }; 162 | var request = new RestRequest(Method.GET); 163 | var response = await client.ExecuteGetAsync(request); 164 | TestResponseCode(response); 165 | var regex = new Regex(@"\?courseid=(\d+?)&clazzid=(\d+)&cpi=\d+"""); 166 | var matches = regex.Matches(response.Content); 167 | foreach (Match match in matches) 168 | { 169 | if (match.Groups.Count <= 2) 170 | { 171 | continue; 172 | } 173 | var courseId = match.Groups[1].Value; 174 | var classId = match.Groups[2].Value; 175 | var (chatId, courseName, className) = await GetClassDetailAsync(courseId, classId); 176 | var obj = new JObject 177 | { 178 | ["CourseId"] = courseId, 179 | ["ClassId"] = classId, 180 | ["ChatId"] = chatId, 181 | ["CourseName"] = courseName, 182 | ["ClassName"] = className 183 | }; 184 | course[chatId] = obj; 185 | } 186 | } 187 | 188 | private async Task<(string ChatId, string CourseName, string ClassName)> GetClassDetailAsync(string courseId, string classId) 189 | { 190 | var client = new RestClient($"https://mobilelearn.chaoxing.com/v2/apis/class/getClassDetail?fid={Fid}&courseId={courseId}&classId={classId}") 191 | { 192 | CookieContainer = _cookie 193 | }; 194 | var request = new RestRequest(Method.GET); 195 | var response = await client.ExecuteGetAsync(request); 196 | TestResponseCode(response); 197 | var json = JObject.Parse(response.Content); 198 | if (json["result"]!.Value() != 1) 199 | { 200 | throw new Exception(json["msg"]?.Value()); 201 | } 202 | var data = json["data"]; 203 | var chatId = data!["chatid"]!.Value(); 204 | var courseName = data["course"]!["data"]![0]!["name"]!.Value(); 205 | var className = data["name"]!.Value(); 206 | return (chatId, courseName, className); 207 | } 208 | 209 | public async Task UploadImageAsync(string path) 210 | { 211 | // 预览: 212 | // https://p.ananas.chaoxing.com/star3/170_220c/f5b88e10d3dfedf9829ca8c009029e7b.png 213 | // https://p.ananas.chaoxing.com/star3/origin/f5b88e10d3dfedf9829ca8c009029e7b.png 214 | // https://pan-yz.chaoxing.com/thumbnail/origin/f5b88e10d3dfedf9829ca8c009029e7b?type=img 215 | var client = new RestClient("https://pan-yz.chaoxing.com/upload") 216 | { 217 | CookieContainer = _cookie 218 | }; 219 | var request = new RestRequest(Method.POST); 220 | request.AddParameter("puid", PUid); 221 | request.AddParameter("_token", await GetTokenAsync()); 222 | request.AddFile("file", path); 223 | var response = await client.ExecutePostAsync(request); 224 | var json = JObject.Parse(response.Content); 225 | if (json["result"]!.Value() != true) 226 | { 227 | throw new Exception(json["msg"]?.Value()); 228 | } 229 | return json["objectId"]!.Value(); 230 | } 231 | 232 | private void ParseCookies() 233 | { 234 | var cookies = _cookie.GetCookies(new Uri("http://chaoxing.com")); 235 | Fid = cookies["fid"]?.Value; 236 | PUid = cookies["_uid"]!.Value; 237 | } 238 | 239 | public static void TestResponseCode(IRestResponse response) 240 | { 241 | var code = response.StatusCode; 242 | if (code != HttpStatusCode.OK) 243 | { 244 | throw new Exception($"非 200 状态响应:{code:D} {code:G}\n{response.Content}"); 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /CxSignHelper/CxSignHelper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | https://github.com/cyanray/cx-auto-sign 6 | 1.2.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /CxSignHelper/Models/LoginObject.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace CxSignHelper.Models 4 | { 5 | internal class LoginObject 6 | { 7 | [JsonProperty("mes")] 8 | public string Message { get; set; } 9 | 10 | [JsonProperty("status")] 11 | public bool Status { get; set; } 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /CxSignHelper/Models/SignOptions.cs: -------------------------------------------------------------------------------- 1 | namespace CxSignHelper.Models 2 | { 3 | public class SignOptions 4 | { 5 | public string Address { get; init; } = "中国"; 6 | public string Latitude { get; init; } = "-1"; 7 | public string Longitude { get; init; } = "-1"; 8 | public string ClientIp { get; init; } = "1.1.1.1"; 9 | public string ImageId { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CxSignHelper/Models/SignType.cs: -------------------------------------------------------------------------------- 1 | namespace CxSignHelper.Models 2 | { 3 | public enum SignType 4 | { 5 | // 普通签到 6 | Normal, 7 | 8 | // 图片签到 9 | Photo, 10 | 11 | // 二维码签到 12 | Qr, 13 | 14 | // 手势签到 15 | Gesture, 16 | 17 | // 位置签到 18 | Location 19 | } 20 | } -------------------------------------------------------------------------------- /CxSignHelper/Models/TokenObject.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace CxSignHelper.Models 4 | { 5 | internal class TokenObject 6 | { 7 | [JsonProperty("result")] 8 | public bool Result { get; set; } 9 | [JsonProperty("_token")] 10 | public string Token { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Cyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cx-auto-sign 2 | ## 👀因作者精力有限本仓库已经停止维护,可以考虑其他同类型项目或由开源爱好者维护的本项目的 [fork](https://github.com/moeshin/cx-auto-sign)。 3 | 4 | ## ⚠注意 5 | * 由于超星学习通更新,目前无法签到二维码签到 [#23](https://github.com/cyanray/cx-auto-sign/issues/23) 。其他类型的签到正常。 6 | * **自动签到** 功能默认是 **关闭** 的,详见「[配置说明](#配置说明)」 7 | 8 | ## 项目简介 9 | 10 | ![](https://github.com/cyanray/cx-auto-sign/workflows/.NET%20Core/badge.svg) 11 | 12 | cx-auto-sign 是基于 .NET5 的超星学习通自动签到工具。 13 | 14 | 本项目最低程度实现超星学习通的即时通讯协议。通过超星学习通的即时通讯协议监测最新的课程活动。当指定的课程有新的消息,就检查该课程是否有新的签到任务,如果有则进行签到。该方法与轮询相比,灵敏度更高,占用资源更低。 15 | 16 | ## 项目特性 17 | 18 | - [x] 支持 **手机号登录** 和 **学号登录** 两种登录方式 19 | - [x] 支持 `init` 指令,用以生成配置文件 20 | - [x] 支持 `update` 指令,更新课程信息 21 | - [x] 实现自动签到工作流程 22 | - [x] 支持 **WebApi** 控制自动签到启停 23 | - [x] 支持通过 **Server 酱** 发送通知 24 | - [x] 支持通过 **PushPlus 推送加** 发送通知 25 | - [x] 支持签到成功后发送 **邮件通知** 26 | - [x] 支持多账号配置 27 | 28 | 29 | ## 使用方法 30 | 31 | ### 0x00 运行环境 32 | 33 | 首先需要在 [.Net Runtime 下载页](https://dotnet.microsoft.com/download/dotnet/current/runtime) 下载并安装 **.NET5 Runtime**(提示:Run server apps下边的下载)。 34 | 35 | 然后在 [Release页面](https://github.com/cyanray/cx-auto-sign/releases) 下载 cx-auto-sign.zip,并解压到某个目录。 36 | 37 | (你也可以在 [Actions](https://github.com/cyanray/cx-auto-sign/actions) 中找到自动编译的测试版) 38 | 39 | ### 0x01 登录并初始化配置文件 40 | 41 | 在 cx-auto-sign.dll 所在的目录执行以下命令行(Windows 和 Linux都适用): 42 | 43 | ```shell 44 | # 通过手机号码登录,不需要学校编码 45 | dotnet ./cx-auto-sign.dll init -u "双引号里面填手机号" -p "双引号里面填密码" 46 | ``` 47 | 48 | **或:** 49 | 50 | ```shell 51 | # 通过学号登录,需要学校编码 52 | dotnet ./cx-auto-sign.dll init -u "双引号里面填学号" -p "双引号里面填密码" -f "学校编码" 53 | ``` 54 | 55 | ### 0x02 开始自动签到 56 | 57 | 在 `cx-auto-sign.dll` 所在的目录执行以下命令行: 58 | 59 | ```shell 60 | dotnet ./cx-auto-sign.dll work 61 | ``` 62 | 63 | 即可开始自动签到。 64 | 65 | ## 配置说明 66 | 67 | 配置文件均采用 **json5** 格式,方便编辑、打注释。 68 | 69 | ### 目录结构 70 | 71 | ```text 72 | cx-auto-sign 73 | ├── Configs 74 | │ └── Username.json5 75 | ├── Images 76 | │ └── ... 77 | ├── AppConfig.json5 78 | ├── ... 79 | ``` 80 | 81 | * `AppConfig.json5` 文件为 **应用配置文件**。 82 | * `Configs` 文件夹存放 **用户配置文件**。 83 | * `Username.json5` 文件为 **用户配置文件**,其中 `Username` 指用户名。 84 | * `Images` 文件夹存放「图片签到」要提交的图片。 85 | 86 | 执行 `init` 指令时会创建以上文件 87 | 88 | ### 优先级 89 | 90 | `课程配置` > `用户配置` > `应用配置` 91 | 92 | 当配置中没有该属性,或值为 `null` 时会向下一级取值。 93 | 94 | * **应用配置**:为 `AppConfig.json5` 文件 95 | * **用户配置**:为 `Configs` 文件夹下的文件 96 | * **课程配置**:为 **用户配置** 中 `Couses` 属性 97 | 98 | ### 应用配置 99 | 100 | ```json5 101 | { 102 | // 通知,可以在 应用配置 和 用户配置 中配置 103 | "ServerChanKey": "", // Server 酱的 SendKey https://sct.ftqq.com/sendkey 104 | "PushPlusToken": "", // PushPlus 的 Token https://www.pushplus.plus/ 105 | 106 | // 邮箱通知,可以在 应用配置 和 用户配置 中配置 107 | "Email": "", // 接收通知的邮箱 108 | "SmtpHost": "", // Smtp 发件主机 109 | "SmtpPort": 0, // Smtp 发件端口 110 | "SmtpUsername": "", // Smtp 发件邮箱 111 | "SmtpPassword": "", // Smtp 发件密码 112 | "SmtpSecure": true, // Smtp 是否使用 SSL 113 | 114 | // 签到配置,可以在 应用配置、用户配置 和 课程配置 中配置 115 | "SignEnable": false, // 允许自动,注意:默认是不会自动签到的!!! 116 | "SignNormal": true, // 允许普通签到 117 | "SignGesture": true, // 允许手势签到 118 | "SignPhoto": true, // 允许图片签到,默认一张黑图,可在这里设置值,详见「拍照签到参数说明」 119 | "SignLocation": true, // 允许位置签到 120 | "SignDelay": 0, // 检测到新签到活动后延迟签到的秒数(过小容易出现秒签到现象) 121 | "SignClientIp": "1.1.1.1", // 签到时提交的客户端 ip 地址 122 | "SignAddress": "中国", // 位置签到的中文名地址 123 | // 经纬度坐标使用的是百度坐标系,可通过此工具来确定 https://tool.lu/coordinate/ 124 | // 注意不要弄反了,不然你就在地球的另一头上课了 125 | "SignLongitude": "-1",// 位置签到的经度 126 | "SignLatitude": "-1", // 位置签到的纬度 127 | 128 | // 以下为特有属性,不会被优先级覆盖 129 | "DefaultUsername": "", // 默认用户 130 | } 131 | ``` 132 | 133 | ### 用户配置 134 | 135 | ```json5 136 | { 137 | // 以下为特有属性,不会被优先级覆盖 138 | "Username": "", // 学号或手机号 139 | "Password": "", // 密码 140 | "Fid": "", // 学校代号,fid 为 null 时使用手机号登录 141 | "WebApi": false, // 是否启动 Web Api,可以指定监听规则,详见「WebApi 说明」 142 | "Courses": {}, // 课程配置 143 | } 144 | ``` 145 | 146 | ### 课程配置 147 | 148 | 课程信息由程序获取的,不建议修改,但可以为课程配置不一样的签到设置。 149 | 150 | 可用 `init` 初始化或 `update` 更新课程信息 151 | 152 | ```json5 153 | { 154 | "ClassId": { // 属性名为会话 Id,不建议修改 155 | "CourseId": "", // 课程 Id,不建议修改 156 | "ClassId": "", // 班级 Id,不建议修改 157 | "ChatId": "", // 会话 Id,不建议修改 158 | "CourseName": "", // 课程名,不建议修改 159 | "ClassName": "", // 班级名,不建议修改 160 | 161 | // 可在此配置签到设置 162 | // 例如: 163 | "SignEnable": true, // 默认是不会自动签到的!!! 164 | } 165 | } 166 | ``` 167 | 168 | ### 拍照签到参数说明 169 | 170 | * `true` 或 `""` 等无效路径:一张黑图 171 | * `"."`:随机使用 `Images` 文件夹下的一张图片 172 | * `["1.png", "2.jpg"]` 数组:随机使用数组中的一张图片 173 | * 可使用绝对路径和相对路径,相对路径是相对于 `Images` 文件夹 174 | * 可指定文件夹,将随机使用文件夹下的一张图片,会 **遍历** 子文件夹 175 | 176 | 例如: 177 | 178 | * `"机房/"`:随机使用 `Images/机房` 文件夹下的一张图片 179 | * `["", "机房/", "电脑/", "老师.jpg"]`:将从黑图片、`Images/机房` 文件夹、`Images/电脑` 文件夹和 `Images/老师.jpg` 图片中随机使用一张图片 180 | 181 | 182 | ## WebApi 说明 183 | 184 |
185 | 详细 186 | 187 | WebApi 默认监听规则是 `http://localhost:5743`,可在配置文件中修改。 188 | 189 | 若要监听全部网卡的 5743 端口,可写为:`http://*:5743`。 190 | 191 | ### 查看状态 192 | 193 | 请求:`GET` `/status` 194 | 195 | 响应: 196 | 197 | ```jsonc 198 | { 199 | "username":"0000000000", // 学号或手机号 200 | "cxAutoSignEnabled":true // 是否启动自动签到,默认为 true 201 | } 202 | ``` 203 | 204 | ### 启动自动签到 205 | 206 | 请求:`GET` `/status/enable` 207 | 208 | 响应: 209 | 210 | ```jsonc 211 | { 212 | "code": 0, 213 | "msg":"success" 214 | } 215 | ``` 216 | 217 | ### 停止自动签到 218 | 219 | 请求:`GET` `/status/disable` 220 | 221 | 响应: 222 | 223 | ```jsonc 224 | { 225 | "code": 0, 226 | "msg":"success" 227 | } 228 | ``` 229 | 230 |
231 | 232 | ## FQA 233 | 234 | ### 1. 如何关闭自动更新检测? 235 | 在 `cx-auto-sign.dll` 所在目录下创建一个名为 `.noupdate` 的文件。 236 | 237 | ## 声明 238 | 239 | 一切开发旨在学习,请勿用于非法用途。 240 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/Controllers/StatusController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace cx_auto_sign.WebApi.Controllers 4 | { 5 | [ApiController] 6 | [Route("[controller]")] 7 | public class StatusController : ControllerBase 8 | { 9 | [HttpGet] 10 | public IActionResult Get() 11 | { 12 | return new JsonResult(IntervalData.Status); 13 | } 14 | 15 | [HttpGet("Enable")] 16 | public IActionResult Enable() 17 | { 18 | IntervalData.Status.CxAutoSignEnabled = true; 19 | return Ok(new { code = 0, msg = "success" }); 20 | } 21 | 22 | [HttpGet("Disable")] 23 | public IActionResult Disable() 24 | { 25 | IntervalData.Status.CxAutoSignEnabled = false; 26 | return Ok(new { code = 0, msg = "success" }); 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/IntervalData.cs: -------------------------------------------------------------------------------- 1 | namespace cx_auto_sign.WebApi 2 | { 3 | public static class IntervalData 4 | { 5 | public static Status Status { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Serilog; 5 | using Serilog.Events; 6 | 7 | namespace cx_auto_sign.WebApi 8 | { 9 | public static class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | Log.Logger = new LoggerConfiguration() 14 | .MinimumLevel.Debug() 15 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information) 16 | .Enrich.FromLogContext() 17 | .WriteTo.Console() 18 | .CreateLogger(); 19 | 20 | try 21 | { 22 | Log.Information("Starting web host"); 23 | CreateHostBuilder(args).Build().Run(); 24 | } 25 | catch (Exception ex) 26 | { 27 | Log.Fatal(ex, "Host terminated unexpectedly"); 28 | } 29 | finally 30 | { 31 | Log.CloseAndFlush(); 32 | } 33 | } 34 | 35 | private static IHostBuilder CreateHostBuilder(string[] args) => 36 | Host.CreateDefaultBuilder(args) 37 | .UseSerilog() 38 | .ConfigureWebHostDefaults(webBuilder => 39 | { 40 | webBuilder.UseStartup(); 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true 6 | }, 7 | "profiles": { 8 | "cx_auto_sign.WebApi": { 9 | "commandName": "Project", 10 | "launchBrowser": true, 11 | "launchUrl": "status", 12 | "applicationUrl": "http://localhost:5743", 13 | "environmentVariables": { 14 | "ASPNETCORE_ENVIRONMENT": "Development" 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace cx_auto_sign.WebApi 8 | { 9 | public class Startup 10 | { 11 | public static string Rule { get; set; } 12 | 13 | public Startup(IConfiguration configuration) 14 | { 15 | if (Rule != null) 16 | { 17 | // 动态修改监听规则 18 | configuration["Kestrel:EndPoints:Http:Url"] = Rule; 19 | } 20 | } 21 | 22 | // This method gets called by the runtime. Use this method to add services to the container. 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | services.AddControllers(); 26 | } 27 | 28 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 29 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 30 | { 31 | if (env.IsDevelopment()) 32 | { 33 | app.UseDeveloperExceptionPage(); 34 | } 35 | 36 | app.UseRouting(); 37 | 38 | // app.UseAuthorization(); 39 | 40 | app.UseEndpoints(endpoints => 41 | { 42 | endpoints.MapControllers(); 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/Status.cs: -------------------------------------------------------------------------------- 1 | namespace cx_auto_sign.WebApi 2 | { 3 | public class Status 4 | { 5 | // ReSharper disable once UnusedAutoPropertyAccessor.Global 6 | public string Username { get; init; } 7 | public bool CxAutoSignEnabled { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "Kestrel": { 11 | "EndPoints": { 12 | "Http": { 13 | "Url": "http://localhost:5743" 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cx-auto-sign.WebApi/cx-auto-sign.WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | cx_auto_sign.WebApi 6 | Library 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /cx-auto-sign.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30011.22 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "cx-auto-sign", "cx-auto-sign\cx-auto-sign.csproj", "{7D5A1B2D-2A90-4663-B2AF-F672E7602524}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CxSignHelper", "CxSignHelper\CxSignHelper.csproj", "{8D378115-C579-47FD-934C-87EDB284106C}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cx-auto-sign.WebApi", "cx-auto-sign.WebApi\cx-auto-sign.WebApi.csproj", "{B3E3D7FD-2DBE-4F3F-862E-53D9EF6CBD25}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {7D5A1B2D-2A90-4663-B2AF-F672E7602524}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {7D5A1B2D-2A90-4663-B2AF-F672E7602524}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {7D5A1B2D-2A90-4663-B2AF-F672E7602524}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {7D5A1B2D-2A90-4663-B2AF-F672E7602524}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {8D378115-C579-47FD-934C-87EDB284106C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {8D378115-C579-47FD-934C-87EDB284106C}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {8D378115-C579-47FD-934C-87EDB284106C}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {8D378115-C579-47FD-934C-87EDB284106C}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {B3E3D7FD-2DBE-4F3F-862E-53D9EF6CBD25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {B3E3D7FD-2DBE-4F3F-862E-53D9EF6CBD25}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {B3E3D7FD-2DBE-4F3F-862E-53D9EF6CBD25}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {B3E3D7FD-2DBE-4F3F-862E-53D9EF6CBD25}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {627DE1FA-1490-4BF2-BCD8-3CB080911FD4} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /cx-auto-sign/AppDataConfig.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Newtonsoft.Json.Linq; 3 | using Serilog; 4 | 5 | namespace cx_auto_sign 6 | { 7 | public class AppDataConfig: BaseDataConfig 8 | { 9 | private const string Name = "AppConfig.json5"; 10 | 11 | private readonly JObject _data; 12 | 13 | public AppDataConfig() 14 | { 15 | if (File.Exists(Name)) 16 | { 17 | _data = JObject.Parse(File.ReadAllText(Name)); 18 | } 19 | else 20 | { 21 | _data = new JObject(); 22 | _data.Merge(UserConfig.Default); 23 | _data.Merge(CourseConfig.Default); 24 | } 25 | } 26 | 27 | public override JToken GetData() 28 | { 29 | return _data; 30 | } 31 | 32 | public void Save() 33 | { 34 | Log.Debug("保存应用配置中..."); 35 | File.WriteAllText(Name, _data.ToString()); 36 | Log.Debug("已保存应用配置"); 37 | } 38 | 39 | public string DefaultUsername 40 | { 41 | get => GetString(nameof(DefaultUsername)); 42 | set => _data[nameof(DefaultUsername)] = value; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /cx-auto-sign/BaseConfig.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace cx_auto_sign 4 | { 5 | public abstract class BaseConfig 6 | { 7 | protected abstract JToken Get(string key); 8 | 9 | protected static JToken Get(JToken token) 10 | { 11 | return token == null || token.Type == JTokenType.Null ? null : token; 12 | } 13 | 14 | protected string GetString(string key) 15 | { 16 | return Get(key)?.Value(); 17 | } 18 | 19 | protected string GetMustString(string key) 20 | { 21 | var token = Get(key); 22 | return token?.Type == JTokenType.String ? token.Value() : null; 23 | } 24 | 25 | private int GetInt(string key, int def) 26 | { 27 | var token = Get(key); 28 | return token?.Type == JTokenType.Integer ? token.Value() : def; 29 | } 30 | 31 | protected int GetInt(string key) 32 | { 33 | return GetInt(key, 0); 34 | } 35 | 36 | protected bool GetBool(string key) 37 | { 38 | var token = Get(key); 39 | return token?.Type == JTokenType.Boolean && token.Value(); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /cx-auto-sign/BaseDataConfig.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace cx_auto_sign 4 | { 5 | public abstract class BaseDataConfig: BaseConfig 6 | { 7 | public abstract JToken GetData(); 8 | 9 | protected sealed override JToken Get(string key) 10 | { 11 | return GetData()?[key]; 12 | } 13 | 14 | public override string ToString() 15 | { 16 | return GetData().ToString(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /cx-auto-sign/CommandBase.cs: -------------------------------------------------------------------------------- 1 | using McMaster.Extensions.CommandLineUtils; 2 | using Serilog; 3 | using System.Threading.Tasks; 4 | 5 | namespace cx_auto_sign 6 | { 7 | [HelpOption("-h|--help")] 8 | public abstract class CommandBase 9 | { 10 | protected CommandBase() 11 | { 12 | Log.Logger = new LoggerConfiguration() 13 | .WriteTo.Console() 14 | .CreateLogger(); 15 | } 16 | 17 | // ReSharper disable once UnusedMemberHierarchy.Global 18 | protected virtual Task OnExecuteAsync(CommandLineApplication app) 19 | { 20 | return Task.FromResult(0); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cx-auto-sign/CourseConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using CxSignHelper; 7 | using CxSignHelper.Models; 8 | using Newtonsoft.Json.Linq; 9 | using Serilog; 10 | 11 | namespace cx_auto_sign 12 | { 13 | public class CourseConfig: BaseConfig 14 | { 15 | private const string ImageDir = "Images"; 16 | private const string ImageNoneId = "041ed4756ca9fdf1f9b6dde7a83f8794"; 17 | 18 | private static readonly string[] ImageSuffixes = { "png", "jpg", "jpeg", "bmp", "gif", "webp" }; 19 | 20 | private readonly JToken _app; 21 | private readonly JToken _user; 22 | private readonly JToken _course; 23 | 24 | public bool SignEnable => GetBool(nameof(SignEnable)); 25 | public int SignDelay => GetInt(nameof(SignDelay)); 26 | public string SignAddress => GetString(nameof(SignAddress)); 27 | public string SignLongitude => GetString(nameof(SignLongitude)); 28 | public string SignLatitude => GetString(nameof(SignLatitude)); 29 | public string SignClientIp => GetString(nameof(SignClientIp)); 30 | 31 | public static readonly JObject Default = new() 32 | { 33 | [nameof(SignEnable)] = false, 34 | [GetSignTypeKey(SignType.Normal)] = true, 35 | [GetSignTypeKey(SignType.Gesture)] = true, 36 | [GetSignTypeKey(SignType.Photo)] = true, 37 | [GetSignTypeKey(SignType.Location)] = true, 38 | [nameof(SignDelay)] = 0, 39 | [nameof(SignAddress)] = "中国", 40 | [nameof(SignLatitude)] = "-1", 41 | [nameof(SignLongitude)] = "-1", 42 | [nameof(SignClientIp)] = "1.1.1.1" 43 | }; 44 | 45 | public CourseConfig(BaseDataConfig app, BaseDataConfig user, BaseDataConfig course) 46 | { 47 | _app = app.GetData(); 48 | _user = user.GetData(); 49 | _course = course?.GetData(); 50 | } 51 | 52 | protected override JToken Get(string key) 53 | { 54 | return Get(_course?[key]) ?? 55 | Get(_user?[key]) ?? 56 | Get(_app?[key]) ?? 57 | Get(Default[key]); 58 | } 59 | 60 | private static string GetSignTypeKey(SignType type) 61 | { 62 | return "Sign" + type; 63 | } 64 | 65 | public SignOptions GetSignOptions(SignType type) 66 | { 67 | var config = Get(GetSignTypeKey(type)); 68 | if (config?.Type == JTokenType.Boolean && config.Value() == false) 69 | { 70 | return null; 71 | } 72 | return new SignOptions 73 | { 74 | Address = SignAddress, 75 | Latitude = SignLatitude, 76 | Longitude = SignLongitude, 77 | ClientIp = SignClientIp 78 | }; 79 | } 80 | 81 | private static string GetImageFullPath(string path) 82 | { 83 | if (string.IsNullOrEmpty(path)) 84 | { 85 | return null; 86 | } 87 | 88 | if (!Path.IsPathRooted(path)) 89 | { 90 | path = Path.Combine(ImageDir, path!); 91 | } 92 | return Path.GetFullPath(path); 93 | } 94 | 95 | private IEnumerable GetImageSet() 96 | { 97 | var set = new HashSet(); 98 | var photo = Get(GetSignTypeKey(SignType.Photo)); 99 | // ReSharper disable once InvertIf 100 | if (photo != null) 101 | { 102 | var type = photo.Type; 103 | // ReSharper disable once ConvertIfStatementToSwitchStatement 104 | if (type == JTokenType.String) 105 | { 106 | AddToImageSet(set, photo); 107 | } 108 | else if (type == JTokenType.Array) 109 | { 110 | foreach (var token in photo) 111 | { 112 | if (token.Type == JTokenType.String) 113 | { 114 | AddToImageSet(set, token); 115 | } 116 | } 117 | } 118 | } 119 | return set; 120 | } 121 | 122 | private static void AddFileToImageSet(ISet set, string path) 123 | { 124 | var name = Path.GetFileName(path); 125 | var index = name.LastIndexOf('.') + 1; 126 | if (index == 0) 127 | { 128 | return; 129 | } 130 | var suffix = name[index..].ToLower(); 131 | if (!ImageSuffixes.Contains(suffix)) 132 | { 133 | return; 134 | } 135 | set.Add(path); 136 | } 137 | 138 | private static void AddToImageSet(ISet set, string path) 139 | { 140 | if (string.IsNullOrEmpty(path)) 141 | { 142 | set.Add(""); 143 | return; 144 | } 145 | if (File.Exists(path)) 146 | { 147 | AddFileToImageSet(set, path); 148 | } 149 | else if (Directory.Exists(path)) 150 | { 151 | AddDirToImageSet(set, path); 152 | } 153 | } 154 | 155 | private static void AddToImageSet(ISet set, JToken token) 156 | { 157 | AddToImageSet(set, GetImageFullPath(token.Value())); 158 | } 159 | 160 | private static void AddDirToImageSet(ISet set, string path) 161 | { 162 | var infos = new DirectoryInfo(path).GetFileSystemInfos(); 163 | foreach (var info in infos) 164 | { 165 | var name = info.FullName; 166 | if ((info.Attributes & FileAttributes.Directory) != 0) 167 | { 168 | AddDirToImageSet(set, name); 169 | } 170 | else 171 | { 172 | AddFileToImageSet(set, name); 173 | } 174 | } 175 | } 176 | 177 | public async Task GetImageIdAsync(CxSignClient client, ILogger log) 178 | { 179 | var array = GetImageSet().ToArray(); 180 | var length = array.Length; 181 | if (length != 0) 182 | { 183 | log.Information("将从这些图片中随机选择一张进行图片签到:{Array}", array); 184 | var path = array[new Random().Next(length)]; 185 | if (!string.IsNullOrEmpty(path)) 186 | { 187 | log.Information("将使用这张照片进行图片签到:{Path}", path); 188 | try 189 | { 190 | return await client.UploadImageAsync(path); 191 | } 192 | catch (Exception e) 193 | { 194 | log.Error(e, "上传图片失败"); 195 | } 196 | } 197 | } 198 | log.Information("将使用一张黑图进行图片签到"); 199 | return ImageNoneId; 200 | } 201 | } 202 | } -------------------------------------------------------------------------------- /cx-auto-sign/CourseDataConfig.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace cx_auto_sign 4 | { 5 | public class CourseDataConfig: BaseDataConfig 6 | { 7 | private readonly JToken _data; 8 | 9 | public readonly string CourseId; 10 | public readonly string ClassId; 11 | public readonly string CourseName; 12 | 13 | public CourseDataConfig(JToken data) 14 | { 15 | _data = data; 16 | CourseId = GetString(nameof(CourseId)); 17 | ClassId = GetString(nameof(ClassId)); 18 | CourseName = GetString(nameof(CourseName)); 19 | } 20 | 21 | public override JToken GetData() 22 | { 23 | return _data; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /cx-auto-sign/Cxim.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | 5 | namespace cx_auto_sign 6 | { 7 | public static class Cxim 8 | { 9 | private static string Pack(byte[] data) 10 | { 11 | return new StringBuilder() 12 | .Append("[\"") 13 | .Append(Convert.ToBase64String(data)) 14 | .Append("\"]") 15 | .ToString(); 16 | } 17 | 18 | public static string BuildLoginPackage(string uid, string imToken) 19 | { 20 | var timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); 21 | using var s = new MemoryStream(); 22 | using var bw = new BinaryWriter(s); 23 | bw.Write(new byte[] { 0x08, 0x00, 0x12 }); 24 | bw.Write((byte)(52 + uid.Length)); // 接下来到 webim_{timestamp} 的内容长度 25 | bw.Write(new byte[] { 0x0a, 0x0e }); 26 | bw.Write(Encoding.ASCII.GetBytes("cx-dev#cxstudy")); 27 | bw.Write(new byte[] { 0x12 }); 28 | bw.Write((byte)uid.Length); 29 | bw.Write(Encoding.ASCII.GetBytes(uid)); 30 | bw.Write(new byte[] { 0x1a, 0x0b }); 31 | bw.Write(Encoding.ASCII.GetBytes("easemob.com")); 32 | bw.Write(new byte[] { 0x22, 0x13 }); 33 | bw.Write(Encoding.ASCII.GetBytes($"webim_{timestamp}")); 34 | bw.Write(new byte[] { 0x1a, 0x85, 0x01 }); 35 | bw.Write(Encoding.ASCII.GetBytes("$t$")); 36 | bw.Write(Encoding.ASCII.GetBytes($"{imToken}")); 37 | bw.Write(new byte[] { 0x40, 0x03, 0x4a, 0xc0, 0x01, 0x08, 0x10, 0x12, 0x05, 0x33, 0x2e, 0x30, 0x2e, 0x30, 0x28, 0x00, 0x30, 0x00, 0x4a, 0x0d }); 38 | bw.Write(Encoding.ASCII.GetBytes($"{timestamp}")); 39 | bw.Write(new byte[] { 0x62, 0x05, 0x77, 0x65, 0x62, 0x69, 0x6d, 0x6a, 0x13, 0x77, 0x65, 0x62, 0x69, 0x6d, 0x5f }); 40 | bw.Write(Encoding.ASCII.GetBytes($"{timestamp}")); 41 | bw.Write(new byte[] { 0x72, 0x85, 0x01, 0x24, 0x74, 0x24 }); 42 | bw.Write(Encoding.ASCII.GetBytes($"{imToken}")); 43 | bw.Write(new byte[] { 0x50, 0x00, 0x58, 0x00 }); 44 | return Pack(s.ToArray()); 45 | } 46 | 47 | public static string GetChatId(byte[] bytes) 48 | { 49 | var b = new byte[1]; 50 | Array.Copy(bytes, 9, b, 0, 1); 51 | var len = Convert.ToUInt32(b[0]); 52 | var id = new byte[len]; 53 | Array.Copy(bytes, 10, id, 0, len); 54 | return Encoding.UTF8.GetString(id); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /cx-auto-sign/InitCommand.cs: -------------------------------------------------------------------------------- 1 | using McMaster.Extensions.CommandLineUtils; 2 | using Serilog; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | 6 | namespace cx_auto_sign 7 | { 8 | [Command(Description = "登录超星学习通以初始化课程配置")] 9 | public class InitCommand : CommandBase 10 | { 11 | // ReSharper disable UnassignedGetOnlyAutoProperty 12 | [Option("-u", Description = "用户名(学号)")] 13 | [Required] 14 | private string Username { get; } 15 | 16 | [Option("-p", Description = "密码")] 17 | [Required] 18 | private string Password { get; } 19 | 20 | [Option("-f", Description = "学校代码")] 21 | private string Fid { get; } 22 | 23 | [Option("-d", Description = "设置为默认账号")] 24 | private bool IsSetDefault { get; } 25 | // ReSharper restore UnassignedGetOnlyAutoProperty 26 | 27 | protected override async Task OnExecuteAsync(CommandLineApplication app) 28 | { 29 | 30 | var userConfig = new UserDataConfig(Username, Password, Fid); 31 | await userConfig.UpdateAsync(); 32 | var appConfig = new AppDataConfig(); 33 | if (IsSetDefault || appConfig.DefaultUsername == null) 34 | { 35 | Log.Information("将 {Username} 设置为默认用户", Username); 36 | appConfig.DefaultUsername = Username; 37 | appConfig.Save(); 38 | } 39 | Log.Warning("开始自动签到请执行:dotnet ./cx-auto-sign.dll work"); 40 | Log.Information("程序执行完毕"); 41 | return await Task.FromResult(0); 42 | } 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /cx-auto-sign/Notification.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using CxSignHelper; 4 | using MailKit.Net.Smtp; 5 | using MimeKit; 6 | using Newtonsoft.Json.Linq; 7 | using RestSharp; 8 | using Serilog; 9 | using Serilog.Core; 10 | using Serilog.Events; 11 | 12 | namespace cx_auto_sign 13 | { 14 | public class Notification : ILogEventSink 15 | { 16 | private const string Title = "cx-auto-sign 自动签到通知"; 17 | private readonly StringBuilder _stringBuilder = new(); 18 | private readonly UserConfig _userConfig; 19 | private readonly Logger _log; 20 | 21 | private string _title; 22 | private bool? _ok; 23 | 24 | private Notification(Logger log, UserConfig userConfig) 25 | { 26 | _userConfig = userConfig; 27 | _log = log; 28 | } 29 | 30 | public void Emit(LogEvent logEvent) 31 | { 32 | if (logEvent.Level == LogEventLevel.Error) 33 | { 34 | bool? b; 35 | if ((b = GetBool(logEvent, nameof(Status))) != null) 36 | { 37 | _ok = b; 38 | return; 39 | } 40 | if ((b = GetBool(logEvent, nameof(Send))) != null && b == true) 41 | { 42 | if (_stringBuilder.Length == 0) 43 | { 44 | return; 45 | } 46 | if (_ok != null) 47 | { 48 | var status = _ok == true ? '✔' : '✖'; 49 | _stringBuilder.Insert(0, "自动签到 " + status + '\n'); 50 | _title = Title + ' ' + status; 51 | } 52 | var content = _stringBuilder.ToString(); 53 | NotifyByEmail(content); 54 | NotifyByServerChan(content); 55 | NotifyByPushPlus(content); 56 | return; 57 | } 58 | } 59 | _log.Write(logEvent); 60 | _stringBuilder 61 | .Append(logEvent.Timestamp.ToString("HH:mm:ss")) 62 | .Append(' ') 63 | .Append(logEvent.Level.ToString()[0]) 64 | .Append(' ') 65 | .Append(logEvent.RenderMessage()) 66 | .Append('\n'); 67 | if (logEvent.Exception != null) 68 | { 69 | _stringBuilder.Append(logEvent.Exception).Append('\n'); 70 | } 71 | } 72 | 73 | private string GetTitle() 74 | { 75 | return _title ?? Title; 76 | } 77 | 78 | private static bool? GetBool(LogEvent logEvent, string name) 79 | { 80 | if (logEvent.Properties.TryGetValue(name, out var value) 81 | && bool.TryParse(value.ToString(), out var b)) 82 | { 83 | return b; 84 | } 85 | return null; 86 | } 87 | 88 | private static void WriteBool(ILogger log, string name, bool b) 89 | { 90 | log.Write(new LogEvent(DateTimeOffset.Now, LogEventLevel.Error, null, MessageTemplate.Empty, 91 | new LogEventProperty[] 92 | { 93 | new(name, new ScalarValue(b)) 94 | }) 95 | ); 96 | } 97 | 98 | public static void Status(ILogger log, bool ok) 99 | { 100 | WriteBool(log, nameof(Status), ok); 101 | } 102 | 103 | public static void Send(ILogger log) 104 | { 105 | WriteBool(log, nameof(Send), true); 106 | } 107 | 108 | private static void NotifyByEmail(string subject, string text, string email, string host, int port, 109 | string user, string pass, bool secure = false) 110 | { 111 | var message = new MimeMessage(); 112 | message.From.Add(new MailboxAddress("cx-auto-sign", user)); 113 | message.To.Add(new MailboxAddress("cx-auto-sign", email)); 114 | message.Subject = subject; 115 | message.Body = new TextPart("plain") 116 | { 117 | Text = text 118 | }; 119 | using var client = new SmtpClient(); 120 | client.Connect(host, port, secure); 121 | client.Authenticate(user, pass); 122 | client.Send(message); 123 | client.Disconnect(true); 124 | } 125 | 126 | private void NotifyByEmail(string content) 127 | { 128 | if (string.IsNullOrEmpty(_userConfig.Email)) 129 | { 130 | _log.Warning($"由于 {nameof(UserConfig.Email)} 为空,没有发送邮件通知"); 131 | return; 132 | } 133 | if (string.IsNullOrEmpty(_userConfig.SmtpHost) || 134 | string.IsNullOrEmpty(_userConfig.SmtpUsername) || 135 | string.IsNullOrEmpty(_userConfig.SmtpPassword)) 136 | { 137 | _log.Error("邮件配置不正确"); 138 | return; 139 | } 140 | try 141 | { 142 | _log.Information("正在发送邮件通知"); 143 | NotifyByEmail(GetTitle(), content, 144 | _userConfig.Email, _userConfig.SmtpHost, _userConfig.SmtpPort, 145 | _userConfig.SmtpUsername, _userConfig.SmtpPassword, _userConfig.SmtpSecure); 146 | _log.Information("已发送邮件通知"); 147 | } 148 | catch (Exception e) 149 | { 150 | _log.Error(e, "发送邮件通知失败!"); 151 | } 152 | } 153 | 154 | private static void NotifyByServerChan(string key, string title, string text = null) 155 | { 156 | var client = new RestClient($"https://sctapi.ftqq.com/{key}.send?title={title}"); 157 | var request = new RestRequest(Method.POST); 158 | request.AddHeader("Content-Type", "application/x-www-form-urlencoded"); 159 | if (!string.IsNullOrEmpty(text)) 160 | { 161 | request.AddParameter("desp", "```text\n" + text + "\n```"); 162 | } 163 | var response = client.Execute(request); 164 | CxSignClient.TestResponseCode(response); 165 | var json = JObject.Parse(response.Content); 166 | if(json["data"]!["errno"]!.Value() != 0) 167 | { 168 | throw new Exception(json["data"]!["error"]!.ToString()); 169 | } 170 | } 171 | 172 | private void NotifyByServerChan(string content) 173 | { 174 | if (string.IsNullOrEmpty(_userConfig.ServerChanKey)) 175 | { 176 | _log.Warning($"由于 {nameof(UserConfig.ServerChanKey)} 为空,没有发送 ServerChan 通知"); 177 | return; 178 | } 179 | try 180 | { 181 | _log.Information("正在发送 ServerChan 通知"); 182 | NotifyByServerChan(_userConfig.ServerChanKey, GetTitle(), content); 183 | _log.Information("已发送 ServerChan 通知"); 184 | } 185 | catch (Exception e) 186 | { 187 | _log.Error(e, "发送 ServerChan 通知失败!"); 188 | } 189 | } 190 | 191 | private static void NotifyByPushPlus(string token, string title, string text) 192 | { 193 | var client = new RestClient("https://www.pushplus.plus/send"); 194 | var request = new RestRequest(Method.POST); 195 | request.AddJsonBody(new JObject 196 | { 197 | ["token"] = token, 198 | ["title"] = title, 199 | ["content"] = text, 200 | ["template"] = "txt" 201 | }.ToString()); 202 | var response = client.Execute(request); 203 | CxSignClient.TestResponseCode(response); 204 | var json = JObject.Parse(response.Content); 205 | if(json["code"]?.Value() != 200) 206 | { 207 | throw new Exception(json["msg"]?.ToString()); 208 | } 209 | } 210 | 211 | private void NotifyByPushPlus(string content) 212 | { 213 | if (string.IsNullOrEmpty(_userConfig.PushPlusToken)) 214 | { 215 | _log.Warning($"由于 {nameof(UserConfig.PushPlusToken)} 为空,没有发送 PushPlus 通知"); 216 | return; 217 | } 218 | try 219 | { 220 | _log.Information("正在发送 PushPlus 通知"); 221 | NotifyByPushPlus(_userConfig.PushPlusToken, GetTitle(), content); 222 | _log.Information("已发送 PushPlus 通知"); 223 | } 224 | catch (Exception e) 225 | { 226 | _log.Error(e, "发送 PushPlus 通知失败!"); 227 | } 228 | } 229 | 230 | public static Logger CreateLogger(UserConfig userConfig, double startTime) 231 | { 232 | var console = new LoggerConfiguration() 233 | .Enrich.WithProperty("StartTime", startTime) 234 | .WriteTo.Console(outputTemplate: 235 | "[{Timestamp:HH:mm:ss} {Level:u3}] [{StartTime}] {Message:lj}{NewLine}{Exception}") 236 | .CreateLogger(); 237 | return new LoggerConfiguration() 238 | .WriteTo.Sink(new Notification(console, userConfig)) 239 | .CreateLogger(); 240 | } 241 | } 242 | } -------------------------------------------------------------------------------- /cx-auto-sign/Program.cs: -------------------------------------------------------------------------------- 1 | using McMaster.Extensions.CommandLineUtils; 2 | using Newtonsoft.Json.Linq; 3 | using RestSharp; 4 | using System; 5 | using System.IO; 6 | using System.Net; 7 | using System.Reflection; 8 | using System.Threading.Tasks; 9 | 10 | namespace cx_auto_sign 11 | { 12 | [Command( 13 | Name = "cx-auto-sign", 14 | Description = "超星自动签到工具", 15 | ExtendedHelpText = @" 16 | 提示: 17 | 本程序采用 MIT 协议开源(https://github.com/cyanray/cx-auto-sign). 18 | 任何人可免费使用本程序并查看其源代码. 19 | ")] 20 | [VersionOptionFromMember("--version", MemberName = nameof(GetVersion))] 21 | [Subcommand( 22 | typeof(InitCommand), 23 | typeof(WorkCommand), 24 | typeof(UpdateCommand) 25 | )] 26 | internal class Program : CommandBase 27 | { 28 | private static async Task Main(string[] args) 29 | { 30 | await CheckUpdate(); 31 | return await CommandLineApplication.ExecuteAsync(args); 32 | } 33 | 34 | private static async Task CheckUpdate() 35 | { 36 | if (File.Exists(".noupdate")) 37 | { 38 | Console.WriteLine("已跳过检查更新"); 39 | return; 40 | } 41 | try 42 | { 43 | Console.WriteLine("正在检查更新..."); 44 | var (version, info) = await GetLatestVersion(); 45 | if (!version.Contains(GetVersion())) 46 | { 47 | Console.WriteLine($"发现新版本: {version}"); 48 | Console.WriteLine(info); 49 | Console.WriteLine("请前往 https://github.com/cyanray/cx-auto-sign/releases 下载更新,或者按任意键继续..."); 50 | Console.ReadKey(); 51 | } 52 | } 53 | catch (Exception ex) 54 | { 55 | Console.WriteLine(ex.Message); 56 | Console.WriteLine("获取版本信息失败,请访问 https://github.com/cyanray/cx-auto-sign/releases 检查是否有更新"); 57 | } 58 | } 59 | 60 | private static string GetVersion() 61 | => typeof(Program).Assembly.GetCustomAttribute()?.InformationalVersion; 62 | 63 | protected override async Task OnExecuteAsync(CommandLineApplication app) 64 | { 65 | app.ShowHelp(); 66 | return await base.OnExecuteAsync(app); 67 | } 68 | 69 | private static async Task<(string Version, string Info)> GetLatestVersion() 70 | { 71 | var client = new RestClient($"https://api.github.com/repos/cyanray/cx-auto-sign/releases/latest"); 72 | var request = new RestRequest(Method.GET); 73 | var response = await client.ExecuteGetAsync(request); 74 | var json = JObject.Parse(response.Content); 75 | if (response.StatusCode != HttpStatusCode.OK) 76 | { 77 | var message = string.Empty; 78 | if (json.ContainsKey("message")) 79 | { 80 | message = json["message"]!.Value(); 81 | } 82 | throw new Exception($"获取最新版本失败: {message}"); 83 | } 84 | var version = json["tag_name"]!.Value(); 85 | var info = json["body"]!.Value(); 86 | return (version, info); 87 | } 88 | 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /cx-auto-sign/UpdateCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using McMaster.Extensions.CommandLineUtils; 3 | using Serilog; 4 | 5 | namespace cx_auto_sign 6 | { 7 | [Command(Description = "更新课程")] 8 | public class UpdateCommand: CommandBase 9 | { 10 | // ReSharper disable UnassignedGetOnlyAutoProperty 11 | [Option("-u", Description = "指定用户名(学号)")] 12 | private string Username { get; } 13 | 14 | [Option("-a", Description = "更新全部用户")] 15 | private bool IsAll { get; } 16 | // ReSharper restore UnassignedGetOnlyAutoProperty 17 | 18 | protected override async Task OnExecuteAsync(CommandLineApplication app) 19 | { 20 | if (IsAll) 21 | { 22 | await UserDataConfig.UpdateAllAsync(); 23 | } 24 | else 25 | { 26 | var user = Username ?? new AppDataConfig().DefaultUsername; 27 | if (user == null) 28 | { 29 | Log.Error("没有设置用户和默认用户,可以使用 -u 指定用户"); 30 | return 1; 31 | } 32 | await UserDataConfig.UpdateAsync(user); 33 | } 34 | Log.Information("已完成更新"); 35 | return await base.OnExecuteAsync(app); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /cx-auto-sign/UserConfig.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | 3 | namespace cx_auto_sign 4 | { 5 | public class UserConfig: BaseConfig 6 | { 7 | private readonly JToken _app; 8 | private readonly JToken _user; 9 | 10 | // Notification 11 | public readonly string ServerChanKey; 12 | public readonly string PushPlusToken; 13 | 14 | // Email 15 | public readonly string Email; 16 | public readonly string SmtpHost; 17 | public readonly int SmtpPort; 18 | public readonly string SmtpUsername; 19 | public readonly string SmtpPassword; 20 | public readonly bool SmtpSecure; 21 | 22 | public static readonly JObject Default = new() 23 | { 24 | [nameof(ServerChanKey)] = "", 25 | [nameof(PushPlusToken)] = "", 26 | 27 | [nameof(Email)] = "", 28 | [nameof(SmtpHost)] = "", 29 | [nameof(SmtpPort)] = 0, 30 | [nameof(SmtpUsername)] = "", 31 | [nameof(SmtpPassword)] = "", 32 | [nameof(SmtpSecure)] = false 33 | }; 34 | 35 | public UserConfig(BaseDataConfig app, BaseDataConfig user) 36 | { 37 | _app = app?.GetData(); 38 | _user = user?.GetData(); 39 | 40 | ServerChanKey = GetMustString(nameof(ServerChanKey)); 41 | PushPlusToken = GetMustString(nameof(PushPlusToken)); 42 | 43 | Email = GetMustString(nameof(Email)); 44 | SmtpHost = GetString(nameof(SmtpHost)); 45 | SmtpPort = GetInt(nameof(SmtpPort)); 46 | SmtpUsername = GetString(nameof(SmtpUsername)); 47 | SmtpPassword = GetString(nameof(SmtpPassword)); 48 | SmtpSecure = GetBool(nameof(SmtpSecure)); 49 | } 50 | 51 | protected override JToken Get(string key) 52 | { 53 | return Get(_user?[key]) ?? 54 | Get(_app?[key]) ?? 55 | Get(Default[key]); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /cx-auto-sign/UserDataConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using CxSignHelper; 5 | using Newtonsoft.Json.Linq; 6 | using Serilog; 7 | 8 | namespace cx_auto_sign 9 | { 10 | public class UserDataConfig: BaseDataConfig 11 | { 12 | private const string Dir = "Configs"; 13 | private const string KeyCourse = "Course"; 14 | 15 | private readonly string _path; 16 | 17 | private readonly JObject _data; 18 | private readonly JObject _courses; 19 | 20 | public readonly string Username; 21 | public readonly string Password; 22 | public readonly string Fid; 23 | public readonly JToken WebApi; 24 | 25 | private UserDataConfig(FileSystemInfo file): this(file.FullName, null, null, null) 26 | { 27 | _path = file.FullName; 28 | Username = GetString(nameof(Username)); 29 | Password = GetString(nameof(Password)); 30 | Fid = GetString(nameof(Fid)); 31 | } 32 | 33 | public UserDataConfig(string user, string pass = null, string fid = null) 34 | : this(GetPath(user), user, pass, fid) { } 35 | 36 | private UserDataConfig(string path, string user, string pass, string fid) 37 | { 38 | _path = path; 39 | if (File.Exists(_path)) 40 | { 41 | _data = JObject.Parse(File.ReadAllText(_path)); 42 | _courses = (JObject) (_data[KeyCourse] ?? (_data[KeyCourse] = new JObject())); 43 | if (user != null) 44 | { 45 | SetAuth(nameof(Username), user); 46 | SetAuth(nameof(Password), pass); 47 | SetAuth(nameof(Fid), fid); 48 | } 49 | Username = GetString(nameof(Username)); 50 | Password = GetString(nameof(Password)); 51 | Fid = GetString(nameof(Fid)); 52 | WebApi = Get(nameof(WebApi)); 53 | } 54 | else 55 | { 56 | if (pass == null) 57 | { 58 | throw new Exception("不存在该用户的配置"); 59 | } 60 | _data = new JObject 61 | { 62 | [nameof(Username)] = Username = user, 63 | [nameof(Password)] = Password = pass, 64 | [nameof(Fid)] = Fid = fid, 65 | [nameof(WebApi)] = WebApi = false, 66 | [KeyCourse] = _courses = new JObject() 67 | }; 68 | } 69 | } 70 | 71 | public override JToken GetData() 72 | { 73 | return _data; 74 | } 75 | 76 | public CourseDataConfig GetCourse(string chatId) 77 | { 78 | return new CourseDataConfig(_courses?[chatId]); 79 | } 80 | 81 | private void Save() 82 | { 83 | if (!Directory.Exists(Dir)) 84 | { 85 | Log.Debug("没有用户配置文件夹,并创建:{Dir}", Dir); 86 | Directory.CreateDirectory(Dir); 87 | Log.Debug("已创建用户配置文件夹:{Dir}", Dir); 88 | } 89 | Log.Debug("保存用户配置中..."); 90 | File.WriteAllText(_path, _data.ToString()); 91 | Log.Debug("已保存用户配置"); 92 | } 93 | 94 | private void SetAuth(string key, string val) 95 | { 96 | if (val == null) 97 | { 98 | return; 99 | } 100 | 101 | var token = Get(key); 102 | if (token == null || token.Type == JTokenType.String && token.Value() != key) 103 | { 104 | _data[key] = val; 105 | } 106 | } 107 | 108 | private static string GetPath(string user) 109 | { 110 | return Dir + "/" + user + ".json5"; 111 | } 112 | 113 | public async Task UpdateAsync() 114 | { 115 | Log.Information("正在登录账号:{Username}", Username); 116 | var client = await CxSignClient.LoginAsync(Username, Password, Fid); 117 | Log.Information("成功登录账号"); 118 | 119 | Log.Information("获取课程数据中..."); 120 | await client.GetCoursesAsync(_courses); 121 | foreach (var (_, course) in _courses) 122 | { 123 | Log.Information("发现课程:{CourseName}-{ClassName} ({CourseId}, {ClassId})", 124 | course["CourseName"], course["ClassName"], 125 | course["CourseId"], course["ClassId"]); 126 | } 127 | Save(); 128 | } 129 | 130 | public static async Task UpdateAsync(string user) 131 | { 132 | var path = GetPath(user); 133 | var file = new FileInfo(path); 134 | if (!file.Exists) 135 | { 136 | throw new Exception("不存在该用户的配置"); 137 | } 138 | await new UserDataConfig(file).UpdateAsync(); 139 | } 140 | 141 | public static async Task UpdateAllAsync() 142 | { 143 | var dir = new DirectoryInfo(Dir); 144 | if (!dir.Exists) 145 | { 146 | return; 147 | } 148 | var infos = dir.GetFiles(); 149 | foreach (var file in infos) 150 | { 151 | try 152 | { 153 | await new UserDataConfig(file).UpdateAsync(); 154 | } 155 | catch (Exception e) 156 | { 157 | Log.Error(e, "更新失败"); 158 | } 159 | } 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /cx-auto-sign/WorkCommand.cs: -------------------------------------------------------------------------------- 1 | using CxSignHelper; 2 | using CxSignHelper.Models; 3 | using McMaster.Extensions.CommandLineUtils; 4 | using Newtonsoft.Json.Linq; 5 | using Serilog; 6 | using System; 7 | using System.Linq; 8 | using System.Net.WebSockets; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Serilog.Core; 12 | using Websocket.Client; 13 | 14 | namespace cx_auto_sign 15 | { 16 | [Command(Description = "工作模式, 监听签到任务并自动签到")] 17 | public class WorkCommand : CommandBase 18 | { 19 | private static readonly DateTime DateTime1970 = new(1970, 1, 1, 8, 0, 0); 20 | 21 | // ReSharper disable UnassignedGetOnlyAutoProperty 22 | [Option("-u", Description = "指定用户名(学号)")] 23 | private string Username { get; } 24 | // ReSharper restore UnassignedGetOnlyAutoProperty 25 | 26 | private WebsocketClient _ws; 27 | 28 | protected override async Task OnExecuteAsync(CommandLineApplication app) 29 | { 30 | var appConfig = new AppDataConfig(); 31 | var user = Username ?? appConfig.DefaultUsername; 32 | if (user == null) 33 | { 34 | Log.Error("没有设置用户,可以使用 -u 指定用户"); 35 | return 1; 36 | } 37 | 38 | var userConfig = new UserDataConfig(user); 39 | var auConfig = new UserConfig(appConfig, userConfig); 40 | var username = userConfig.Username; 41 | var password = userConfig.Password; 42 | var fid = userConfig.Fid; 43 | 44 | Log.Information("正在登录账号:{Username}", username); 45 | var client = await CxSignClient.LoginAsync(username, password, fid); 46 | Log.Information("成功登录账号"); 47 | var (imToken, uid) = await client.GetImTokenAsync(); 48 | 49 | var enableWeiApi = false; 50 | var webApi = userConfig.WebApi; 51 | if (webApi != null) 52 | { 53 | string rule = null; 54 | // ReSharper disable once ConvertIfStatementToSwitchStatement 55 | if (webApi.Type == JTokenType.Boolean) 56 | { 57 | if (webApi.Value()) 58 | { 59 | rule = "http://localhost:5743"; 60 | } 61 | } 62 | else if (webApi.Type == JTokenType.String) 63 | { 64 | rule = webApi.Value(); 65 | } 66 | 67 | if (rule != null) 68 | { 69 | // 启动 WebApi 服务 70 | enableWeiApi = true; 71 | Log.Information("启动 WebApi 服务"); 72 | WebApi.Startup.Rule = rule; 73 | WebApi.IntervalData.Status = new WebApi.Status 74 | { 75 | Username = username, 76 | CxAutoSignEnabled = true 77 | }; 78 | _ = Task.Run(() => { WebApi.Program.Main(null); }); 79 | } 80 | } 81 | 82 | // 创建 Websocket 对象,监听消息 83 | var exitEvent = new ManualResetEvent(false); 84 | var url = new Uri("wss://im-api-vip6-v2.easemob.com/ws/032/xvrhfd2j/websocket"); 85 | using (_ws = new WebsocketClient(url, () => new ClientWebSocket 86 | { 87 | Options = 88 | { 89 | KeepAliveInterval = TimeSpan.FromMilliseconds(-1) 90 | } 91 | })) 92 | { 93 | _ws.ReconnectionHappened.Subscribe(info => 94 | Log.Warning("CXIM: Reconnection happened, type: {Type}", info.Type)); 95 | 96 | async void OnMessageReceived(ResponseMessage msg) 97 | { 98 | var startTime = GetTimestamp(); 99 | try 100 | { 101 | Log.Information("CXIM: Message received: {Message}", msg); 102 | if (msg.Text.StartsWith("o")) 103 | { 104 | Log.Information("CXIM 登录"); 105 | var loginPackage = Cxim.BuildLoginPackage(uid, imToken); 106 | Log.Information("CXIM: Message send: {Message}", loginPackage); 107 | _ws.Send(loginPackage); 108 | return; 109 | } 110 | 111 | if (!msg.Text.StartsWith("a")) 112 | { 113 | return; 114 | } 115 | var arrMsg = JArray.Parse(msg.Text[1..]); 116 | foreach (var message in arrMsg) 117 | { 118 | Logger log = null; 119 | try 120 | { 121 | var pkgBytes = Convert.FromBase64String(message.Value()); 122 | if (pkgBytes.Length <= 5) 123 | { 124 | continue; 125 | } 126 | 127 | var header = new byte[5]; 128 | Array.Copy(pkgBytes, header, 5); 129 | if (!header.SequenceEqual(new byte[] { 0x08, 0x00, 0x40, 0x02, 0x4a })) 130 | { 131 | continue; 132 | } 133 | 134 | if (pkgBytes[5] != 0x2b) 135 | { 136 | Log.Warning("可能不是课程消息"); 137 | continue; 138 | } 139 | 140 | Log.Information("接收到课程消息"); 141 | 142 | string chatId; 143 | try 144 | { 145 | chatId = Cxim.GetChatId(pkgBytes); 146 | } 147 | catch (Exception e) 148 | { 149 | throw new Exception("解析失败,无法获取 ChatId", e); 150 | } 151 | 152 | log = Notification.CreateLogger(auConfig, GetTimestamp()); 153 | log.Information("消息时间:{Time}", startTime); 154 | log.Information("ChatId: {ChatId}", chatId); 155 | 156 | var course = userConfig.GetCourse(chatId); 157 | log.Information("获取 {CourseName} 签到任务中", course.CourseName); 158 | var courseConfig = new CourseConfig(appConfig, userConfig, course); 159 | var tasks = await client.GetSignTasksAsync(course.CourseId, course.ClassId); 160 | if (tasks.Count == 0) 161 | { 162 | Log.Error("没有活动任务"); 163 | log = null; 164 | continue; 165 | } 166 | 167 | var task = tasks[0]; 168 | var taskTime = task["startTime"]!.Value(); 169 | log.Information("任务时间: {Time}", taskTime); 170 | var takenTime = startTime - taskTime; 171 | log.Information("消息与任务相差: {Time}ms", takenTime); 172 | if (takenTime > 5000) 173 | { 174 | // 当教师发布作业的等操作也触发「接收到课程消息」 175 | // 但这些操作不会体现在「活动列表」中 176 | // 因此,这里通过活动开始的时间来判断接收到的是否是活动消息 177 | Log.Warning("不是活动消息"); 178 | log = null; 179 | continue; 180 | } 181 | var type = task["type"]; 182 | if (type?.Type != JTokenType.Integer || type.Value() != 2) 183 | { 184 | Log.Warning("不是签到任务"); 185 | log = null; 186 | continue; 187 | } 188 | 189 | var activeId = task["id"]?.ToString(); 190 | if (string.IsNullOrEmpty(activeId)) 191 | { 192 | Log.Error("解析失败,ActiveId 为空"); 193 | log = null; 194 | continue; 195 | } 196 | log.Information("准备签到 ActiveId: {ActiveId}", activeId); 197 | 198 | var data = await client.GetActiveDetailAsync(activeId); 199 | var signType = GetSignType(data); 200 | log.Information("签到类型:{Type}", GetSignTypeName(signType)); 201 | // ReSharper disable once ConvertIfStatementToSwitchStatement 202 | if (signType == SignType.Gesture) 203 | { 204 | log.Information("手势:{Code}", data["signCode"]?.Value()); 205 | } 206 | else if (signType == SignType.Qr) 207 | { 208 | log.Warning("暂时无法二维码签到"); 209 | continue; 210 | } 211 | 212 | if (enableWeiApi && !WebApi.IntervalData.Status.CxAutoSignEnabled) 213 | { 214 | log.Information("因 WebApi 设置,跳过签到"); 215 | continue; 216 | } 217 | 218 | if (!courseConfig.SignEnable) 219 | { 220 | log.Information("因用户配置,跳过签到"); 221 | continue; 222 | } 223 | 224 | var signOptions = courseConfig.GetSignOptions(signType); 225 | if (signOptions == null) 226 | { 227 | log.Warning("因用户课程配置,跳过签到"); 228 | continue; 229 | } 230 | 231 | if (signType == SignType.Photo) 232 | { 233 | signOptions.ImageId = await courseConfig.GetImageIdAsync(client, log); 234 | log.Information("预览:{Url}", 235 | $"https://p.ananas.chaoxing.com/star3/170_220c/{signOptions.ImageId}"); 236 | } 237 | log.Information("签到准备完毕,耗时:{Time}ms", 238 | GetTimestamp() - startTime); 239 | takenTime = GetTimestamp() - taskTime; 240 | log.Information("签到已发布:{Time}ms", takenTime); 241 | var delay = courseConfig.SignDelay; 242 | log.Information("用户配置延迟签到:{Time}s", delay); 243 | if (delay > 0) 244 | { 245 | delay = (int) (delay * 1000 - takenTime); 246 | if (delay > 0) 247 | { 248 | log.Information("将等待:{Delay}ms", delay); 249 | await Task.Delay(delay); 250 | } 251 | } 252 | 253 | log.Information("开始签到"); 254 | var ok = false; 255 | var content = await client.SignAsync(activeId, signOptions); 256 | switch (content) 257 | { 258 | case "success": 259 | content = "签到完成"; 260 | ok = true; 261 | break; 262 | case "您已签到过了": 263 | ok = true; 264 | break; 265 | default: 266 | log.Error("签到失败"); 267 | break; 268 | } 269 | log.Information(content); 270 | Notification.Status(log, ok); 271 | } 272 | catch (Exception e) 273 | { 274 | (log ?? Log.Logger).Error(e, "CXIM 接收到课程消息时出错"); 275 | } 276 | finally 277 | { 278 | if (log != null) 279 | { 280 | Notification.Send(log); 281 | } 282 | } 283 | } 284 | } 285 | catch (Exception e) 286 | { 287 | Log.Error(e, "CXIM 接收到消息处理时出错"); 288 | } 289 | } 290 | 291 | _ws.MessageReceived.Subscribe(OnMessageReceived); 292 | await _ws.Start(); 293 | exitEvent.WaitOne(); 294 | } 295 | 296 | Console.ReadKey(); 297 | 298 | return 0; 299 | } 300 | 301 | private static SignType GetSignType(JToken data) 302 | { 303 | var otherId = data["otherId"].Value(); 304 | switch (otherId) 305 | { 306 | case 2: 307 | return SignType.Qr; 308 | case 3: 309 | return SignType.Gesture; 310 | case 4: 311 | return SignType.Location; 312 | default: 313 | var token = data["ifphoto"]; 314 | return token?.Type == JTokenType.Integer && token.Value() != 0 315 | ? SignType.Photo 316 | : SignType.Normal; 317 | } 318 | } 319 | 320 | private static string GetSignTypeName(SignType type) 321 | { 322 | return type switch 323 | { 324 | SignType.Normal => "普通签到", 325 | SignType.Photo => "图片签到", 326 | SignType.Qr => "二维码签到", 327 | SignType.Gesture => "手势签到", 328 | SignType.Location => "位置签到", 329 | _ => throw new ArgumentOutOfRangeException(nameof(type), type, null) 330 | }; 331 | } 332 | 333 | private static double GetTimestamp() 334 | { 335 | return (DateTime.Now - DateTime1970).TotalMilliseconds; 336 | } 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /cx-auto-sign/cx-auto-sign.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | cx_auto_sign 7 | https://github.com/cyanray/cx-auto-sign 8 | 2.0.0 9 | 2.0.0.0 10 | 2.0.0.0 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "5.0.100", 4 | "rollForward": "latestMajor" 5 | } 6 | } 7 | --------------------------------------------------------------------------------