├── .dockerignore ├── .gitattributes ├── .gitignore ├── CFPABot.sln ├── CFPABot ├── .dockerignore ├── Azusa │ ├── App.razor │ ├── ForkRepoManager.cs │ ├── LoginManager.cs │ ├── PRCreatorModule.cs │ ├── Pages │ │ ├── CommandList.razor │ │ ├── Debug.razor │ │ ├── Diff.razor │ │ ├── EditPR.razor │ │ ├── Error.cshtml │ │ ├── Error.cshtml.cs │ │ ├── GitHubInCNHelper.razor │ │ ├── Index.razor │ │ ├── MappingsManager.razor │ │ ├── ModList.razor │ │ ├── PRCreator.razor │ │ ├── PREditor.razor │ │ ├── PRLLMReview.razor │ │ ├── PRList.razor │ │ ├── SortLangFile.razor │ │ ├── SpecialDiff.razor │ │ ├── SpecialDiffSegment.razor │ │ ├── WeeklyReport.razor │ │ ├── _Host.cshtml │ │ └── _Layout.cshtml │ ├── Shared │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ └── NavMenu.razor.css │ ├── _Imports.razor │ ├── wwwroot │ │ ├── _framework │ │ │ └── blazor.server.js │ │ ├── css │ │ │ ├── BlazorStrap.V5.bundle.scp.css │ │ │ ├── BlazorStrap.bundle.scp.css │ │ │ ├── _content │ │ │ │ ├── BlazorStrap.V5 │ │ │ │ │ └── BlazorStrap.V5.bundle.scp.css │ │ │ │ └── BlazorStrap │ │ │ │ │ └── BlazorStrap.bundle.scp.css │ │ │ ├── bootstrap │ │ │ │ ├── bootstrap.min.css │ │ │ │ └── bootstrap.min.css.map │ │ │ ├── open-iconic │ │ │ │ ├── FONT-LICENSE │ │ │ │ ├── ICON-LICENSE │ │ │ │ ├── README.md │ │ │ │ └── font │ │ │ │ │ ├── css │ │ │ │ │ └── open-iconic-bootstrap.min.css │ │ │ │ │ └── fonts │ │ │ │ │ ├── open-iconic.eot │ │ │ │ │ ├── open-iconic.otf │ │ │ │ │ ├── open-iconic.svg │ │ │ │ │ ├── open-iconic.ttf │ │ │ │ │ └── open-iconic.woff │ │ │ └── site.css │ │ ├── favicon.ico │ │ └── img │ │ │ └── tip1.png │ └── wwwroot2 │ │ ├── FileName.cs │ │ └── _content │ │ ├── BlazorStrap │ │ ├── blazorstrap.js │ │ └── popper.min.js │ │ ├── cfpabot.js │ │ └── editor.js ├── CFPABot - Backup.csproj ├── CFPABot.csproj ├── CFPALLM │ ├── CFPALLMManager.cs │ └── LangDataNormalizer.cs ├── Checks │ ├── LabelCheck.cs │ └── Labeler.cs ├── Command │ ├── CommandProcessor.cs │ └── GitRepoManager.cs ├── CompositionHandler │ └── CompositionFileHandler.cs ├── Controllers │ ├── BMCLController.cs │ ├── CFPAToolsController.cs │ ├── CompareController.cs │ ├── GitHubOAuthController.cs │ ├── MyWebhookEventProcessor.cs │ ├── UtilsController.cs │ └── WebhookListenerController.cs ├── CronTasks.cs ├── DiffEngine │ ├── LangDiffer.cs │ ├── LangFile.cs │ ├── LangFileFetcher.cs │ └── LangFilePath.cs ├── Dockerfile ├── Exceptions │ ├── CheckException.cs │ ├── CommandExceptions.cs │ ├── LangFileNotExistsException.cs │ ├── WTFException.cs │ └── WebhookException.cs ├── IDK │ └── TranslationRequestUpdater.cs ├── LanguageCore │ ├── JsonFormatter.cs │ └── LangFormatter.cs ├── Models │ ├── ArtifactModel.cs │ └── WorkflowRunModel.cs ├── PRData │ └── PRDataManager.cs ├── Program.cs ├── ProjectHex │ └── ProjectHex.cs ├── Properties │ └── launchSettings.example.json ├── Resources │ ├── Locale.Designer.cs │ ├── Locale.resx │ └── Minecraft-Terms-104816.json ├── Startup.cs ├── Utils │ ├── CommentBuilder.cs │ ├── Constants.cs │ ├── CurseManager.cs │ ├── Downloader.cs │ ├── ExFormatter.cs │ ├── FileUtils.cs │ ├── GitHub.cs │ ├── GlobalGitRepoCache.cs │ ├── KeyAnalyzer.cs │ ├── MCVersion.cs │ ├── MailUtils.cs │ ├── ModKeyAnalyzer.cs │ ├── Models │ │ └── GitHubPRReviewModel.cs │ ├── ModrinthManager.cs │ ├── PRAnalyzer.cs │ ├── PRRelationAnalyzer.cs │ ├── RepoAnalyzer.cs │ ├── StreamExtensions.cs │ ├── TermManager.cs │ └── WeeklyReportHelper.cs ├── appsettings.Development.json └── appsettings.json ├── LICENSE ├── README.md ├── build-from-codespace.sh └── docker-compose.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md 26 | **/bin/ 27 | **/obj/ -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CFPABot.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.33627.172 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CFPABot", "CFPABot\CFPABot.csproj", "{9B26E127-EB11-4DA5-8975-59250D691A23}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {9B26E127-EB11-4DA5-8975-59250D691A23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {9B26E127-EB11-4DA5-8975-59250D691A23}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {9B26E127-EB11-4DA5-8975-59250D691A23}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {9B26E127-EB11-4DA5-8975-59250D691A23}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {B14D8BFB-86B2-4662-8F65-D5B935B8C661} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /CFPABot/.dockerignore: -------------------------------------------------------------------------------- 1 | **/bin/ 2 | **/obj/ -------------------------------------------------------------------------------- /CFPABot/Azusa/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 | 正在加载...或者页面不存在 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /CFPABot/Azusa/ForkRepoManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using CFPABot.Exceptions; 8 | using CFPABot.Utils; 9 | using Serilog; 10 | 11 | namespace CFPABot.Azusa 12 | { 13 | public sealed class ForkRepoManager : IDisposable 14 | { 15 | string _token; 16 | public string WorkingDirectory { get; } 17 | 18 | public ForkRepoManager(string token = null) 19 | { 20 | WorkingDirectory = $"/app/caches/repos/{Guid.NewGuid():N}"; 21 | token ??= Constants.GitHubOAuthToken; 22 | _token = token; 23 | Directory.CreateDirectory(WorkingDirectory); 24 | } 25 | 26 | public void Clone(string repoOwner, string repoName, string userName = null, string userEmail = null, string branch = null) 27 | { 28 | Run($"clone {(branch == null ? "" : $"-b {branch}")} https://x-access-token:{_token}@github.com/{repoOwner}/{repoName}.git --depth=1 --reference-if-able /app/repo-cache ."); 29 | if (userEmail != null) 30 | { 31 | Run($"config user.name \"{userName}\""); 32 | Run($"config user.email \"{userEmail}\""); 33 | } 34 | } 35 | 36 | 37 | public void Commit(string message) 38 | { 39 | Run("commit -m \"" + 40 | $"{message.Replace('\"', '*')}" + 41 | "\""); 42 | } 43 | 44 | public void Push() 45 | { 46 | Run("push"); 47 | } 48 | 49 | public void AddAllFiles() 50 | { 51 | Run("add -A"); 52 | } 53 | 54 | public void Run(string args) 55 | { 56 | var process = Process.Start(new ProcessStartInfo("git", args) { RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = WorkingDirectory }); 57 | var stdout = ""; 58 | var stderr = ""; 59 | process.OutputDataReceived += (sender, eventArgs) => { stdout += eventArgs.Data; }; 60 | process.ErrorDataReceived += (sender, eventArgs) => { stderr += eventArgs.Data; }; 61 | process.BeginOutputReadLine(); 62 | process.BeginErrorReadLine(); 63 | process.WaitForExit(); 64 | if (process.ExitCode != 0) 65 | { 66 | // haha 67 | // https://github.com/Cyl18/CFPABot/issues/3 68 | // maybe Regex.Replace(message, "ghs_[0-9a-zA-Z]{36}", "******") 69 | Log.Error($"git.exe {args} exited with {process.ExitCode} - {stdout}{stderr}"); 70 | throw new ProcessException($"git.exe with args `{args}` exited with {process.ExitCode}."); 71 | } 72 | 73 | } 74 | 75 | public void Dispose() 76 | { 77 | try 78 | { 79 | 80 | Directory.Delete(WorkingDirectory, true); 81 | } 82 | catch (Exception e) 83 | { 84 | Log.Warning(e, "clean git repo"); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CFPABot/Azusa/LoginManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CFPABot.Utils; 6 | using Microsoft.AspNetCore.Http; 7 | using NETCore.Encrypt; 8 | using Octokit; 9 | using Octokit.Internal; 10 | using Serilog; 11 | 12 | namespace CFPABot.Azusa 13 | { 14 | public class LoginManager 15 | { 16 | 17 | public static string LoginUrl => 18 | $"https://github.com/login/oauth/authorize?client_id={Constants.GitHubOAuthClientId}&scope=user:email%20public_repo%20workflow"; // &prompt=consent 19 | 20 | public static string GetToken(IHttpContextAccessor http) 21 | { 22 | if (!http.HttpContext!.Request.Cookies.TryGetValue(Constants.GitHubOAuthTokenCookieName, out var token)) 23 | { 24 | return null; 25 | } 26 | 27 | return EncryptProvider.AESDecrypt(token, File.ReadAllText("config/encrypt_key.txt"), "CACTUS&MAMARUO!!"); 28 | } 29 | 30 | public static async Task IsAdmin(IHttpContextAccessor http) 31 | { 32 | var client = GetGitHubClient(http); 33 | if (client == null) return false; 34 | 35 | var user = await client.User.Current().ConfigureAwait(false); 36 | var hasPermission = 37 | await GitHub.Instance.Repository.Collaborator 38 | .IsCollaborator(Constants.Owner, Constants.RepoName, user.Login).ConfigureAwait(false); 39 | 40 | return hasPermission; 41 | } 42 | 43 | public static async Task HasPrPermission(IHttpContextAccessor http, int prid) 44 | { 45 | var client = GetGitHubClient(http); 46 | if (client == null) return false; 47 | 48 | var user = await client.User.Current().ConfigureAwait(false); 49 | var pr = await GitHub.GetPullRequest(prid).ConfigureAwait(false); 50 | var hasPermission = 51 | await GitHub.Instance.Repository.Collaborator 52 | .IsCollaborator(Constants.Owner, Constants.RepoName, user.Login).ConfigureAwait(false); 53 | 54 | if (!hasPermission && (pr.User.Login == user.Login)) 55 | { 56 | // 没有最高权限 但是发送命令的人是 PR 创建者 57 | if (pr.Head.Repository.Owner.Login == user.Login) 58 | { 59 | hasPermission = true; 60 | } 61 | else 62 | { 63 | return false; 64 | } 65 | } 66 | 67 | return hasPermission; 68 | } 69 | 70 | public static bool GetLoginStatus(IHttpContextAccessor http) 71 | { 72 | return http.HttpContext!.Request.Cookies.TryGetValue(Constants.GitHubOAuthTokenCookieName, out _); 73 | } 74 | public static GitHubClient GetGitHubClient(IHttpContextAccessor http) 75 | { 76 | var token = GetToken(http); 77 | if (token == null) return null; 78 | 79 | return GetGitHubClient(token); 80 | } 81 | 82 | public static async Task GetEmails(IHttpContextAccessor http) 83 | { 84 | var client = GetGitHubClient(http); 85 | try 86 | { 87 | var user = await client.User.Current().ConfigureAwait(false); 88 | var aEmail = $"{user.Id}+{user.Login}@users.noreply.github.com"; 89 | try 90 | { 91 | var readOnlyList = await client.User.Email.GetAll(); 92 | return readOnlyList.Select(x => x.Email).Append(aEmail).Distinct().ToArray(); 93 | } 94 | catch (Exception e) 95 | { 96 | Log.Error(e, "email"); 97 | return new[] {aEmail}; 98 | } 99 | } 100 | catch (Exception e) 101 | { 102 | Log.Error(e, "email"); 103 | return new []{ "cyl18a+error@gmail.com" }; 104 | } 105 | } 106 | 107 | public static GitHubClient GetGitHubClient(string accessToken) 108 | { 109 | var client = new GitHubClient(new ProductHeaderValue("cfpa-bot"), new InMemoryCredentialStore(new Credentials(accessToken))); 110 | return client; 111 | 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /CFPABot/Azusa/PRCreatorModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using CFPABot.Azusa.Pages; 7 | using CFPABot.Utils; 8 | using GammaLibrary.Extensions; 9 | using Octokit; 10 | 11 | namespace CFPABot.Azusa 12 | { 13 | public class PRCreatorModule 14 | { 15 | GitHubClient _gitHubClient; 16 | readonly PRCreator.FileCache _enCache; 17 | readonly PRCreator.FileCache _cnCache; 18 | readonly string _email; 19 | readonly string _slug; 20 | readonly string _versionString; 21 | readonly string _prTitle; 22 | readonly Action _updateAction; 23 | bool forkCreated = false; 24 | string _token; 25 | string _domain; 26 | 27 | public PRCreatorModule(GitHubClient gitHubClient, PRCreator.FileCache enCache, PRCreator.FileCache cnCache, string email, string slug, 28 | string versionString, string prTitle, Action updateAction, string githubOauthToken, string modDomain, string branchName = null) 29 | { 30 | _gitHubClient = gitHubClient; 31 | _enCache = enCache; 32 | _cnCache = cnCache; 33 | _email = email; 34 | _slug = slug; 35 | _versionString = versionString; 36 | _prTitle = prTitle; 37 | _updateAction = updateAction; 38 | _token = githubOauthToken; 39 | _domain = modDomain; 40 | BranchName = branchName.IsNullOrWhiteSpace() 41 | ? "CFPA-Helper-" + Guid.NewGuid().ToString("N").Substring(0, 8) 42 | : branchName; 43 | } 44 | 45 | public Repository repo { get; private set; } 46 | public User user { get; private set; } 47 | public string BranchName { get; private set; } 48 | 49 | public async Task Run() 50 | { 51 | user = await _gitHubClient.User.Current(); 52 | 53 | try 54 | { 55 | _updateAction("尝试直接获取 fork 库.."); 56 | repo = await _gitHubClient.Repository.Get(user.Login, "Minecraft-Mod-Language-Package"); 57 | goto start; 58 | } 59 | catch (NotFoundException e) 60 | { 61 | _updateAction("获取失败,正在查找所有 fork 库..可能较慢.."); 62 | } 63 | 64 | var forks = await _gitHubClient.Repository.Forks.GetAll(Constants.RepoID); 65 | if (forks.FirstOrDefault(x => x.Owner.Login == user.Login) is {} fork) 66 | { 67 | repo = fork; 68 | } 69 | else 70 | { 71 | _updateAction("当前你没有 Fork, 正在创建一个新的 Fork"); 72 | repo = await _gitHubClient.Repository.Forks.Create(Constants.RepoID, new NewRepositoryFork()); 73 | _updateAction("已经提交创建 Fork 请求,等待可用..."); 74 | await Task.Delay(8000); 75 | } 76 | start: 77 | 78 | var localRepo = new ForkRepoManager(_token); 79 | _updateAction("Cloning Repo..."); 80 | 81 | localRepo.Clone(user.Login, repo.Name, user.Login, _email); 82 | localRepo.Run("remote add upstream https://github.com/CFPAOrg/Minecraft-Mod-Language-Package.git"); 83 | _updateAction("Fetching upstream..."); 84 | 85 | localRepo.Run("fetch upstream main"); 86 | _updateAction("Creating branch..."); 87 | localRepo.Run($"switch -c {BranchName} upstream/main"); 88 | var baseLocation = $"{localRepo.WorkingDirectory}/projects/{_versionString}/assets/{_slug}/{_domain}/lang"; 89 | _updateAction("Placing files..."); 90 | Directory.CreateDirectory(baseLocation); 91 | File.WriteAllText(baseLocation+$"/{_enCache.FileName}", File.ReadAllText(_enCache.FilePath), new UTF8Encoding(false)); 92 | File.WriteAllText(baseLocation+$"/{_cnCache.FileName}", File.ReadAllText(_cnCache.FilePath), new UTF8Encoding(false)); 93 | localRepo.Run($"add -A"); 94 | localRepo.Commit(_prTitle + "\n\n提交自 CFPA-Helper"); 95 | _updateAction("Pushing to origin..."); 96 | localRepo.Run($"push origin {BranchName}"); 97 | 98 | } 99 | 100 | public async Task CreatePR(string body) 101 | { 102 | return await _gitHubClient.PullRequest.Create(Constants.RepoID, 103 | new NewPullRequest(_prTitle, $"{user.Login}:{BranchName}", "main") { Body = body }); 104 | } 105 | 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/CommandList.razor: -------------------------------------------------------------------------------- 1 | @page "/CommandList" 2 |

CommandList

3 | 善用 GitHub Files 中的复制按钮~ 4 |
5 | 6 |

7 | /rename <from> <to> 将 from 重命名(移动)至 to 8 |
如/rename projects/1.16/assets/create/create/lang/zh_ch.json projects/1.16/assets/create/create/lang/zh_cn.json 9 |

10 | 11 |

12 | /mv <from> <to> 将 from 文件夹移动至 to 13 |

14 | 15 |

16 | /update-en <mod> <game-version> 更新指定模组的英文文件,此命令 Bot 会自动提示 17 |

18 | 19 |

20 | /replace <a> <b> 将 PR 更改文件中的 所有中文文件中的 a 替换为 b 21 |

22 | 23 |

24 | /add-comment <part-of-key> <comment> 对中文语言文件添加一行注释,part-of-key 为要添加注释的行的键 25 |

26 | 27 |

28 | /add-co-author <github-user-name> 添加指定的协作者到 PR,可以直接 @@ 对方 29 |

30 | 31 |

32 | /diff 没啥用了,去用网页版的吧 33 |

34 | 35 |

36 | /sort-keys <file-path> 重排指定文件的键序,一般为 MCreator 使用 37 |

38 | 39 |

40 | /revert 撤销上一个 commit 41 |

42 | 43 |

44 | /revert <commit-hash> 撤销指定的 commit 45 |

-------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/Debug.razor: -------------------------------------------------------------------------------- 1 | @page "/Debug" 2 | @using Microsoft.AspNetCore.Http 3 |

Debug

4 | @inject IBlazorStrap _blazorStrap 5 | @inject NavigationManager _navigationManager 6 | @inject IHttpContextAccessor _http 7 | 8 | @code { 9 | protected override void OnInitialized() 10 | { 11 | if (!LoginManager.IsAdmin(_http).Result) 12 | { 13 | _navigationManager.NavigateTo("/"); 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/EditPR.razor: -------------------------------------------------------------------------------- 1 | @page "/EditPR/{PPRID?}" 2 | @using GammaLibrary.Extensions 3 | @using Microsoft.AspNetCore.Http 4 | 5 | @inject IBlazorStrap _blazorStrap 6 | @inject IHttpContextAccessor _http 7 | @inject NavigationManager _navigationManager 8 | 9 | @inject IJSRuntime js 10 | 11 |

EditPR

12 | 13 | PR 编辑 14 | 15 | 16 | @if (!LoginManager.GetLoginStatus(_http)) 17 | { 18 | 警告:登录后才可以编辑,你可以去主页登录 19 | } 20 | 21 | 22 | PRID 23 | 24 | @if (!true) 25 | { 26 | 开始 27 | } 28 | else 29 | { 30 | 加载中! 31 | } 32 | 33 | 34 | @code { 35 | 36 | 37 | [Parameter] 38 | public string PPrid { get; set; } 39 | int step = 0; 40 | int? prid = null; 41 | 42 | protected override void OnInitialized() 43 | { 44 | prid = PPrid.ToIntOrNull(); 45 | if (PPrid != null) 46 | { 47 | 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model BlazorServerBase.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using System.Diagnostics; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace BlazorServerBase.Pages 7 | { 8 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 9 | [IgnoreAntiforgeryToken] 10 | public class ErrorModel : PageModel 11 | { 12 | public string? RequestId { get; set; } 13 | 14 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 15 | 16 | private readonly ILogger _logger; 17 | 18 | public ErrorModel(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | public void OnGet() 24 | { 25 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/GitHubInCNHelper.razor: -------------------------------------------------------------------------------- 1 | @page "/GitHubInCNHelper" 2 | @using BlazorStrap.Shared.Components.Modal 3 | 4 | @inject IBlazorStrap _blazorStrap 5 | GitHub 无法访问 6 | 7 | 8 | 9 |
请输入主群群号
10 | 11 |
12 | 630943368 (提示:可以复制粘贴) 13 | 14 |
15 | 16 |
17 |
18 | OK 19 |
20 |
21 | @if (!flag) 22 | { 23 | 开始人🐔验证 24 | 25 | } 26 |
27 | @if (flag) 28 | { 29 |

如果你遇到了 GitHub 访问很慢的问题:

30 |

- 使用 Watt Toolkit(免费,非广告)

31 |

- 使用 UU 加速器 搜索并 加速学术资源(免费,需要实名,非广告)

32 |

- 使用 UsbEAm(非广告, 上面无效可以试试这个)

33 |

- 使用 Minecraft 中七根木棍(找你认识的朋友)

34 |
35 |

如果你找到了更多方法,欢迎告诉我(Cyl18)加到这个页面~

36 | } 37 | @code { 38 | 39 | string groupNumber = ""; 40 | bool flag = false; 41 | void ShowToast(string text) 42 | { 43 | _blazorStrap.Toaster.Add("警告", text, o => 44 | { 45 | o.Color = BSColor.Warning; 46 | o.HasIcon = true; 47 | o.Toast = Toast.TopRight; 48 | }); 49 | } 50 | 51 | void Confirm(BSModalBase bsModalBase) 52 | { 53 | bsModalBase.HideAsync(); 54 | if (groupNumber.Trim() == "630943368") 55 | { 56 | flag = true; 57 | } 58 | else 59 | { 60 | ShowToast("输入错了..."); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using CFPABot.PRData 3 | @using Microsoft.AspNetCore.Http 4 | 5 | @inject IBlazorStrap _blazorStrap 6 | @inject IHttpContextAccessor _http 7 | @inject NavigationManager _navigationManager 8 | Index 9 | 10 |

CFPA Helper Beta

11 | 12 |

13 | 这里是一些辅助 CFPA/Minecraft-Mod-Language-Package 的一些小工具 14 |

15 |
16 | @if (LoginManager.GetLoginStatus(_http)) 17 | { 18 | @code{ 19 | string userName = "[加载中]"; 20 | string userAvatar = ""; 21 | void GetName() 22 | { 23 | userName = (LoginManager.GetGitHubClient(_http).User.Current().Result).Login; 24 | } 25 | void GetAvatar() 26 | { 27 | userAvatar = (LoginManager.GetGitHubClient(_http).User.Current().Result).AvatarUrl; 28 | } 29 | 30 | protected override async Task OnInitializedAsync() 31 | { 32 | if (LoginManager.GetLoginStatus(_http)) 33 | Task.Run(() => 34 | { 35 | GetName(); 36 | GetAvatar(); 37 | InvokeAsync(StateHasChanged); 38 | }); 39 | 40 | 41 | } 42 | 43 | } 44 | 45 | 登录账号: @(userName) 登出 46 | } 47 | else 48 | { 49 | 你还没有登录. 50 | GitHub 登录 51 | 52 | } -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/MappingsManager.razor: -------------------------------------------------------------------------------- 1 | @page "/MappingsManager" 2 |

MappingsManager

3 | 4 | @code { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/PREditor.razor: -------------------------------------------------------------------------------- 1 | @page "/PREditor" 2 | @inject IJSRuntime js 3 | @inject IBlazorStrap _blazorStrap 4 | @inject IHttpContextAccessor _http 5 | @inject NavigationManager _navigationManager 6 | @using CFPABot.PRData 7 | @using CFPABot.Utils 8 | @using GammaLibrary.Extensions 9 | @using Microsoft.AspNetCore.Http 10 | @using Octokit 11 | @using Serilog 12 | @implements IDisposable 13 |

PREditor

14 | 15 | 如果你遇到了什么问题,可以在 CFPA 群中 @@Cyl18 反馈,或者前往 这里 提交 Issue。 16 |
17 | 18 | 19 | @if (LoginManager.GetLoginStatus(_http)) 20 | { 21 | @code{ 22 | string userName = "[加载中]"; 23 | string userAvatar = ""; 24 | void GetName() 25 | { 26 | userName = (LoginManager.GetGitHubClient(_http).User.Current().Result).Login; 27 | } 28 | void GetAvatar() 29 | { 30 | userAvatar = (LoginManager.GetGitHubClient(_http).User.Current().Result).AvatarUrl; 31 | } 32 | 33 | GitHubClient gitHubClient; 34 | protected override async Task OnInitializedAsync() 35 | { 36 | if (LoginManager.GetLoginStatus(_http)) 37 | Task.Run(() => 38 | { 39 | GetName(); 40 | GetAvatar(); 41 | InvokeAsync(StateHasChanged); 42 | }); 43 | Task.Run(async () => 44 | { 45 | gitHubClient = LoginManager.GetGitHubClient(_http); 46 | var user = await gitHubClient.User.Current(); 47 | var prs = await gitHubClient.PullRequest.GetAllForRepository(Constants.RepoID); 48 | if (prs.Count == 0) 49 | { 50 | InvokeAsync(() => ShowToast("你没有提交任何 PR")); 51 | return; 52 | } 53 | var prsFiltered = prs.Where(x => x.User.Login == user.Login).ToArray(); 54 | ns = prsFiltered.Select(x => x.Number).ToArray(); 55 | prTitles.Clear(); 56 | foreach (var pullRequest in prsFiltered) 57 | { 58 | prTitles.Add(pullRequest.Number, $"#{pullRequest.Number} | {pullRequest.Title}"); 59 | } 60 | await js.InvokeAsync("load_editor"); 61 | ready = true; 62 | InvokeAsync(StateHasChanged); 63 | }); 64 | 65 | } 66 | 67 | } 68 | 69 | 登录账号: @(userName) 登出 70 | 71 | @if (ready) 72 | { 73 | @code{ 74 | 75 | bool prDisabled = false; 76 | bool prWarning = false; 77 | } 78 | 79 | 80 | PR 号 81 | 82 | 83 | 84 | 85 | 你自己提交的 PR 86 | 87 | 88 | 89 | @code 90 | { 91 | int[] ns = new[] { -1 }; 92 | 93 | Dictionary prTitles = new Dictionary() 94 | { 95 | { -1, "#-1 | 未加载" } 96 | }; 97 | 98 | void ChangePrNumber(int x) => prNumber = x; 99 | } 100 | 101 | @foreach (var x in ns) 102 | { 103 | @(prTitles[x]) 104 | } 105 | 106 | 107 | 开始编辑此 PR 108 | 109 | 110 | 111 | @if (prWarning) 112 | { 113 | 检测到你可能没有编辑此 PR 的权限,请前往 GitHub 页面提交修改建议。 114 | } 115 | 116 | @code{ 117 | PullRequest pr; 118 | IReadOnlyList prFiles; 119 | ForkRepoManager repo; 120 | string originalFile; 121 | string currentFile; 122 | 123 | async Task GetPr() 124 | { 125 | prDisabled = true; 126 | try 127 | { 128 | pr = await gitHubClient.PullRequest.Get(Constants.RepoID, prNumber); 129 | prFiles = await gitHubClient.PullRequest.Files(Constants.RepoID, prNumber); 130 | prWarning = await LoginManager.HasPrPermission(_http, prNumber); 131 | } 132 | catch (Exception e) 133 | { 134 | Log.Error(e, "PREditor"); 135 | prDisabled = false; 136 | } 137 | } 138 | 139 | 140 | 141 | 142 | 143 | } 144 | 145 | 146 | 147 | 148 | @logger 149 | 150 | 151 | 152 | 153 | } 154 | 155 | 156 | 157 | } 158 | else 159 | { 160 | 你还没有登录. 161 | GitHub 登录 162 | 163 | } 164 | 165 |
166 | 167 | @code 168 | { 169 | void ShowToast(string text) 170 | { 171 | _blazorStrap.Toaster.Add("警告", text, o => 172 | { 173 | o.Color = BSColor.Warning; 174 | o.HasIcon = true; 175 | o.Toast = Toast.TopRight; 176 | }); 177 | } 178 | } 179 | 180 | 181 | 182 | 183 | @code { 184 | string logger = ""; 185 | bool ready = true; 186 | 187 | public string[] texts = new string[0]; 188 | public int[] indexs = new int[0]; 189 | int prNumber = 0; 190 | 191 | public void Dispose() 192 | { 193 | 194 | 195 | } 196 | 197 | } -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/PRList.razor: -------------------------------------------------------------------------------- 1 | @page "/PRList" 2 | @using CFPABot.PRData 3 | @using System.Text.RegularExpressions 4 | @using System.Web 5 | @using CFPABot.DiffEngine 6 | @using GammaLibrary.Extensions 7 | 8 |

PR 列表

9 | 此列表在刷新时会更新 10 | 11 | 12 | 13 | Mod Slug 14 | PR 15 | 16 | 17 | 18 | 19 | @foreach (var x in PRDataManager.Relation.OrderBy(y => y.Key)) 20 | { 21 | 22 | @x.Key 23 | 24 | @foreach (var y in x.Value.GroupBy(z => z.prid)) 25 | { 26 | $"https://github.com/CFPAOrg/Minecraft-Mod-Language-Package/pull/{a}")>#@y.Key@:   27 | @(y.Select(a => ModPath.GetVersionDirectory(a.modVersion.MinecraftVersion, a.modVersion.ModLoader)).Distinct().Connect()) 28 |
29 | } 30 |
31 |
32 | } 33 | 34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/SortLangFile.razor: -------------------------------------------------------------------------------- 1 | @page "/SortLangFile" 2 |

重排文件键序

3 | 4 | 5 | @code { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/SpecialDiffSegment.razor: -------------------------------------------------------------------------------- 1 | @using System.Diagnostics 2 | @using System.Text.Json 3 | @using CFPABot.Utils 4 | @using GammaLibrary.Extensions 5 | @using Ganss.Xss 6 | @using Markdig 7 | @using Microsoft.AspNetCore.Http 8 | @using Octokit 9 | @inject IJSRuntime js 10 | @inject IBlazorStrap _blazorStrap 11 | @inject IHttpContextAccessor _http 12 | 13 | 文件名:@FileName 14 | 对所选内容进行注释 15 | 16 | @foreach (var s in Comments) 17 | { 18 |

19 | @if (s.OriginalCommitId != s.CommitId) 20 | { 21 | 已过时: 22 | } 23 | @(s.User.Login) 的建议:
24 | @((MarkupString)(new HtmlSanitizer().Sanitize(Markdown.ToHtml(s.Body, 25 | new MarkdownPipelineBuilder().UsePipeTables().UseBootstrap().UseGridTables().Build())).Replace("\n", "
"))) 26 |

27 | 28 | } 29 | 30 |
31 | 32 | 33 | 34 | 提交 35 | 36 |
37 | Review 内容 38 | 39 |
40 |
41 | 42 | Cancel 43 | 提交 44 | 45 |
46 |
47 | 48 | @code { 49 | 50 | 51 | object model = new object(); 52 | [Parameter] 53 | public string FilePath { get; set; } 54 | [Parameter] 55 | public PullRequest PR { get; set; } 56 | 57 | [Parameter] 58 | public string FileName { get; set; } 59 | [Parameter] 60 | public string EnFile { get; set; } 61 | [Parameter] 62 | public string CnFile { get; set; } 63 | [Parameter] 64 | public List Comments { get; set; } 65 | [Parameter] 66 | public GitHubClient GitHubClient { get; set; } 67 | 68 | 69 | string suggestion; 70 | 71 | public string Id = Guid.NewGuid().ToString("N"); 72 | 73 | 74 | protected override async Task OnAfterRenderAsync(bool firstRender) 75 | { 76 | if (firstRender) 77 | { 78 | await js.InvokeAsync("load_diff_editor", Id, true); 79 | await js.InvokeAsync("set_diff_content", Id, EnFile, CnFile, FileName.EndsWith(".json") ? "json" : "ini"); 80 | 81 | // var content = CustomCommandContentConfig.Instance.Content; 82 | // await js.InvokeAsync("set_editor_content", content.IsNullOrWhiteSpace() ? GetDefaultContent() : content); 83 | } 84 | } 85 | 86 | int realStartLine; 87 | async void Test() 88 | { 89 | 90 | try 91 | { 92 | var a = await js.InvokeAsync("get_editor_selection", Id); 93 | var json = JsonDocument.Parse(a).RootElement; 94 | var startLine = json.GetProperty("startLineNumber").GetInt32() -1; 95 | var endLine = json.GetProperty("endLineNumber").GetInt32() -1; 96 | realStartLine = startLine + 1; 97 | 98 | suggestion = "```suggestion\n" + CnFile.Split('\n').Skip(startLine).Take(/*endLine - startLine + 1*/1).Connect("\n") + "\n```"; 99 | } 100 | catch (Exception e) 101 | { 102 | 103 | } 104 | 105 | } 106 | 107 | async void Update() 108 | { 109 | await GitHubClient.PullRequest.ReviewComment.Create(Constants.RepoID, PR.Number, new PullRequestReviewCommentCreate(suggestion, PR.Head.Sha, FilePath, realStartLine)); 110 | _blazorStrap.Toaster.Add("Okay!", op => op.CloseAfter = 3); 111 | 112 | 113 | } 114 | 115 | public ElementReference Ref { get; set; } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/WeeklyReport.razor: -------------------------------------------------------------------------------- 1 | @page "/WeeklyReport" 2 | @using System.IO 3 |

WeeklyReport

4 | 5 | @code { 6 | 7 | 8 | 9 | 10 | 11 | List GetDates() 12 | { 13 | var files = Directory.GetFiles("/app/config/weekly-reports/"); 14 | var list = files.Select(Path.GetFileNameWithoutExtension).ToList(); 15 | var last = DateOnly.FromDateTime(DateTime.Now); 16 | while (last.DayOfWeek != DayOfWeek.Sunday) 17 | { 18 | last = last.AddDays(-1); 19 | } 20 | var next = last.AddDays(7); 21 | var s1 = last.ToString("O"); 22 | var s2 = next.ToString("O"); 23 | if (!list.Contains(s1)) list.Add(s1); 24 | if (!list.Contains(s2)) list.Add(s2); 25 | return list; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/_Host.cshtml: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @namespace CFPABot.Azusa.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | @{ 5 | // ReSharper disable once Razor.LayoutNotResolved 6 | Layout = "_Layout"; 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Pages/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | @namespace CFPABot.Azusa.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | @RenderBody() 23 | 24 |
25 | 26 | An error has occurred. This application may no longer respond until reloaded. 27 | 28 | 29 | An unhandled exception has occurred. See browser dev tools for details. 30 | 31 | Reload 32 | 🗙 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Http 2 | @inherits LayoutComponentBase 3 | @inject IHttpContextAccessor http; 4 | CFPA Azusa Tools 5 | 6 |
7 | 10 | 11 |
12 |
13 | @code 14 | { 15 | bool admin = false; 16 | protected override async Task OnInitializedAsync() 17 | { 18 | admin = await LoginManager.IsAdmin(http); 19 | InvokeAsync(StateHasChanged); 20 | } 21 | } 22 | @if (admin) 23 | { 24 | 你是管理员哦 25 | 26 | } 27 | About 28 |
29 | 30 |
31 | @Body 32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Shared/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | /* background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | 14 | */ 15 | background-image: linear-gradient(180deg, rgba(41,23,147,1) 30%, rgba(102,204,255,1) 100%); 16 | 17 | } 18 | 19 | .top-row { 20 | background-color: #444444; 21 | border-bottom: 1px solid #444444; 22 | justify-content: flex-end; 23 | height: 3.5rem; 24 | display: flex; 25 | align-items: center; 26 | } 27 | 28 | .top-row ::deep a, .top-row .btn-link { 29 | white-space: nowrap; 30 | margin-left: 1.5rem; 31 | } 32 | 33 | .top-row a:first-child { 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | } 37 | 38 | @media (max-width: 640.98px) { 39 | .top-row:not(.auth) { 40 | display: none; 41 | } 42 | 43 | .top-row.auth { 44 | justify-content: space-between; 45 | } 46 | 47 | .top-row a, .top-row .btn-link { 48 | margin-left: 0; 49 | } 50 | } 51 | 52 | @media (min-width: 641px) { 53 | .page { 54 | flex-direction: row; 55 | } 56 | 57 | .sidebar { 58 | width: 250px; 59 | height: 100vh; 60 | position: sticky; 61 | top: 0; 62 | } 63 | 64 | .top-row { 65 | position: sticky; 66 | top: 0; 67 | z-index: 1; 68 | } 69 | 70 | .top-row, article { 71 | padding-left: 2rem !important; 72 | padding-right: 1.5rem !important; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  9 | 10 |
11 | 53 |
54 | 55 | @code { 56 | private bool collapseNavMenu = true; 57 | [Parameter] 58 | public bool admin { get; set; } 59 | 60 | private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null; 61 | 62 | private void ToggleNavMenu() 63 | { 64 | collapseNavMenu = !collapseNavMenu; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CFPABot/Azusa/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | } 4 | 5 | .top-row { 6 | height: 3.5rem; 7 | background-color: rgba(0,0,0,0.4); 8 | } 9 | 10 | .navbar-brand { 11 | font-size: 1.1rem; 12 | } 13 | 14 | .oi { 15 | width: 2rem; 16 | font-size: 1.1rem; 17 | vertical-align: text-top; 18 | top: -2px; 19 | } 20 | 21 | .nav-item { 22 | font-size: 0.9rem; 23 | padding-bottom: 0.5rem; 24 | } 25 | 26 | .nav-item:first-of-type { 27 | padding-top: 1rem; 28 | } 29 | 30 | .nav-item:last-of-type { 31 | padding-bottom: 1rem; 32 | } 33 | 34 | .nav-item ::deep a { 35 | color: #d7d7d7; 36 | border-radius: 4px; 37 | height: 3rem; 38 | display: flex; 39 | align-items: center; 40 | line-height: 3rem; 41 | } 42 | 43 | .nav-item ::deep a.active { 44 | background-color: rgba(255,255,255,0.25); 45 | color: white; 46 | } 47 | 48 | .nav-item ::deep a:hover { 49 | background-color: rgba(255,255,255,0.1); 50 | color: white; 51 | } 52 | 53 | @media (min-width: 641px) { 54 | .navbar-toggler { 55 | display: none; 56 | } 57 | 58 | .collapse { 59 | /* Never collapse the sidebar for wide screens */ 60 | display: block; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /CFPABot/Azusa/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using CFPABot 10 | @using CFPABot.Azusa 11 | @using CFPABot.Azusa.Shared 12 | @using BlazorStrap.V5 13 | @using BlazorStrap 14 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/BlazorStrap.V5.bundle.scp.css: -------------------------------------------------------------------------------- 1 | /* _content/BlazorStrap.V5/Components/Common/BSCollapse.razor.rz.scp.css */ 2 | .collapsing.collapse-horizontal[b-cu907r7tvn] { 3 | width: 0; 4 | height: auto; 5 | transition: width .35s ease; 6 | } 7 | /* _content/BlazorStrap.V5/Components/Common/BSPopover.razor.rz.scp.css */ 8 | .popover:not(.show)[b-jitnwoparl] { 9 | visibility: hidden; 10 | } 11 | /* _content/BlazorStrap.V5/Components/Common/BSToast.razor.rz.scp.css */ 12 | .toast-icon[b-rmszuegdcu]{ 13 | color: rgba(255,255,255, .7); 14 | } 15 | /* _content/BlazorStrap.V5/Components/Common/BSTooltip.razor.rz.scp.css */ 16 | .tooltip:not(.show)[b-t15xljjw7v] { 17 | visibility: hidden; 18 | } 19 | /* _content/BlazorStrap.V5/Components/Datatable/BSDataTableHead.razor.rz.scp.css */ 20 | /*Credit https://codepen.io/imarkrige/pen/kyOjoL */ 21 | th a[b-cue1wbex1l], 22 | td a[b-cue1wbex1l] { 23 | display: block; 24 | width: 100%; 25 | } 26 | 27 | th a.sort-by[b-cue1wbex1l] { 28 | padding-right: 18px; 29 | position: relative; 30 | } 31 | 32 | a.sort-by[b-cue1wbex1l]:before, 33 | a.sort-by[b-cue1wbex1l]:after { 34 | border: 4px solid transparent; 35 | content: ""; 36 | display: block; 37 | height: 0; 38 | right: 5px; 39 | top: 50%; 40 | position: absolute; 41 | width: 0; 42 | } 43 | 44 | a.sort-by[b-cue1wbex1l]:before { 45 | border-bottom-color: #666; 46 | margin-top: -9px; 47 | } 48 | 49 | a.sort-by[b-cue1wbex1l]:after { 50 | border-top-color: #666; 51 | margin-top: 1px; 52 | } 53 | 54 | th a.sort[b-cue1wbex1l] { 55 | padding-right: 18px; 56 | position: relative; 57 | } 58 | 59 | a.sort[b-cue1wbex1l]:before, 60 | a.sort[b-cue1wbex1l]:after { 61 | border: 4px solid transparent; 62 | content: ""; 63 | display: block; 64 | height: 0; 65 | right: 5px; 66 | top: 50%; 67 | position: absolute; 68 | width: 0; 69 | } 70 | 71 | a.sort[b-cue1wbex1l]:before { 72 | border-bottom-color: #666; 73 | margin-top: -9px; 74 | } 75 | 76 | th a.sort-desc[b-cue1wbex1l] { 77 | padding-right: 18px; 78 | position: relative; 79 | } 80 | 81 | a.sort-desc[b-cue1wbex1l]:before, 82 | a.sort-desc[b-cue1wbex1l]:after { 83 | border: 4px solid transparent; 84 | content: ""; 85 | display: block; 86 | height: 0; 87 | right: 5px; 88 | top: 50%; 89 | position: absolute; 90 | width: 0; 91 | } 92 | 93 | a.sort-desc[b-cue1wbex1l]:after { 94 | border-top-color: #666; 95 | margin-top: 1px; 96 | } 97 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/BlazorStrap.bundle.scp.css: -------------------------------------------------------------------------------- 1 | /* _content/BlazorStrap/Shared/InternalComponents/Error.razor.rz.scp.css */ 2 | .bserror[b-pct091ldyq] { 3 | padding: .25rem 1rem; 4 | margin-top: 1.25rem; 5 | margin-bottom: 1.25rem; 6 | border: 1px solid #d8d8d8; 7 | border-left-width: .2rem; 8 | border-right-width: .2rem; 9 | border-radius: .25rem; 10 | border-left-color: var(--bs-red); 11 | border-right-color: var(--bs-red); 12 | } 13 | .bserror-header[b-pct091ldyq] { 14 | font-size: 1.5em; 15 | } 16 | .bserror-header svg[b-pct091ldyq]{ 17 | vertical-align: middle; 18 | } 19 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/_content/BlazorStrap.V5/BlazorStrap.V5.bundle.scp.css: -------------------------------------------------------------------------------- 1 | /* _content/BlazorStrap.V5/Components/Common/BSCollapse.razor.rz.scp.css */ 2 | .collapsing.collapse-horizontal[b-cu907r7tvn] { 3 | width: 0; 4 | height: auto; 5 | transition: width .35s ease; 6 | } 7 | /* _content/BlazorStrap.V5/Components/Common/BSPopover.razor.rz.scp.css */ 8 | .popover:not(.show)[b-jitnwoparl] { 9 | visibility: hidden; 10 | } 11 | /* _content/BlazorStrap.V5/Components/Common/BSToast.razor.rz.scp.css */ 12 | .toast-icon[b-rmszuegdcu]{ 13 | color: rgba(255,255,255, .7); 14 | } 15 | /* _content/BlazorStrap.V5/Components/Common/BSTooltip.razor.rz.scp.css */ 16 | .tooltip:not(.show)[b-t15xljjw7v] { 17 | visibility: hidden; 18 | } 19 | /* _content/BlazorStrap.V5/Components/Datatable/BSDataTableHead.razor.rz.scp.css */ 20 | /*Credit https://codepen.io/imarkrige/pen/kyOjoL */ 21 | th a[b-cue1wbex1l], 22 | td a[b-cue1wbex1l] { 23 | display: block; 24 | width: 100%; 25 | } 26 | 27 | th a.sort-by[b-cue1wbex1l] { 28 | padding-right: 18px; 29 | position: relative; 30 | } 31 | 32 | a.sort-by[b-cue1wbex1l]:before, 33 | a.sort-by[b-cue1wbex1l]:after { 34 | border: 4px solid transparent; 35 | content: ""; 36 | display: block; 37 | height: 0; 38 | right: 5px; 39 | top: 50%; 40 | position: absolute; 41 | width: 0; 42 | } 43 | 44 | a.sort-by[b-cue1wbex1l]:before { 45 | border-bottom-color: #666; 46 | margin-top: -9px; 47 | } 48 | 49 | a.sort-by[b-cue1wbex1l]:after { 50 | border-top-color: #666; 51 | margin-top: 1px; 52 | } 53 | 54 | th a.sort[b-cue1wbex1l] { 55 | padding-right: 18px; 56 | position: relative; 57 | } 58 | 59 | a.sort[b-cue1wbex1l]:before, 60 | a.sort[b-cue1wbex1l]:after { 61 | border: 4px solid transparent; 62 | content: ""; 63 | display: block; 64 | height: 0; 65 | right: 5px; 66 | top: 50%; 67 | position: absolute; 68 | width: 0; 69 | } 70 | 71 | a.sort[b-cue1wbex1l]:before { 72 | border-bottom-color: #666; 73 | margin-top: -9px; 74 | } 75 | 76 | th a.sort-desc[b-cue1wbex1l] { 77 | padding-right: 18px; 78 | position: relative; 79 | } 80 | 81 | a.sort-desc[b-cue1wbex1l]:before, 82 | a.sort-desc[b-cue1wbex1l]:after { 83 | border: 4px solid transparent; 84 | content: ""; 85 | display: block; 86 | height: 0; 87 | right: 5px; 88 | top: 50%; 89 | position: absolute; 90 | width: 0; 91 | } 92 | 93 | a.sort-desc[b-cue1wbex1l]:after { 94 | border-top-color: #666; 95 | margin-top: 1px; 96 | } 97 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/_content/BlazorStrap/BlazorStrap.bundle.scp.css: -------------------------------------------------------------------------------- 1 | /* _content/BlazorStrap/Shared/InternalComponents/Error.razor.rz.scp.css */ 2 | .bserror[b-pct091ldyq] { 3 | padding: .25rem 1rem; 4 | margin-top: 1.25rem; 5 | margin-bottom: 1.25rem; 6 | border: 1px solid #d8d8d8; 7 | border-left-width: .2rem; 8 | border-right-width: .2rem; 9 | border-radius: .25rem; 10 | border-left-color: var(--bs-red); 11 | border-right-color: var(--bs-red); 12 | } 13 | .bserror-header[b-pct091ldyq] { 14 | font-size: 1.5em; 15 | } 16 | .bserror-header svg[b-pct091ldyq]{ 17 | vertical-align: middle; 18 | } 19 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](https://github.com/iconic/open-iconic) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](https://github.com/iconic/open-iconic). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](https://github.com/iconic/open-iconic) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](https://github.com/iconic/open-iconic) and [Reference](https://github.com/iconic/open-iconic) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyl18/CFPABot/8f3dcdfffa4eaba90378d805efec9f84072d7de7/CFPABot/Azusa/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyl18/CFPABot/8f3dcdfffa4eaba90378d805efec9f84072d7de7/CFPABot/Azusa/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyl18/CFPABot/8f3dcdfffa4eaba90378d805efec9f84072d7de7/CFPABot/Azusa/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyl18/CFPABot/8f3dcdfffa4eaba90378d805efec9f84072d7de7/CFPABot/Azusa/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .fade-in { 8 | opacity: 1; 9 | animation-name: fadeInOpacity; 10 | animation-iteration-count: 1; 11 | animation-timing-function: ease-in; 12 | animation-duration: 0.4s; 13 | } 14 | 15 | @keyframes fadeInOpacity { 16 | 0% { 17 | opacity: 0; 18 | } 19 | 20 | 100% { 21 | opacity: 1; 22 | } 23 | } 24 | 25 | h1:focus { 26 | outline: none; 27 | } 28 | 29 | a, .btn-link { 30 | color: #0071c1; 31 | } 32 | 33 | .btn-primary { 34 | color: #fff; 35 | background-color: #1b6ec2; 36 | border-color: #1861ac; 37 | } 38 | 39 | .content { 40 | padding-top: 1.1rem; 41 | } 42 | 43 | .valid.modified:not([type=checkbox]) { 44 | outline: 1px solid #26b050; 45 | } 46 | 47 | .invalid { 48 | outline: 1px solid red; 49 | } 50 | 51 | .validation-message { 52 | color: red; 53 | } 54 | 55 | #blazor-error-ui { 56 | background: lightyellow; 57 | bottom: 0; 58 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 59 | display: none; 60 | left: 0; 61 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 62 | position: fixed; 63 | width: 100%; 64 | z-index: 1000; 65 | } 66 | 67 | #blazor-error-ui .dismiss { 68 | cursor: pointer; 69 | position: absolute; 70 | right: 0.75rem; 71 | top: 0.5rem; 72 | } 73 | 74 | .blazor-error-boundary { 75 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 76 | padding: 1rem 1rem 1rem 3.7rem; 77 | color: white; 78 | } 79 | 80 | .blazor-error-boundary::after { 81 | content: "An error has occurred." 82 | } 83 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyl18/CFPABot/8f3dcdfffa4eaba90378d805efec9f84072d7de7/CFPABot/Azusa/wwwroot/favicon.ico -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot/img/tip1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyl18/CFPABot/8f3dcdfffa4eaba90378d805efec9f84072d7de7/CFPABot/Azusa/wwwroot/img/tip1.png -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot2/FileName.cs: -------------------------------------------------------------------------------- 1 | namespace CFPABot.Azusa.wwwroot2 2 | { 3 | public class FileName 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot2/_content/cfpabot.js: -------------------------------------------------------------------------------- 1 | ScrollElementIntoView = element => element.scrollIntoView(); -------------------------------------------------------------------------------- /CFPABot/Azusa/wwwroot2/_content/editor.js: -------------------------------------------------------------------------------- 1 | // https://blog.expo.io/building-a-code-editor-with-monaco-f84b3a06deaf 2 | 3 | 4 | 5 | // ReSharper disable StringLiteralWrongQuotes 6 | 7 | let editor = {} ; 8 | let model = {} ; 9 | 10 | function set_editor_content(id, s, ct) { 11 | require.config({ 12 | paths: { 13 | 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.41.0/min/vs' 14 | } 15 | }); 16 | require(['vs/editor/editor.main'], function () { 17 | 18 | model[id] = monaco.editor.createModel(s, ct); 19 | editor[id].setModel(model[id]); 20 | 21 | }); 22 | } 23 | 24 | function get_editor_selection(id) { 25 | return JSON.stringify(editor[id].getSelection()); 26 | } 27 | 28 | function get_editor_content(id) { 29 | return model[id].getValue(); 30 | } 31 | 32 | function set_location(id, line){ 33 | editor[id].setPosition({column: 1, lineNumber: line}); 34 | } 35 | 36 | function load_editor(id, readonly) { 37 | require.config({ 38 | paths: { 39 | 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.41.0/min/vs' 40 | } 41 | }); 42 | 43 | 44 | let currentFile = '/index.php'; 45 | 46 | 47 | 48 | // localStorage.removeItem('files'); 49 | 50 | 51 | require(['vs/editor/editor.main'], function () { 52 | 53 | monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true); 54 | monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ 55 | allowNonTsExtensions: true 56 | }); 57 | 58 | 59 | editor[id] = monaco.editor.create(document.getElementById('container-'+id), { 60 | automaticLayout: true, 61 | scrollBeyondLastLine: false, 62 | model: null, 63 | readOnly: readonly, 64 | theme: "vs-dark", 65 | // roundedSelection: false, 66 | }); 67 | 68 | 69 | }); 70 | } 71 | 72 | function load_diff_editor(id, readonly) { 73 | require.config({ 74 | paths: { 75 | 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.41.0/min/vs' 76 | } 77 | }); 78 | 79 | 80 | let currentFile = '/index.php'; 81 | 82 | 83 | 84 | // localStorage.removeItem('files'); 85 | 86 | 87 | require(['vs/editor/editor.main'], function () { 88 | 89 | monaco.languages.typescript.javascriptDefaults.setEagerModelSync(true); 90 | monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ 91 | allowNonTsExtensions: true 92 | }); 93 | 94 | 95 | editor[id] = monaco.editor.createDiffEditor(document.getElementById('container-' + id), { 96 | automaticLayout: true, 97 | scrollBeyondLastLine: false, 98 | model: null, 99 | readOnly: readonly, 100 | theme: "vs-dark", 101 | diffWordWrap: "on", 102 | wordWrap: "on" 103 | // roundedSelection: false, 104 | }); 105 | 106 | }); 107 | } 108 | 109 | 110 | function set_diff_content(id, s1, s2, ct) { 111 | require.config({ 112 | paths: { 113 | 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.41.0/min/vs' 114 | } 115 | }); 116 | require(['vs/editor/editor.main'], function () { 117 | 118 | model[id] = { 119 | original: monaco.editor.createModel(s1, ct), 120 | modified: monaco.editor.createModel(s2, ct) 121 | } 122 | editor[id].setModel(model[id]); 123 | editor[id].updateOptions( 124 | { 125 | diffWordWrap: "on", 126 | wordWrap: "on" 127 | } 128 | ); 129 | }); 130 | } 131 | -------------------------------------------------------------------------------- /CFPABot/CFPABot - Backup.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | Linux 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | True 42 | True 43 | Locale.resx 44 | 45 | 46 | 47 | 48 | 49 | PublicResXFileCodeGenerator 50 | Locale.Designer.cs 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /CFPABot/CFPABot.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net7.0 5 | Linux 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Always 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | True 51 | True 52 | Locale.resx 53 | 54 | 55 | 56 | 57 | 58 | PublicResXFileCodeGenerator 59 | Locale.Designer.cs 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /CFPABot/CFPALLM/CFPALLMManager.cs: -------------------------------------------------------------------------------- 1 | using CFPABot.Exceptions; 2 | using CFPABot.Utils; 3 | using GammaLibrary.Extensions; 4 | using OpenAI; 5 | using OpenAI.Chat; 6 | using System; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Text; 10 | using System.Text.Encodings.Web; 11 | using System.Text.Json; 12 | using System.Text.RegularExpressions; 13 | using System.Threading.Tasks; 14 | using Serilog; 15 | 16 | namespace CFPABot.CFPALLM; 17 | public record PRReviewAssistantData(string Key, long InReplyToId, string ReplyContent); 18 | 19 | public static class CFPALLMManager 20 | { 21 | public static async Task<(PRReviewAssistantData[] data, string rawOutput, string indentedjson)> RunPRReview(int prid, string path, string prompt, bool diffMode, IProgress progress) 22 | { 23 | var delta = ""; 24 | var openAiClient = new OpenAIClient(clientSettings: new OpenAIClientSettings("https://ark.cn-beijing.volces.com/api", apiVersion: "v3"), 25 | openAIAuthentication: Environment.GetEnvironmentVariable("HUOSHAN_API_KEY"), client: new HttpClient() { Timeout = TimeSpan.FromMinutes(1000) }); 26 | var response = await openAiClient.ChatEndpoint.StreamCompletionAsync(new ChatRequest(new[] 27 | { 28 | new Message(Role.User, prompt + await ProcessPrReviewInput(prid, path, diffMode)) 29 | }, "deepseek-r1-250120", responseFormat: ChatResponseFormat.Json), chatResponse => 30 | { 31 | var value = chatResponse.FirstChoice?.Delta?.ToString(); 32 | 33 | if (value != null) 34 | { 35 | delta += value; 36 | progress.Report(delta); 37 | } 38 | }); 39 | var s = response.FirstChoice.Message.ToString(); 40 | Log.Information($"{prid} 的 LLM 审核结果:"); 41 | Log.Information(s); 42 | var last = s.Split("").Last(); 43 | var regex = new Regex(@"\[(.|\n)*\]", RegexOptions.Multiline); 44 | var rawJson = regex.Match(last).Value; 45 | var prReviewAssistantDatas = rawJson.JsonDeserialize(); 46 | ; 47 | return (prReviewAssistantDatas, s, prReviewAssistantDatas.ToJsonString(new JsonSerializerOptions() 48 | { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, })); 49 | } 50 | public static async Task ProcessPrReviewInput(int prid, string path, bool diffMode) 51 | { 52 | var sb = new StringBuilder(); 53 | var pr = await GitHub.GetPullRequest(prid); 54 | var enFile = await GetLangFileFromGitHub(pr.Head.Sha, path.Replace("zh_cn", "en_us")); 55 | string enContent; 56 | if (path.Contains(".json")) 57 | { 58 | if (!LangDataNormalizer.ProcessJsonSingle(enFile, out var ens)) throw new CheckException("Json 语法错误"); 59 | enContent = ens.Select(x => $"{x.Key}={x.Value.Replace("\n", "\\n")}").Connect("\n"); 60 | } 61 | else 62 | { 63 | enContent = LangDataNormalizer.ProcessLangSingle(enFile).Select(x => $"{x.Key}={x.Value.Replace("\n", "\\n")}").Connect("\n"); 64 | } 65 | 66 | if (!diffMode) 67 | { 68 | var cnFile = await GetLangFileFromGitHub(pr.Head.Sha, path); 69 | if (cnFile == null || enFile == null) 70 | { 71 | throw new CheckException("找不到对应的文件"); 72 | } 73 | 74 | sb.AppendLine("[英文文件]\n" + enContent); 75 | sb.AppendLine("[中文文件]\nSTART_OF_FILE\n" + cnFile + "END_OF_FILE"); 76 | } 77 | else 78 | { 79 | var files = await GitHub.Instance.PullRequest.Files(Constants.RepoID, prid); 80 | var cnDiff = files.First(x => x.FileName == path).Patch; 81 | sb.AppendLine("[英文文件]\n" + enContent); 82 | sb.AppendLine("[中文文件Diff]\nSTART_OF_FILE\n" + cnDiff + "END_OF_FILE"); 83 | 84 | 85 | } 86 | var prreviewData = await GitHub.GetPRReviewData(prid); 87 | 88 | foreach (var oprr in await GitHub.Instance.PullRequest.ReviewComment.GetAll(Constants.RepoID, prid)) 89 | { 90 | bool resolved = false; 91 | foreach (var reviewThreadsEdge in prreviewData.Repository.PullRequest.ReviewThreads.Edges) 92 | { 93 | if (reviewThreadsEdge.Node.Comments.Edges.Any(x => x.Node.FullDatabaseId == oprr.Id)) 94 | { 95 | if (reviewThreadsEdge.Node.IsResolved) 96 | { 97 | resolved = true; 98 | } 99 | } 100 | } 101 | 102 | sb.AppendLine("[Comment]"); 103 | if (resolved) 104 | { 105 | sb.AppendLine("此回复已解决"); 106 | } 107 | sb.AppendLine($"Id:{oprr.Id}"); 108 | if (oprr.InReplyToId != null) 109 | { 110 | sb.AppendLine($"InReplyTo:{oprr.InReplyToId}"); 111 | } 112 | 113 | sb.AppendLine("Content:"); 114 | sb.AppendLine(oprr.Body); 115 | sb.AppendLine("EndOfContent"); 116 | } 117 | 118 | return sb.ToString(); 119 | } 120 | // public static async Task ProcessPrReviewInput(int prid, string path) 121 | // { 122 | // var sb = new StringBuilder(); 123 | // var pr = await GitHub.GetPullRequest(prid); 124 | // 125 | // var cnFile = await GetLangFileFromGitHub(pr.Head.Sha, path); 126 | // var enFile = await GetLangFileFromGitHub(pr.Head.Sha, path.Replace("zh_cn", "en_us")); 127 | // if (cnFile == null || enFile == null) 128 | // { 129 | // throw new CheckException("找不到对应的文件"); 130 | // } 131 | // 132 | // string enContent; 133 | // if (path.Contains(".json")) 134 | // { 135 | // if (!LangDataNormalizer.ProcessJsonSingle(enFile, out var ens)) throw new CheckException("Json 语法错误"); 136 | // enContent = ens.Select(x => $"{x.Key}={x.Value}").Connect("\n"); 137 | // } 138 | // else 139 | // { 140 | // enContent = LangDataNormalizer.ProcessLangSingle(enFile).Select(x => $"{x.Key}={x.Value}").Connect("\n"); 141 | // 142 | // } 143 | // 144 | // sb.AppendLine("[英文文件]\n"+enContent); 145 | // sb.AppendLine("[中文文件]\n" + cnFile); 146 | // 147 | // foreach (var oprr in await GitHub.Instance.PullRequest.ReviewComment.GetAll(Constants.RepoID, prid)) 148 | // { 149 | // sb.AppendLine("[Comment]"); 150 | // sb.AppendLine($"Id:{oprr.Id}"); 151 | // if (oprr.InReplyToId != null) 152 | // { 153 | // sb.AppendLine($"InReplyTo:{oprr.InReplyToId}"); 154 | // } 155 | // 156 | // sb.AppendLine("Content:"); 157 | // sb.AppendLine(oprr.Body); 158 | // sb.AppendLine("EndOfContent"); 159 | // } 160 | // 161 | // return sb.ToString(); 162 | // } 163 | 164 | private static HttpClient hc = new HttpClient() {Timeout = TimeSpan.FromMinutes(5)}; 165 | static async Task GetLangFileFromGitHub(string sha, string path) 166 | { 167 | try 168 | { 169 | var s = await hc.GetStringAsync( 170 | $"https://raw.githubusercontent.com/CFPAOrg/Minecraft-Mod-Language-Package/{sha}/{path}"); 171 | return s; 172 | } 173 | catch (Exception e) 174 | { 175 | return null; 176 | } 177 | } 178 | } -------------------------------------------------------------------------------- /CFPABot/CFPALLM/LangDataNormalizer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text; 4 | using System.Text.Json; 5 | using GammaLibrary.Extensions; 6 | 7 | namespace CFPABot.CFPALLM 8 | { 9 | record LangDataNormalizerData(List> EnOriginalData, List> CnOriginalData, List> NormalizedEnData, List> NormalizedCnData); 10 | 11 | internal class LangDataNormalizer 12 | { 13 | public static LangDataNormalizerData ProcessLangFile(string enfile, string cnfile) 14 | { 15 | var ens = ProcessLangSingle(enfile); 16 | var cns = ProcessLangSingle(cnfile); 17 | return ProcessLastStage(ens.ToList(), cns.ToList()); 18 | } 19 | 20 | public static IEnumerable> ProcessLangSingle(string enfile) 21 | { 22 | var ens = enfile.Split("\n") 23 | .Select(line => line.Trim()) // Trim 24 | .Where(line => !line.IsNullOrWhiteSpace()) // 移除空行 25 | .Where(line => !line.StartsWith("#")) // 移除注释 26 | .Where(line => line.Contains("=")) // 保证有等号 27 | .Select(line => 28 | { 29 | var s = line.Split("=", 2); 30 | return new KeyValuePair(s[0], s[1]); 31 | }) 32 | .DistinctBy(o => o.Key); 33 | return ens; 34 | } 35 | 36 | public static LangDataNormalizerData? ProcessJsonFile(string enfile, string cnfile) 37 | { 38 | if (ProcessJsonSingle(enfile, out var ens)) return null; 39 | if (ProcessJsonSingle(cnfile, out var cns)) return null; 40 | return ProcessLastStage(ens.ToList(), cns.ToList()); 41 | } 42 | 43 | public static bool ProcessJsonSingle(string enfile, out IEnumerable> ens) 44 | { 45 | var document = JsonDocument.Parse(enfile, new() { CommentHandling = JsonCommentHandling.Skip }); 46 | if (!document.RootElement.EnumerateObject().Any()) 47 | { 48 | ens = null; return false; 49 | } 50 | ens = document.RootElement 51 | .EnumerateObject() 52 | .Where(k => !k.Name.StartsWith("_")) 53 | .Select(o => new KeyValuePair(o.Name, o.Value.ValueKind == JsonValueKind.String ? o.Value.GetString() : o.Value.GetRawText())) 54 | .DistinctBy(o => o.Key); 55 | return true; 56 | } 57 | 58 | public static List> Unnormalize(List> data) 59 | { 60 | // a.b.c 61 | // ..d (a.b.d) 62 | // .e (a.e) 63 | // ..f (a.e.f) 64 | var decodedFullKey = new Queue(); 65 | var result = new List>(); 66 | foreach (var s in data) 67 | { 68 | var unprocessedKey = s.Key; 69 | var value = s.Value; 70 | var keyBuilder = new StringBuilder(); 71 | var endFlag = false; 72 | foreach (var c in unprocessedKey) 73 | { 74 | if (endFlag) 75 | { 76 | keyBuilder.Append(c); 77 | } 78 | else 79 | { 80 | if (c == '.') 81 | { 82 | keyBuilder.Append(decodedFullKey.Dequeue() + '.'); 83 | } 84 | else 85 | { 86 | endFlag = true; 87 | keyBuilder.Append(c); 88 | } 89 | } 90 | } 91 | 92 | var key = keyBuilder.ToString().TrimEnd('.'); 93 | result.Add(new KeyValuePair(key, value)); 94 | decodedFullKey = new Queue(key.Split('.')); 95 | } 96 | 97 | return result; 98 | } 99 | public static List> Normalize(List> data) 100 | { 101 | var queue = new Queue(0); 102 | var result = new List>(); 103 | foreach (var (key, value) in data) 104 | { 105 | var segments = key.Split('.'); 106 | var resultKey = new StringBuilder(); 107 | 108 | var difFlag = false; 109 | foreach (var seg in segments) 110 | { 111 | if (difFlag) 112 | { 113 | resultKey.Append(seg + "."); 114 | continue; 115 | } 116 | if (queue.TryDequeue(out var baseSeg)) 117 | { 118 | if (baseSeg == seg) 119 | { 120 | resultKey.Append("."); 121 | } 122 | else 123 | { 124 | resultKey.Append(seg + "."); 125 | difFlag = true; 126 | } 127 | } 128 | else 129 | { 130 | resultKey.Append(seg + "."); 131 | } 132 | } 133 | 134 | queue = new Queue(segments); 135 | result.Add(new KeyValuePair(resultKey.ToString().TrimEnd('.'), value)); 136 | } 137 | 138 | return result; 139 | } 140 | static LangDataNormalizerData ProcessLastStage(List> ens, List> cns) 141 | { 142 | List> ens2 = new(); 143 | List> cns2 = new(); 144 | foreach (var (key, env) in ens) 145 | { 146 | if (cns.FirstOrDefault(x => x.Key == key) is { } pair && pair.Key == null) continue; 147 | //if (!IsChinese(cnv)) continue; 148 | var cnv = pair.Value; 149 | ens2.Add(new KeyValuePair(key, env.Trim())); 150 | cns2.Add(new KeyValuePair(key, cnv.Trim())); 151 | } 152 | 153 | return new LangDataNormalizerData(ens2, cns2, Normalize(ens2), Normalize(cns2)); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /CFPABot/Checks/LabelCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CFPABot.Utils; 6 | using GammaLibrary.Extensions; 7 | using Octokit; 8 | 9 | namespace CFPABot.Checks 10 | { 11 | public class LabelCheck 12 | { 13 | private int prid; 14 | 15 | public LabelCheck(int prid) 16 | { 17 | this.prid = prid; 18 | } 19 | 20 | public async Task Run() 21 | { 22 | var labels = await GitHub.Instance.Issue.Labels.GetAllForIssue(Constants.Owner, Constants.RepoName, prid); 23 | var deniedLabels = new string[] {"NO-MERGE", "needs author action", "changes required", "ready to reject", "即将被搁置", "即将拒收" }; 24 | var result = labels.Any(label => deniedLabels.Contains(label.Name)); 25 | var pr = await GitHub.GetPullRequest(this.prid); 26 | 27 | await GitHub.Instance.Check.Run.Create(Constants.Owner, Constants.RepoName, 28 | new NewCheckRun("标签检查器", pr.Head.Sha) 29 | { 30 | Conclusion = new StringEnum(result ? CheckConclusion.Failure : CheckConclusion.Success), 31 | Status = new StringEnum(CheckStatus.Completed), 32 | Output = new NewCheckRunOutput(result ? $"检测到不能被 Merge 的标签:{labels.Where(label => deniedLabels.Contains(label.Name)).Select(label => label.Name).Connect()}。" : "标签检查通过。", ""), 33 | CompletedAt = DateTimeOffset.Now 34 | }); 35 | 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CFPABot/Checks/Labeler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CFPABot.Utils; 6 | using GammaLibrary.Extensions; 7 | using Octokit; 8 | 9 | namespace CFPABot.Checks 10 | { 11 | public class Labeler 12 | { 13 | private int prid; 14 | public Labeler(int prid) 15 | { 16 | this.prid = prid; 17 | } 18 | 19 | public async Task Run() 20 | { 21 | var labelManager = GitHub.Instance.Issue.Labels; 22 | var currentLabels = (await labelManager.GetAllForIssue(Constants.RepoID, prid)).Select(x => x.Name).ToHashSet(); 23 | var resultLabels = currentLabels.ToHashSet(); 24 | 25 | List<(string name, int min, int max)> lineLabels = new() 26 | { 27 | ("1+", 0, 10), 28 | ("10+", 10, 40), 29 | ("40+", 40, 100), 30 | ("100+", 100, 500), 31 | ("500+", 500, 1000), 32 | ("1000+", 1000, 2000), 33 | ("2000+", 2000, 5000), 34 | ("5000+", 5000, int.MaxValue), 35 | }; 36 | 37 | List<(string name, string pathPrefix)> pathLabels = new() 38 | { 39 | ("config", "config"), 40 | ("source", "src"), 41 | }; 42 | 43 | resultLabels.RemoveWhere(x => lineLabels.Any(l => l.name == x) || pathLabels.Any(l => l.name == x)); 44 | 45 | var pr = await GitHub.Instance.PullRequest.Files(Constants.RepoID, prid); 46 | var lines = pr.Sum(x => x.Changes); 47 | string lineTag = null; 48 | foreach (var (name, min, max) in lineLabels) 49 | { 50 | if (min <= lines && lines < max) 51 | { 52 | lineTag = name; 53 | break; 54 | } 55 | } 56 | if (lineTag == null) throw new ArgumentOutOfRangeException(); 57 | resultLabels.Add(lineTag); 58 | 59 | foreach (var (name, pathPrefix) in pathLabels) 60 | { 61 | if (pr.Any(x => x.FileName.StartsWith(pathPrefix))) 62 | { 63 | resultLabels.Add(name); 64 | } 65 | } 66 | 67 | currentLabels.SymmetricExceptWith(resultLabels); 68 | if (currentLabels.Count != 0) 69 | { 70 | await labelManager.ReplaceAllForIssue(Constants.RepoID, prid, resultLabels.ToArray()); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CFPABot/Command/GitRepoManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using CFPABot.Exceptions; 8 | using CFPABot.Utils; 9 | using Serilog; 10 | 11 | namespace CFPABot.Command 12 | { 13 | public sealed class GitRepoManager : IDisposable 14 | { 15 | public string WorkingDirectory { get; } 16 | 17 | public GitRepoManager() 18 | { 19 | WorkingDirectory = $"/app/caches/repos/{Guid.NewGuid():N}"; 20 | Directory.CreateDirectory(WorkingDirectory); 21 | } 22 | 23 | public void Clone(string repoOwner, string repoName, string branchName, bool noDepth) 24 | { 25 | Run($"clone https://x-access-token:{GitHub.GetToken()}@github.com/{repoOwner}/{repoName}.git --reference-if-able /app/repo-cache {(noDepth ? "" : "--depth=3")} . -b {branchName}"); 26 | Run("config user.name \"cfpa-bot[bot]\""); 27 | Run("config user.email \"101878103+cfpa-bot[bot]@users.noreply.github.com\""); 28 | } 29 | 30 | public static string[] SplitArguments(string commandLine) 31 | { 32 | var parmChars = commandLine.ToCharArray(); 33 | var inSingleQuote = false; 34 | var inDoubleQuote = false; 35 | for (var index = 0; index < parmChars.Length; index++) 36 | { 37 | if (parmChars[index] == '"' && !inSingleQuote) 38 | { 39 | inDoubleQuote = !inDoubleQuote; 40 | parmChars[index] = '\n'; 41 | } 42 | if (parmChars[index] == '\'' && !inDoubleQuote) 43 | { 44 | inSingleQuote = !inSingleQuote; 45 | parmChars[index] = '\n'; 46 | } 47 | if (!inSingleQuote && !inDoubleQuote && parmChars[index] == ' ') 48 | parmChars[index] = '\n'; 49 | } 50 | return (new string(parmChars)).Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries); 51 | } 52 | 53 | public void Commit(string message, GitHubUser coAuthor) 54 | { 55 | Run("commit -m \"" + 56 | $"{message}\n\n" + 57 | $"Co-authored-by: {coAuthor.Login} <{coAuthor.ID}+{coAuthor.Login}@users.noreply.github.com>" + 58 | "\""); 59 | } 60 | 61 | public void Push() 62 | { 63 | Run("push"); 64 | } 65 | 66 | public void AddAllFiles() 67 | { 68 | Run("add -A"); 69 | } 70 | 71 | public void Run(string args) 72 | { 73 | var process = Process.Start(new ProcessStartInfo("git", args) { RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = WorkingDirectory }); 74 | var stdout = ""; 75 | var stderr = ""; 76 | process.OutputDataReceived += (sender, eventArgs) => { stdout += eventArgs.Data; }; 77 | process.ErrorDataReceived += (sender, eventArgs) => { stderr += eventArgs.Data; }; 78 | process.BeginOutputReadLine(); 79 | process.BeginErrorReadLine(); 80 | process.WaitForExit(); 81 | if (process.ExitCode != 0) 82 | { 83 | // haha 84 | // https://github.com/Cyl18/CFPABot/issues/3 85 | // maybe Regex.Replace(message, "ghs_[0-9a-zA-Z]{36}", "******") 86 | Log.Error($"git.exe {args} exited with {process.ExitCode} - {stdout}{stderr}"); 87 | throw new ProcessException($"git.exe with args `{args}` exited with {process.ExitCode}."); 88 | } 89 | 90 | } 91 | 92 | public void Dispose() 93 | { 94 | try 95 | { 96 | Directory.Delete(WorkingDirectory, true); 97 | } 98 | catch (Exception e) 99 | { 100 | Log.Warning(e, "clean git repo"); 101 | } 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /CFPABot/CompositionHandler/CompositionFileHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.IO.Compression; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using CFPABot.Azusa; 9 | using GammaLibrary.Extensions; 10 | using NuGet.ContentModel; 11 | using Octokit; 12 | 13 | namespace CFPABot.CompositionHandler 14 | { 15 | public class CompositionFileHandler : IDisposable 16 | { 17 | 18 | private ForkRepoManager repo = new ForkRepoManager(); 19 | public CompositionFileHandler(PullRequest pr) 20 | { 21 | this.pr = pr; 22 | } 23 | 24 | private bool inited = false; 25 | private PullRequest pr; 26 | 27 | private void Init() 28 | { 29 | if (inited) return; 30 | inited = true; 31 | 32 | repo.Clone(pr.Head.User.Login, pr.Head.Repository.Name, branch: pr.Head.Ref); 33 | } 34 | 35 | private List packedVersions = new List(); 36 | public string AcquireLangFile(string curseId, string modid, string versionString) 37 | { 38 | Init(); 39 | RunPacker(versionString); 40 | using var zipArchive = new ZipArchive(File.OpenRead(Path.Combine(repo.WorkingDirectory, $"Minecraft-Mod-Language-Package-{versionString}.zip")), ZipArchiveMode.Read, false, Encoding.UTF8); 41 | foreach (var entry in zipArchive.Entries) 42 | { 43 | if (entry.FullName.Equals($"assets/{modid}/lang/zh_cn.lang", StringComparison.OrdinalIgnoreCase) || 44 | entry.FullName.Equals($"assets/{modid}/lang/zh_cn.json", StringComparison.OrdinalIgnoreCase)) 45 | { 46 | return entry.Open().ReadToEnd(); 47 | } 48 | } 49 | 50 | throw new FileNotFoundException($"找不到 {curseId}/{modid}/{versionString} 的组合文件"); 51 | } 52 | 53 | void RunPacker(string versionString) 54 | { 55 | if (packedVersions.Contains(versionString)) 56 | { 57 | return; 58 | } 59 | packedVersions.Add(versionString); 60 | var process = Process.Start(new ProcessStartInfo("Packer", $"--version=\"{versionString}\"") { WorkingDirectory = repo.WorkingDirectory, RedirectStandardOutput = true }); 61 | process.StandardOutput.ReadToEnd(); 62 | process.WaitForExit(); 63 | } 64 | 65 | public void Dispose() 66 | { 67 | repo?.Dispose(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CFPABot/Controllers/BMCLController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Runtime.Serialization; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization; 8 | using System.Threading.Tasks; 9 | using CFPABot.DiffEngine; 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.AspNetCore.Mvc; 12 | 13 | namespace CFPABot.Controllers 14 | { 15 | 16 | [Route("api/[controller]")] 17 | [ApiController] 18 | public class BMCLController : ControllerBase 19 | { 20 | [HttpGet("ModList")] 21 | public IActionResult ModList() 22 | { 23 | return RedirectPermanent("/api/BakaXL/ModList"); 24 | } 25 | 26 | } 27 | [Route("api/[controller]")] 28 | [ApiController] 29 | public class BakaXLController : ControllerBase 30 | { 31 | [HttpGet("ModList")] 32 | public string ModList() 33 | { 34 | var list = new List(); 35 | var config = Azusa.Pages.ModList.ModListConfig.Instance; 36 | foreach (var (modSlug, modName, modDomain, curseForgeLink, versions) in config.ModLists) 37 | { 38 | var list1 = new List(); 39 | foreach (var (version, repoLink) in versions) 40 | { 41 | list1.Add(new VersionModel(ModPath.GetVersionDirectory(version.MinecraftVersion, ModLoader.Forge), version.ModLoader.ToString(), repoLink)); 42 | } 43 | 44 | list.Add(new ModListModel(modSlug, modName, modDomain, curseForgeLink, list1)); 45 | } 46 | 47 | var options = new JsonSerializerOptions 48 | { 49 | WriteIndented = false, 50 | }; 51 | 52 | 53 | return JsonSerializer.Serialize(new BMCLModListModel(list, config.LastTime), options); 54 | } 55 | 56 | } 57 | 58 | public record BMCLModListModel(List modlist, DateTime lastUpdate); 59 | 60 | public record VersionModel(string version, string loader, string repoLink); 61 | public record ModListModel(string modSlug, string modName, string modDomain, string curseForgeLink, List versions); 62 | 63 | 64 | } 65 | -------------------------------------------------------------------------------- /CFPABot/Controllers/CFPAToolsController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CFPABot.DiffEngine; 6 | using CFPABot.PRData; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | 10 | namespace CFPABot.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | [ApiController] 14 | public class CFPAToolsController : ControllerBase 15 | { 16 | [HttpGet("PRRelation/{prid}")] 17 | public IActionResult PRRelation(int prid) 18 | { 19 | var mods = new List(); 20 | var emods = PRDataManager.Relation 21 | .Where(x => 22 | x.Value.Any(y => y.prid == prid)).ToArray(); 23 | foreach (var (key, value) in emods) 24 | { 25 | foreach (var tuple in value) 26 | { 27 | var modId = PRDataManager.GetModID(prid, tuple.modVersion, key); 28 | if (modId == null) continue; // 排除主PR没有此版本的情况 29 | 30 | var gameVersion = tuple.modVersion.ToVersionDirectory(); 31 | var link = PRDataManager.GetPath(prid, tuple.modVersion, key); 32 | var otherPrs = new List(); 33 | foreach (var (s, hashSet) in PRDataManager.Relation 34 | .Where(x => 35 | x.Key == key)) 36 | { 37 | foreach (var (subPrid, modVersion) in hashSet) 38 | { 39 | if (subPrid == prid) continue; // 排除当前pr 40 | 41 | var subLink = PRDataManager.GetPath(subPrid, modVersion, key); 42 | otherPrs.Add(new OtherPrs(subPrid, subLink.en, subLink.cn, modVersion.ToVersionDirectory())); 43 | 44 | } 45 | 46 | } 47 | mods.Add(new Mod("curseforge", key, modId, link.en, link.cn, 48 | gameVersion, otherPrs)); 49 | } 50 | } 51 | 52 | if (mods.Count == 0) 53 | { 54 | return StatusCode(418); 55 | } 56 | //var res = new PRRelationResult(prid,); 57 | return new JsonResult(new PRRelationResult(prid, mods)); 58 | } 59 | } 60 | 61 | record PRRelationResult(int number, List mod_list); 62 | 63 | record Mod(string type, string id, string modid, string enlink, string zhlink, string version, List other); 64 | 65 | record OtherPrs(int number, string enlink, string zhlink, string version); 66 | } 67 | -------------------------------------------------------------------------------- /CFPABot/Controllers/GitHubOAuthController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | using System.Net.Http.Json; 6 | using System.Text; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | using CFPABot.Azusa; 10 | using CFPABot.Utils; 11 | using Microsoft.AspNetCore.Http; 12 | using Microsoft.AspNetCore.Mvc; 13 | using NETCore.Encrypt; 14 | using Octokit; 15 | using Octokit.Internal; 16 | using ProductHeaderValue = Octokit.ProductHeaderValue; 17 | 18 | namespace CFPABot.Controllers 19 | { 20 | [Route("api/[controller]")] 21 | [ApiController] 22 | public class GitHubOAuthController : ControllerBase 23 | { 24 | static HttpClient hc; 25 | 26 | static GitHubOAuthController() 27 | { 28 | hc = new(); 29 | hc.DefaultRequestHeaders.Accept.Clear(); 30 | hc.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 31 | } 32 | [HttpGet] 33 | public async Task OAuth([FromQuery] string code) 34 | { 35 | var p = await hc.PostAsync("https://github.com/login/oauth/access_token", new FormUrlEncodedContent(new [] 36 | { 37 | new KeyValuePair("client_id", Constants.GitHubOAuthClientId), 38 | new KeyValuePair("code", code), 39 | new KeyValuePair("client_secret", Environment.GetEnvironmentVariable("CFPA_HELPER_GITHUB_OAUTH_CLIENT_SECRET")), 40 | 41 | })); 42 | var clientAccessToken = JsonDocument.Parse(await p.Content.ReadAsStream().ReadToEndAsync1()).RootElement.GetProperty("access_token").GetString(); 43 | try 44 | { 45 | var client = LoginManager.GetGitHubClient(clientAccessToken); 46 | await client.Repository.Get(Constants.RepoID); 47 | } 48 | catch (Exception e) 49 | { 50 | return Content($"验证错误: {e.Message}"); 51 | } 52 | 53 | if (!System.IO.File.Exists("config/encrypt_key.txt")) 54 | { 55 | System.IO.File.WriteAllText("config/encrypt_key.txt", Guid.NewGuid().ToString("N"), new UTF8Encoding(false)); 56 | } 57 | HttpContext.Response.Cookies.Append(Constants.GitHubOAuthTokenCookieName, EncryptProvider.AESEncrypt(clientAccessToken, 58 | System.IO.File.ReadAllText("config/encrypt_key.txt"), "CACTUS&MAMARUO!!"), new CookieOptions() {HttpOnly = true, MaxAge = TimeSpan.FromDays(7)}); 59 | return Redirect("/Azusa"); 60 | } 61 | 62 | [HttpGet("Signout")] 63 | public IActionResult Signout() 64 | { 65 | HttpContext.Response.Cookies.Delete(Constants.GitHubOAuthTokenCookieName); 66 | return Redirect("/Azusa"); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CFPABot/Controllers/UtilsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using CFPABot.Azusa.Pages; 9 | using CFPABot.DiffEngine; 10 | using CFPABot.Utils; 11 | using CurseForge.APIClient.Models.Mods; 12 | using GammaLibrary.Extensions; 13 | using Octokit; 14 | using Octokit.Webhooks.Models.PullRequestEvent; 15 | using System.IO; 16 | 17 | namespace CFPABot.Controllers 18 | { 19 | [Route("api/[Controller]")] 20 | [ApiController] 21 | public class UtilsController : ControllerBase 22 | { 23 | [HttpGet("PathValidation")] 24 | public async Task PathValidation([FromQuery] string pr) 25 | { 26 | var fileDiff = await GitHub.Diff(pr.ToInt()); 27 | var sb = new StringBuilder(); 28 | foreach (var diff1 in fileDiff.Where(diff => !diff.To.ToCharArray().All(x => char.IsDigit(x) || char.IsLower(x) || x is '_' or '-' or '.' or '/') && diff.To.Contains("lang"))) 29 | { 30 | sb.AppendLine($"{diff1.To}"); 31 | } 32 | 33 | return sb.ToString(); 34 | } 35 | 36 | 37 | [HttpGet("GitHubToken")] 38 | public IActionResult GitHubToken() 39 | { 40 | if (!VerifyAccess()) return Unauthorized(); 41 | return Content(GitHub.GetToken()); 42 | } 43 | 44 | [HttpGet("ModID")] 45 | public async Task ModID([FromQuery]string slug, [FromQuery] string versionString) 46 | { 47 | return await CurseManager.GetModID(await CurseManager.GetAddon(slug), versionString.ToMCVersion(), true, false); 48 | } 49 | 50 | [HttpGet("GetAllModFilesInRepo")] 51 | public async Task GetAllModFilesInRepo() 52 | { 53 | var mods = ModList.ModListConfig.Instance.ModLists.Select(x => new {slug=x.modSlug, cfid= ModIDMappingMetadata.Instance.Mapping.GetValueOrDefault(x.modSlug), versions = x.versions.Select(y => y.version.ToVersionDirectory()) }) 54 | .Where(x => x.cfid != 0); 55 | 56 | return new JsonResult(mods); 57 | } 58 | 59 | [HttpGet("SendMail")] 60 | public IActionResult SendMail([FromQuery] string password, [FromQuery] string mail) 61 | { 62 | if (password != Constants.GitHubWebhookSecret) return Unauthorized(); 63 | MailUtils.SendNotification(mail, "https://github.com/CFPAOrg/Minecraft-Mod-Language-Package/pull/3731"); 64 | return Ok(); 65 | } 66 | 67 | [HttpGet("GetCsv/{prid}")] 68 | public IActionResult GetCsv(string prid) 69 | { 70 | var dir = "/app/caches/csv"; 71 | var path1 = dir + $"/{prid}.csv"; 72 | 73 | return File(System.IO.File.ReadAllBytes(path1), "text/csv"); 74 | } 75 | [HttpGet("GetDiff/{prid}")] 76 | public IActionResult GetDiff(string prid) 77 | { 78 | var dir = "/app/caches/csv"; 79 | var path1 = dir + $"/{prid}.md"; 80 | HttpContext.Response.Headers.AcceptCharset = "utf-8"; 81 | return File(System.IO.File.ReadAllBytes(path1), "text/markdown"); 82 | } 83 | 84 | [HttpGet("Diff/{prid}")] 85 | public async Task Diff(string prid) 86 | { 87 | var s = await Download.String(Constants.BaseRepoUrl + $"/pull/{prid}.diff", true); 88 | return Content(s); 89 | } 90 | 91 | [HttpGet("ModName/{slug}")] 92 | public async Task GetModName(string slug) 93 | { 94 | try 95 | { 96 | if (slug.StartsWith("modrinth-")) 97 | { 98 | slug = slug["modrinth-".Length..]; 99 | var mod = await ModrinthManager.GetMod(slug); 100 | return Content(mod.Title); 101 | } 102 | else 103 | { 104 | var mod = await CurseManager.GetAddon(slug); 105 | return Content(mod.Name); 106 | } 107 | 108 | } 109 | catch (Exception e) 110 | { 111 | return StatusCode(418, e.GetType() + "\n" + e.Message); 112 | } 113 | } 114 | 115 | [HttpGet("AddMapping/{id}")] 116 | public async Task AddMapping(string id) 117 | { 118 | try 119 | { 120 | var addon = await CurseManager.GetAddon(id.ToInt()); 121 | ModIDMappingMetadata.Instance.Mapping[addon.Slug] = id.ToInt(); 122 | ModIDMappingMetadata.Save(); 123 | return Content($"成功添加 Mapping:{addon.Slug} => {id}"); 124 | } 125 | catch (Exception e) 126 | { 127 | return Content("失败:"+e.Message); 128 | } 129 | } 130 | 131 | private bool VerifyAccess() 132 | { 133 | return HttpContext.Request.Headers["Authorization"].FirstOrDefault() == Constants.GitHubWebhookSecret; 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /CFPABot/CronTasks.cs: -------------------------------------------------------------------------------- 1 | namespace CFPABot; 2 | 3 | public class CronTask 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /CFPABot/DiffEngine/LangDiffer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace CFPABot.DiffEngine 5 | { 6 | public record LangDiffLine(string Key, string SourceEn, string CurrentEn, string SourceCn, string CurrentCn); 7 | public static class LangDiffer 8 | { 9 | public static List Run(LangFilePair lang) 10 | { 11 | var list = new List(); 12 | foreach (var (key, currentCnValue) in lang.ToCNFile.Content) 13 | { 14 | var fromEnValue = lang.FromENFile?.Content.GetValueOrDefault(key); 15 | var fromCnValue = lang.FromCNFile?.Content.GetValueOrDefault(key); 16 | var currentEnValue = lang.ToEnFile?.Content.GetValueOrDefault(key); 17 | list.Add(new LangDiffLine(key?.PostProcess(), 18 | fromEnValue?.PostProcess(), 19 | currentEnValue?.PostProcess(), 20 | fromCnValue?.PostProcess(), 21 | currentCnValue?.PostProcess())); 22 | } 23 | 24 | var g = list.ToArray(); 25 | var g1 = g.Where(l => l.CurrentEn != l.CurrentCn); 26 | var g2 = g.Where(l => l.CurrentEn == l.CurrentCn); 27 | return g1.Concat(g2).ToList(); 28 | } 29 | 30 | private static string PostProcess(this string str) 31 | { 32 | return str; 33 | if (str.Contains("$")) 34 | { 35 | return $"`{str.Replace("<", "\\<").Replace("`", "\\`").Replace("\n", "[换行符]")}`"; 36 | } 37 | else 38 | { 39 | return str.Replace("<", "\\<").Replace("`", "\\`").Replace("\n", "
").Replace("*", "\\*").Replace("|", "\\|"); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /CFPABot/DiffEngine/LangFile.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using CFPABot.Utils; 7 | using GammaLibrary.Extensions; 8 | 9 | namespace CFPABot.DiffEngine 10 | { 11 | public class LangFileWrapper 12 | { 13 | private string url { get; set; } 14 | private string content { get; set; } 15 | private bool isWeb { get; set; } = false; 16 | private bool isLangFile { get; set; } = false; 17 | private LangFileType type { get; set; } 18 | private LangFile langFile { get; set; } 19 | 20 | public static LangFileWrapper FromWebUrl(string url) 21 | { 22 | return new LangFileWrapper() { url = url, isWeb = true }; 23 | } 24 | public static LangFileWrapper FromContent(string content) 25 | { 26 | return new LangFileWrapper() { content = content, isWeb = false }; 27 | } 28 | 29 | public static LangFileWrapper FromLangFile(LangFile content) 30 | { 31 | return new LangFileWrapper() { langFile = content, isLangFile = true }; 32 | } 33 | 34 | public static LangFileType GuessType(string content) 35 | { 36 | // 我觉得不会有作者写成一行的 /**/ { 吧???? 37 | if (content.Split('\n').Any(x => x.TrimStart().StartsWith('{'))) 38 | return LangFileType.Json; 39 | return LangFileType.Lang; 40 | } 41 | 42 | public async ValueTask Get() 43 | { 44 | if (isWeb) 45 | { 46 | var s = await Download.String(url); 47 | return LangFile.FromString(s, GuessType(s)); 48 | } 49 | if (isLangFile) 50 | { 51 | return langFile; 52 | } 53 | return LangFile.FromString(content, GuessType(content)); 54 | 55 | } 56 | } 57 | 58 | public class LangFile 59 | { 60 | public Dictionary Content { get; private set; }= new Dictionary(); 61 | public static LangFile Empty { get; } = new LangFile() { Content = new Dictionary() }; 62 | public string OriginalFile { get; private set; } 63 | private LangFile() 64 | { 65 | } 66 | 67 | public static LangFile FromString(string content, LangFileType langFileType) 68 | { 69 | var result = new LangFile(); 70 | result.OriginalFile = content; 71 | switch (langFileType) 72 | { 73 | case LangFileType.Lang: 74 | result.LoadLang(content); 75 | break; 76 | case LangFileType.Json: 77 | result.LoadJson(content); 78 | break; 79 | } 80 | return result; 81 | } 82 | 83 | void LoadJson(string content) 84 | { 85 | var document = JsonDocument.Parse(content, new() { CommentHandling = JsonCommentHandling.Skip }); 86 | if (!document.RootElement.EnumerateObject().Any()) return; 87 | Content = document.RootElement 88 | .EnumerateObject() 89 | .Where(k => !k.Name.StartsWith("_")) 90 | .Select(o => new KeyValuePair(o.Name, o.Value.ValueKind == JsonValueKind.String ? o.Value.GetString() : o.Value.GetRawText())) 91 | .DistinctBy(o => o.Key) // workaround https://github.com/CFPAOrg/Minecraft-Mod-Language-Package/pull/2070 92 | .ToDictionary(o => o.Key, o => o.Value); 93 | } 94 | 95 | void LoadLang(string content) 96 | { 97 | Content = content.Split("\n") 98 | .Select(line => line.Trim()) // Trim 99 | .Where(line => !line.IsNullOrWhiteSpace()) // 移除空行 100 | .Where(line => !line.StartsWith("#")) // 移除注释 101 | .Where(line => line.Contains("=")) // 保证有等号 102 | .Select(line => 103 | { 104 | var s = line.Split("=", 2); 105 | return new KeyValuePair(s[0], s[1]); 106 | }) 107 | .DistinctBy(o => o.Key) // workaround https://github.com/CFPAOrg/Minecraft-Mod-Language-Package/pull/2198 108 | .ToDictionary(o => o.Key, o => o.Value); // 提取 Key 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CFPABot/DiffEngine/LangFileFetcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CFPABot.Exceptions; 6 | using CFPABot.Utils; 7 | using Serilog; 8 | 9 | namespace CFPABot.DiffEngine 10 | { 11 | public record LangFilePair(LangFile FromENFile, LangFile ToEnFile, LangFile FromCNFile, LangFile ToCNFile, ModPath ModPath); 12 | public class LangFileFetcher 13 | { 14 | public static async Task<(LangFile fromEnLang, LangFile fromCnLang)> FromRepo(int prid, string slug, ModVersion version) 15 | { 16 | 17 | var diff = await GitHub.Diff(prid); 18 | var pr = await GitHub.GetPullRequest(prid); 19 | var mods = PRAnalyzer.RunBleedingEdge(diff).Where(x => x.CurseForgeSlug == slug).ToArray(); 20 | var fromCommit = pr.Base.Sha; 21 | for (var index = 0; index < mods.Length; index++) 22 | { 23 | var mod = mods[index]; 24 | mod.ModVersion = version; 25 | 26 | var fromEn1 = new LangFilePath(mod, LangType.EN).FetchFromCommit(fromCommit); 27 | var fromCn1 = new LangFilePath(mod, LangType.CN).FetchFromCommit(fromCommit); 28 | var fromEn = await fromEn1; 29 | var fromCn = await fromCn1; 30 | var fromEnLang = fromEn == null ? null : LangFile.FromString(fromEn, mod.LangFileType); 31 | var fromCnLang = fromCn == null ? null : LangFile.FromString(fromCn, mod.LangFileType); 32 | return (fromEnLang, fromCnLang); 33 | } 34 | 35 | return (null, null); 36 | } 37 | 38 | public static async Task> FromPR(int prid, List outExceptions) 39 | { 40 | var diff = await GitHub.Diff(prid); 41 | var pr = await GitHub.GetPullRequest(prid); 42 | var mods = PRAnalyzer.RunBleedingEdge(diff); 43 | var fromCommit = pr.Base.Sha; 44 | var prCommit = pr.Head.Sha; 45 | 46 | var list = new List(); 47 | foreach (var mod in mods) 48 | { 49 | try 50 | { 51 | var fromEn1 = new LangFilePath(mod, LangType.EN).FetchFromCommit(fromCommit); 52 | var fromCn1 = new LangFilePath(mod, LangType.CN).FetchFromCommit(fromCommit); 53 | var prEn1 = new LangFilePath(mod, LangType.EN).FetchFromCommit(prCommit); 54 | var prCn1 = new LangFilePath(mod, LangType.CN).FetchFromCommit(prCommit); 55 | 56 | await Task.WhenAll(fromEn1, fromCn1, prEn1, prCn1); 57 | var fromEn = await fromEn1; 58 | var fromCn = await fromCn1; 59 | var prEn = await prEn1; 60 | var prCn = await prCn1; 61 | 62 | var fromEnLang = fromEn == null ? null : LangFile.FromString(fromEn, mod.LangFileType); 63 | var fromCnLang = fromCn == null ? null : LangFile.FromString(fromCn, mod.LangFileType); 64 | var prEnLang = prEn == null ? null : LangFile.FromString(prEn, mod.LangFileType); 65 | var prCnLang = prCn == null ? null : LangFile.FromString(prCn, mod.LangFileType); 66 | 67 | 68 | list.Add(new LangFilePair(fromEnLang, prEnLang, fromCnLang, prCnLang, mod)); 69 | } 70 | catch (Exception e) 71 | { 72 | Log.Warning(e, "LangFileFetcher"); 73 | outExceptions.Add(e); 74 | } 75 | } 76 | 77 | return list; 78 | } 79 | 80 | 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /CFPABot/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base 4 | ENV TZ=Asia/Shanghai 5 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 6 | RUN printf "deb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye main contrib non-free\ndeb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-updates main contrib non-free\ndeb https://mirrors.tuna.tsinghua.edu.cn/debian/ bullseye-backports main contrib non-free\ndeb https://mirrors.tuna.tsinghua.edu.cn/debian-security bullseye-security main contrib non-free" > /etc/apt/sources.list 7 | RUN apt -y update && apt install -y git && apt install -y curl && rm -rf /var/lib/apt/lists/* 8 | WORKDIR /app 9 | EXPOSE 80 10 | 11 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS publish 12 | WORKDIR /src 13 | COPY ["CFPABot/CFPABot.csproj", "CFPABot/"] 14 | RUN dotnet restore "CFPABot/CFPABot.csproj" 15 | COPY . . 16 | RUN dotnet clean CFPABot.sln --configuration "Debug" 17 | WORKDIR "/src/CFPABot" 18 | RUN rm -rf bin && rm -rf obj 19 | RUN dotnet publish "CFPABot.csproj" -r debian.9-x64 --no-self-contained -c Debug -o /app/publish && mkdir -p /app/publish/wwwrootx/css && cp /src/CFPABot/obj/Debug/net7.0/debian.9-x64/scopedcss/bundle/CFPABot.styles.css /app/publish/wwwrootx/css/ 20 | 21 | FROM base AS final 22 | WORKDIR /app 23 | COPY --from=publish /app/publish . 24 | ENV LD_LIBRARY_PATH=/app/runtimes/debian.9-x64/native/ 25 | HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD curl --fail http://127.0.0.1:8080/healthcheck || exit 1 26 | ENTRYPOINT ["dotnet", "CFPABot.dll"] -------------------------------------------------------------------------------- /CFPABot/Exceptions/CheckException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Threading.Tasks; 6 | 7 | namespace CFPABot.Exceptions 8 | { 9 | [Serializable] 10 | public class CheckException : Exception 11 | { 12 | 13 | public CheckException(string message) : base(message) 14 | { 15 | } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CFPABot/Exceptions/CommandExceptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Text.RegularExpressions; 6 | using System.Threading.Tasks; 7 | 8 | namespace CFPABot.Exceptions 9 | { 10 | [Serializable] 11 | public class ProcessException : Exception 12 | { 13 | public ProcessException(string message) : base(message) 14 | { 15 | 16 | } 17 | } 18 | 19 | [Serializable] 20 | public class CommandException : Exception 21 | { 22 | public CommandException(string message) : base(message) 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CFPABot/Exceptions/LangFileNotExistsException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CFPABot.Exceptions 4 | { 5 | public class LangFileNotExistsException : Exception 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CFPABot/Exceptions/WTFException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace CFPABot.Exceptions 5 | { 6 | [Serializable] 7 | public class WTFException : Exception 8 | { 9 | public WTFException() 10 | { 11 | } 12 | 13 | public WTFException(string message) : base(message) 14 | { 15 | } 16 | 17 | public WTFException(string message, Exception inner) : base(message, inner) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CFPABot/Exceptions/WebhookException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using JetBrains.Annotations; 6 | 7 | namespace CFPABot.Exceptions 8 | { 9 | public class WebhookException : Exception 10 | { 11 | public WebhookException(string message) : base(message) 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CFPABot/IDK/TranslationRequestUpdater.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Text.RegularExpressions; 3 | using System.Threading.Tasks; 4 | using CFPABot.Utils; 5 | 6 | namespace CFPABot.IDK 7 | { 8 | public partial class TranslationRequestUpdater 9 | { 10 | [GeneratedRegex("\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|")] 11 | private static partial Regex r(); 12 | 13 | public static async Task Run() 14 | { 15 | var original = await GitHub.Instance.Issue.Get(Constants.RepoID, 2702); 16 | 17 | } 18 | 19 | private static string Normalize(string s) 20 | { 21 | if (s == null) return s; 22 | 23 | var l = s.ToLowerInvariant().ToCharArray().ToList(); 24 | l.RemoveAll(x => x == ' ' || !char.IsLetterOrDigit(x)); 25 | return new string(l.ToArray()); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CFPABot/LanguageCore/JsonFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | using System.Text.Encodings.Web; 7 | using System.Text.Json; 8 | using System.Text.RegularExpressions; 9 | using System.Text.Unicode; 10 | 11 | using Newtonsoft.Json; 12 | using Newtonsoft.Json.Linq; 13 | 14 | using JsonSerializer = System.Text.Json.JsonSerializer; 15 | 16 | // Attribution-NonCommercial-ShareAlike 4.0 International 17 | // by baka-gourd 18 | 19 | namespace Language.Core 20 | { 21 | public sealed class JsonFormatter 22 | { 23 | private readonly StreamReader _reader; 24 | private readonly StreamWriter _writer; 25 | public JsonFormatter(StreamReader reader, StreamWriter writer) 26 | { 27 | _reader = reader; 28 | _writer = writer; 29 | } 30 | 31 | public void Format() 32 | { 33 | var builder = new StringBuilder(); 34 | while (!_reader.EndOfStream) 35 | { 36 | builder.AppendLine(_reader.ReadLine()); 37 | } 38 | 39 | _reader.BaseStream.Seek(0, SeekOrigin.Begin); 40 | 41 | try 42 | { 43 | if (string.IsNullOrEmpty(builder.ToString())) 44 | { 45 | throw new NullReferenceException(); 46 | } 47 | 48 | if (string.IsNullOrWhiteSpace(builder.ToString())) 49 | { 50 | throw new NullReferenceException(); 51 | } 52 | //有憨憨作者在json里写除了string以外的内容全部抛出 53 | JsonSerializer.Deserialize>(builder.ToString(), new JsonSerializerOptions() 54 | { 55 | AllowTrailingCommas = true, 56 | ReadCommentHandling = JsonCommentHandling.Skip, 57 | Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping 58 | }); 59 | 60 | var jr = new JsonTextReader(_reader); 61 | var jo = new JObject(); 62 | var jt = (JObject)JToken.ReadFrom(jr, new JsonLoadSettings() { DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Ignore, CommentHandling = CommentHandling.Ignore }); 63 | foreach (var (key, value) in jt) 64 | { 65 | jo.Add(key, value.Value()); 66 | } 67 | _writer.Write(jo.ToString()); 68 | _writer.Close(); 69 | _writer.Dispose(); 70 | _reader.Close(); 71 | _reader.Dispose(); 72 | } 73 | catch 74 | { 75 | if (!Directory.Exists($"{Directory.GetCurrentDirectory()}/broken")) 76 | { 77 | Directory.CreateDirectory($"{Directory.GetCurrentDirectory()}/broken"); 78 | } 79 | _writer.Write("{}"); 80 | _writer.Close(); 81 | _writer.Dispose(); 82 | _reader.Close(); 83 | _reader.Dispose(); 84 | //File.WriteAllText($"{Directory.GetCurrentDirectory()}/broken/{_modName}{DateTime.UtcNow.Millisecond}.json", builder.ToString()); 85 | } 86 | } 87 | } 88 | } -------------------------------------------------------------------------------- /CFPABot/LanguageCore/LangFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Language.Core 5 | { 6 | 7 | /* 8 | * MIT LICENSE 9 | * 10 | * Copyright 2021 Cyl18 11 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 14 | */ 15 | public sealed class LangFormatter 16 | { 17 | readonly StreamReader _reader; 18 | readonly StreamWriter _writer; 19 | // ReSharper disable once InconsistentNaming 20 | const int EOF = -1; 21 | 22 | /// 23 | /// 初始化格式化器类,需要提供两个参数:reader和writer 24 | /// 25 | /// 26 | /// 27 | public LangFormatter(StreamReader reader, StreamWriter writer) 28 | { 29 | _reader = reader; 30 | _writer = writer; 31 | } 32 | 33 | /// 34 | /// 格式化语言文件 35 | /// 36 | public void Format() 37 | { 38 | int c; 39 | var lineStart = true; 40 | var multiLineComment = false; 41 | while ((c = Consume()) != EOF) 42 | { 43 | if (lineStart) 44 | { 45 | switch (c) 46 | { 47 | case '/': 48 | switch (Peek()) 49 | { 50 | // '//' 的实现仅支持行首出现 在 enderio 的语言文件有 YouTube 链接 会导致问题 51 | // 当然也可以手动设置开关 52 | case '/': 53 | Consume(); 54 | Write('#'); 55 | continue; 56 | // '/**/' 的实现假定 '/*' 仅仅在行首出现 '*/' 仅在行末出现 57 | case '*': 58 | Consume(); 59 | multiLineComment = true; 60 | continue; 61 | } 62 | break; 63 | case '=': 64 | SkipLine(); 65 | continue; 66 | } 67 | 68 | if (multiLineComment) Write('#'); 69 | } 70 | 71 | if (multiLineComment && c == '*' && Peek() == '/') 72 | { 73 | Consume(); 74 | multiLineComment = false; 75 | continue; 76 | } 77 | lineStart = c == '\n'; 78 | Write(c); 79 | } 80 | 81 | _writer.Close(); 82 | _writer.Dispose(); 83 | _reader.Close(); 84 | _reader.Dispose(); 85 | } 86 | void SkipLine() 87 | { 88 | int c; 89 | while ((c = Consume()) != '\n' && c != EOF) { } 90 | } 91 | 92 | int Peek() => _reader.Peek(); 93 | int Consume() => _reader.Read(); 94 | void Write(char c) => _writer.Write(c); 95 | void Write(int c) => Write((char)c); 96 | } 97 | } -------------------------------------------------------------------------------- /CFPABot/Models/ArtifactModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; 6 | 7 | namespace CFPABot.Models.Artifact 8 | { 9 | public partial class ArtifactModel 10 | { 11 | [J("total_count")] public long TotalCount { get; set; } 12 | [J("artifacts")] public Artifact[] Artifacts { get; set; } 13 | } 14 | 15 | public partial class Artifact 16 | { 17 | [J("id")] public long Id { get; set; } 18 | [J("node_id")] public string NodeId { get; set; } 19 | [J("name")] public string Name { get; set; } 20 | [J("size_in_bytes")] public long SizeInBytes { get; set; } 21 | [J("url")] public Uri Url { get; set; } 22 | [J("archive_download_url")] public string ArchiveDownloadUrl { get; set; } 23 | [J("expired")] public bool Expired { get; set; } 24 | [J("created_at")] public DateTimeOffset CreatedAt { get; set; } 25 | [J("updated_at")] public DateTimeOffset UpdatedAt { get; set; } 26 | [J("expires_at")] public DateTimeOffset ExpiresAt { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CFPABot/Models/WorkflowRunModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; 6 | 7 | namespace CFPABot.Models.Workflow 8 | { 9 | 10 | public partial class WorkflowRunModel 11 | { 12 | [J("total_count")] public long TotalCount { get; set; } 13 | [J("workflow_runs")] public WorkflowRun[] WorkflowRuns { get; set; } 14 | } 15 | 16 | public partial class WorkflowRun 17 | { 18 | [J("id")] public long Id { get; set; } 19 | [J("name")] public string Name { get; set; } 20 | [J("node_id")] public string NodeId { get; set; } 21 | [J("head_branch")] public string HeadBranch { get; set; } 22 | [J("head_sha")] public string HeadSha { get; set; } 23 | [J("run_number")] public long RunNumber { get; set; } 24 | [J("event")] public string Event { get; set; } 25 | [J("status")] public string Status { get; set; } 26 | [J("conclusion")] public string Conclusion { get; set; } 27 | [J("workflow_id")] public long WorkflowId { get; set; } 28 | [J("check_suite_id")] public long CheckSuiteId { get; set; } 29 | [J("check_suite_node_id")] public string CheckSuiteNodeId { get; set; } 30 | [J("url")] public string Url { get; set; } 31 | [J("html_url")] public string HtmlUrl { get; set; } 32 | [J("pull_requests")] public object[] PullRequests { get; set; } 33 | [J("created_at")] public DateTimeOffset CreatedAt { get; set; } 34 | [J("updated_at")] public DateTimeOffset UpdatedAt { get; set; } 35 | [J("actor")] public Actor Actor { get; set; } 36 | [J("run_attempt")] public long RunAttempt { get; set; } 37 | [J("run_started_at")] public DateTimeOffset RunStartedAt { get; set; } 38 | [J("triggering_actor")] public Actor TriggeringActor { get; set; } 39 | [J("jobs_url")] public string JobsUrl { get; set; } 40 | [J("logs_url")] public string LogsUrl { get; set; } 41 | [J("check_suite_url")] public string CheckSuiteUrl { get; set; } 42 | [J("artifacts_url")] public string ArtifactsUrl { get; set; } 43 | [J("cancel_url")] public string CancelUrl { get; set; } 44 | [J("rerun_url")] public string RerunUrl { get; set; } 45 | [J("previous_attempt_url")] public object PreviousAttemptUrl { get; set; } 46 | [J("workflow_url")] public string WorkflowUrl { get; set; } 47 | [J("head_commit")] public HeadCommit HeadCommit { get; set; } 48 | [J("repository")] public Repository Repository { get; set; } 49 | [J("head_repository")] public Repository HeadRepository { get; set; } 50 | } 51 | 52 | public partial class Actor 53 | { 54 | [J("login")] public string Login { get; set; } 55 | [J("id")] public long Id { get; set; } 56 | [J("node_id")] public string NodeId { get; set; } 57 | [J("avatar_url")] public string AvatarUrl { get; set; } 58 | [J("gravatar_id")] public string GravatarId { get; set; } 59 | [J("url")] public string Url { get; set; } 60 | [J("html_url")] public string HtmlUrl { get; set; } 61 | [J("followers_url")] public string FollowersUrl { get; set; } 62 | [J("following_url")] public string FollowingUrl { get; set; } 63 | [J("gists_url")] public string GistsUrl { get; set; } 64 | [J("starred_url")] public string StarredUrl { get; set; } 65 | [J("subscriptions_url")] public string SubscriptionsUrl { get; set; } 66 | [J("organizations_url")] public string OrganizationsUrl { get; set; } 67 | [J("repos_url")] public string ReposUrl { get; set; } 68 | [J("events_url")] public string EventsUrl { get; set; } 69 | [J("received_events_url")] public string ReceivedEventsUrl { get; set; } 70 | [J("type")] public string Type { get; set; } 71 | [J("site_admin")] public bool SiteAdmin { get; set; } 72 | } 73 | 74 | public partial class HeadCommit 75 | { 76 | [J("id")] public string Id { get; set; } 77 | [J("tree_id")] public string TreeId { get; set; } 78 | [J("message")] public string Message { get; set; } 79 | [J("timestamp")] public DateTimeOffset Timestamp { get; set; } 80 | [J("author")] public Author Author { get; set; } 81 | [J("committer")] public Author Committer { get; set; } 82 | } 83 | 84 | public partial class Author 85 | { 86 | [J("name")] public string Name { get; set; } 87 | [J("email")] public string Email { get; set; } 88 | } 89 | 90 | public partial class Repository 91 | { 92 | [J("id")] public long Id { get; set; } 93 | [J("node_id")] public string NodeId { get; set; } 94 | [J("name")] public string Name { get; set; } 95 | [J("full_name")] public string FullName { get; set; } 96 | [J("private")] public bool Private { get; set; } 97 | [J("owner")] public Actor Owner { get; set; } 98 | [J("html_url")] public string HtmlUrl { get; set; } 99 | [J("description")] public string Description { get; set; } 100 | [J("fork")] public bool Fork { get; set; } 101 | [J("url")] public string Url { get; set; } 102 | [J("forks_url")] public string ForksUrl { get; set; } 103 | [J("keys_url")] public string KeysUrl { get; set; } 104 | [J("collaborators_url")] public string CollaboratorsUrl { get; set; } 105 | [J("teams_url")] public string TeamsUrl { get; set; } 106 | [J("hooks_url")] public string HooksUrl { get; set; } 107 | [J("issue_events_url")] public string IssueEventsUrl { get; set; } 108 | [J("events_url")] public string EventsUrl { get; set; } 109 | [J("assignees_url")] public string AssigneesUrl { get; set; } 110 | [J("branches_url")] public string BranchesUrl { get; set; } 111 | [J("tags_url")] public string TagsUrl { get; set; } 112 | [J("blobs_url")] public string BlobsUrl { get; set; } 113 | [J("git_tags_url")] public string GitTagsUrl { get; set; } 114 | [J("git_refs_url")] public string GitRefsUrl { get; set; } 115 | [J("trees_url")] public string TreesUrl { get; set; } 116 | [J("statuses_url")] public string StatusesUrl { get; set; } 117 | [J("languages_url")] public string LanguagesUrl { get; set; } 118 | [J("stargazers_url")] public string StargazersUrl { get; set; } 119 | [J("contributors_url")] public string ContributorsUrl { get; set; } 120 | [J("subscribers_url")] public string SubscribersUrl { get; set; } 121 | [J("subscription_url")] public string SubscriptionUrl { get; set; } 122 | [J("commits_url")] public string CommitsUrl { get; set; } 123 | [J("git_commits_url")] public string GitCommitsUrl { get; set; } 124 | [J("comments_url")] public string CommentsUrl { get; set; } 125 | [J("issue_comment_url")] public string IssueCommentUrl { get; set; } 126 | [J("contents_url")] public string ContentsUrl { get; set; } 127 | [J("compare_url")] public string CompareUrl { get; set; } 128 | [J("merges_url")] public string MergesUrl { get; set; } 129 | [J("archive_url")] public string ArchiveUrl { get; set; } 130 | [J("downloads_url")] public string DownloadsUrl { get; set; } 131 | [J("issues_url")] public string IssuesUrl { get; set; } 132 | [J("pulls_url")] public string PullsUrl { get; set; } 133 | [J("milestones_url")] public string MilestonesUrl { get; set; } 134 | [J("notifications_url")] public string NotificationsUrl { get; set; } 135 | [J("labels_url")] public string LabelsUrl { get; set; } 136 | [J("releases_url")] public string ReleasesUrl { get; set; } 137 | [J("deployments_url")] public string DeploymentsUrl { get; set; } 138 | } 139 | 140 | 141 | } 142 | -------------------------------------------------------------------------------- /CFPABot/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.Hosting; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using CFPABot.Azusa; 12 | using CFPABot.Azusa.Pages; 13 | using CFPABot.Command; 14 | using CFPABot.Controllers; 15 | using CFPABot.IDK; 16 | using CFPABot.PRData; 17 | using CFPABot.ProjectHex; 18 | using CFPABot.Utils; 19 | using GammaLibrary.Extensions; 20 | using Microsoft.AspNetCore.Server.Kestrel.Core; 21 | using Octokit; 22 | using Serilog; 23 | using Serilog.Events; 24 | 25 | namespace CFPABot 26 | { 27 | public class Program 28 | { 29 | public static async Task Main(string[] args) 30 | { 31 | await TermManager.Init(); 32 | TaskScheduler.UnobservedTaskException += (sender, eventArgs) => 33 | { 34 | Log.Error(eventArgs.Exception, "UnobservedTaskException"); 35 | }; 36 | var cts = new CancellationTokenSource(); 37 | Console.CancelKeyPress += (sender, eventArgs) => 38 | { 39 | Wait(); 40 | cts.Cancel(); 41 | }; 42 | AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => 43 | { 44 | Wait(); 45 | cts.Cancel(); 46 | }; 47 | 48 | try 49 | { 50 | if (Directory.Exists("caches")) 51 | { 52 | Directory.Delete("caches", true); 53 | } 54 | } 55 | catch (Exception e) 56 | { 57 | Console.WriteLine(e); 58 | } 59 | 60 | if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development") 61 | { 62 | Environment.CurrentDirectory = "C:\\app"; 63 | } 64 | //GitHub.Init(); 65 | Directory.CreateDirectory("config"); 66 | Directory.CreateDirectory("wwwroot"); 67 | Directory.CreateDirectory("config/pr_context"); 68 | Directory.CreateDirectory("logs"); 69 | Directory.CreateDirectory("config/repo_analyze_results"); 70 | Directory.CreateDirectory("config/curse_files_cache"); 71 | Directory.CreateDirectory("config/pr_cache"); 72 | Directory.CreateDirectory("caches/"); 73 | Directory.CreateDirectory("caches/repos/"); 74 | Directory.CreateDirectory("project-hex"); 75 | _ = new GlobalGitRepoCache(); 76 | if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Development") 77 | _ = Task.Run(async () => 78 | { 79 | while (true) 80 | { 81 | await ModList.ModListCache.Refresh(); 82 | await RunProjectHex(); 83 | 84 | await Task.Delay(TimeSpan.FromMinutes(5)); 85 | } 86 | }); 87 | if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Development") 88 | { 89 | _ = Task.Run(async () => 90 | { 91 | while (true) 92 | { 93 | try 94 | { 95 | await CurseForgeIDMappingManager.Update(); 96 | } 97 | catch (Exception e) 98 | { 99 | Console.WriteLine($"Mapping Error: {e}"); 100 | } 101 | 102 | await Task.Delay(TimeSpan.FromMinutes(60)); 103 | } 104 | }); 105 | 106 | _ = Task.Run(async () => 107 | { 108 | while (true) 109 | { 110 | try 111 | { 112 | await TranslationRequestUpdater.Run(); 113 | } 114 | catch (Exception e) 115 | { 116 | Console.WriteLine($"#2702 Error: {e}"); 117 | } 118 | 119 | await Task.Delay(TimeSpan.FromMinutes(60)); 120 | } 121 | }); 122 | } 123 | 124 | 125 | Log.Logger = new LoggerConfiguration() 126 | .MinimumLevel.Debug() 127 | .WriteTo.Console() 128 | .WriteTo.File("logs/myapp.txt", rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Information) 129 | //.WriteTo.File("logs/myapp-debug.txt", rollingInterval: RollingInterval.Day) 130 | .CreateLogger(); 131 | await Init(); 132 | try 133 | { 134 | await CreateHostBuilder(args).Build().RunAsync(cts.Token); 135 | } 136 | catch (Exception e) 137 | { 138 | Console.WriteLine(e); 139 | } 140 | } 141 | 142 | static SemaphoreSlim projectHexLocker = new(1); 143 | public static async Task RunProjectHex(bool force = false) 144 | { 145 | if (!await projectHexLocker.WaitAsync(1000)) 146 | { 147 | return; 148 | } 149 | 150 | try 151 | { 152 | if (!Directory.GetFiles("project-hex").Any() || 153 | (DateTime.Now - ProjectHexConfig.Instance.LastTime).TotalDays > 0.25 || force) 154 | { 155 | try 156 | { 157 | await new ProjectHexRunner().Run(force); 158 | ProjectHexConfig.Instance.LastTime = DateTime.Now; 159 | ProjectHexConfig.Instance.DownloadsSinceLastPack = 0; 160 | ProjectHexConfig.Save(); 161 | } 162 | catch (Exception e) 163 | { 164 | Log.Error(e, "project-hex"); 165 | } 166 | } 167 | 168 | 169 | } 170 | finally 171 | { 172 | projectHexLocker.Release(); 173 | } 174 | } 175 | 176 | internal static bool ShuttingDown { get; private set; } 177 | 178 | static void Wait() 179 | { 180 | ShuttingDown = true; 181 | SpinWait.SpinUntil(() => MyWebhookEventProcessor.commentBuilders.All(c => !c.Value.IsAnyLockAcquired())); 182 | SpinWait.SpinUntil(() => CommandProcessor.CurrentRuns == 0); 183 | } 184 | 185 | static async Task Init() 186 | { 187 | if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Development") 188 | await PRDataManager.Init(); 189 | } 190 | 191 | public static IHostBuilder CreateHostBuilder(string[] args) => 192 | Host.CreateDefaultBuilder(args) 193 | .ConfigureWebHostDefaults(webBuilder => 194 | { 195 | webBuilder.ConfigureKestrel(x => 196 | x.ListenAnyIP(8080)); 197 | webBuilder.UseStartup(); 198 | }).ConfigureWebHost(x => 199 | { 200 | x.UseStaticWebAssets(); 201 | }); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /CFPABot/Properties/launchSettings.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "IIS Express": { 4 | "commandName": "IISExpress", 5 | "launchBrowser": true, 6 | "launchUrl": "weatherforecast", 7 | "environmentVariables": { 8 | "ASPNETCORE_ENVIRONMENT": "Development" 9 | } 10 | }, 11 | "CFPABot": { 12 | "commandName": "Project", 13 | "launchBrowser": false, 14 | "launchUrl": "Azusa/", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development", 17 | 18 | }, 19 | "dotnetRunMessages": "true", 20 | "applicationUrl": "http://localhost:5000" 21 | }, 22 | "Docker": { 23 | "commandName": "Docker", 24 | "launchBrowser": true, 25 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/weatherforecast", 26 | "publishAllPorts": true 27 | } 28 | }, 29 | "$schema": "http://json.schemastore.org/launchsettings.json", 30 | "iisSettings": { 31 | "windowsAuthentication": false, 32 | "anonymousAuthentication": true, 33 | "iisExpress": { 34 | "applicationUrl": "http://localhost:21650", 35 | "sslPort": 0 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /CFPABot/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | using BlazorStrap; 14 | using CFPABot.Controllers; 15 | using CFPABot.ProjectHex; 16 | using CFPABot.Utils; 17 | using MessagePack; 18 | using Microsoft.AspNetCore.Http; 19 | using Microsoft.Extensions.FileProviders; 20 | using Octokit.Webhooks; 21 | using Octokit.Webhooks.AspNetCore; 22 | 23 | namespace CFPABot 24 | { 25 | public class Startup 26 | { 27 | public Startup(IConfiguration configuration) 28 | { 29 | Configuration = configuration; 30 | } 31 | 32 | public IConfiguration Configuration { get; } 33 | 34 | // This method gets called by the runtime. Use this method to add services to the container. 35 | public void ConfigureServices(IServiceCollection services) 36 | { 37 | services.AddRazorPages(options => options.RootDirectory = "/Azusa/Pages"); 38 | services.AddServerSideBlazor(); 39 | services.AddControllers(); 40 | services.AddDirectoryBrowser(); 41 | services.AddHealthChecks(); 42 | services.AddBlazorStrap(); 43 | services.AddSignalR(e => { 44 | e.MaximumReceiveMessageSize = 102400000; 45 | 46 | }) 47 | .AddMessagePackProtocol(options => options.SerializerOptions = MessagePackSerializerOptions.Standard.WithCompression(MessagePackCompression.Lz4Block).WithSecurity(MessagePackSecurity.UntrustedData)); ; 48 | services.AddHttpContextAccessor(); 49 | services.AddSingleton(); 50 | } 51 | 52 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 53 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 54 | { 55 | if (env.IsDevelopment()) 56 | { 57 | app.UseDeveloperExceptionPage(); 58 | } 59 | app.UseHealthChecks("/healthcheck"); 60 | 61 | app.UseDirectoryBrowser(new DirectoryBrowserOptions() { RequestPath = "/project-hex", FileProvider = new PhysicalFileProvider("/app/project-hex") }); 62 | 63 | app.UseStaticFiles(new StaticFileOptions 64 | { 65 | RequestPath = "/static", 66 | FileProvider = new PhysicalFileProvider(Path.GetFullPath("wwwroot")), 67 | OnPrepareResponse = 68 | context => 69 | { 70 | } 71 | }); 72 | if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") != "Development") 73 | { 74 | app.UseStaticFiles(new StaticFileOptions 75 | { 76 | RequestPath = "/css", 77 | FileProvider = new PhysicalFileProvider(Path.GetFullPath("wwwrootx/css")), 78 | 79 | }); 80 | } 81 | 82 | app.UseStaticFiles(new StaticFileOptions() 83 | { 84 | RequestPath = "/project-hex", 85 | FileProvider = new PhysicalFileProvider("/app/project-hex"), 86 | ServeUnknownFileTypes = true, 87 | OnPrepareResponse = 88 | context => 89 | { 90 | lock (ProjectHexConfig.Instance) 91 | { 92 | ProjectHexConfig.Instance.DownloadsSinceLastPack++; 93 | ProjectHexConfig.Instance.TotalDownloads++; 94 | ProjectHexConfig.Instance.TotalDownloadGBs += context.File.Length / 1024.0 / 1024.0 / 1024.0; 95 | ProjectHexConfig.Save(); 96 | } 97 | Console.WriteLine($"Downloading {context.File.Name}"); 98 | } 99 | }); 100 | app.UseStaticFiles(); 101 | 102 | app.UseFileServer(new FileServerOptions 103 | { 104 | FileProvider = new ManifestEmbeddedFileProvider( 105 | typeof(Program).Assembly, "Azusa/wwwroot" 106 | ) 107 | }); 108 | app.UseFileServer(new FileServerOptions 109 | { 110 | FileProvider = new ManifestEmbeddedFileProvider( 111 | typeof(Program).Assembly, "Azusa/wwwroot" 112 | ), 113 | RequestPath = new PathString("/Azusa") 114 | }); 115 | app.UseFileServer(new FileServerOptions 116 | { 117 | FileProvider = new ManifestEmbeddedFileProvider( 118 | typeof(Program).Assembly, "Azusa/wwwroot2" 119 | ), 120 | RequestPath = new PathString("/Azusa") 121 | }); 122 | 123 | app.UseRouting(); 124 | 125 | app.UseAuthorization(); 126 | 127 | 128 | 129 | app.UseEndpoints(endpoints => 130 | { 131 | endpoints.MapGitHubWebhooks("api/WebhookListener", Constants.GitHubWebhookSecret); 132 | endpoints.MapControllers(); 133 | endpoints.MapBlazorHub("/Azusa/_blazor"); 134 | endpoints.MapFallbackToPage("/_Host"); 135 | }); 136 | 137 | 138 | 139 | 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /CFPABot/Utils/Constants.cs: -------------------------------------------------------------------------------- 1 | //#define Test 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | 8 | namespace CFPABot.Utils 9 | { 10 | public static class Constants 11 | { 12 | #if Test 13 | 14 | public const string BaseRepo = "https://github.com/Cyl18/Test"; 15 | public const string Owner = "Cyl18"; 16 | public const string RepoName = "Test"; 17 | 18 | public static string GitHubOAuthToken => Environment.GetEnvironmentVariable("GITHUB_OAUTH_TOKEN"); 19 | public static string GitHubWebhookSecret => Environment.GetEnvironmentVariable("GITHUB_WEBHOOK_SECRET"); 20 | #else 21 | public const string BaseRepoUrl = "https://github.com/CFPAOrg/Minecraft-Mod-Language-Package"; 22 | public const string Owner = "CFPAOrg"; 23 | public const string RepoName = "Minecraft-Mod-Language-Package"; 24 | public const int RepoID = 88008282; 25 | 26 | public const string PRPackerFileName = "pr-packer.yml"; 27 | 28 | public const string GitHubOAuthClientId = "20f9e79dfa770f38e95d"; 29 | public const string GitHubOAuthTokenCookieName = "oauth-token-enc"; 30 | public static string GitHubOAuthToken => Environment.GetEnvironmentVariable("GITHUB_OAUTH_TOKEN"); 31 | public static string GitHubWebhookSecret => Environment.GetEnvironmentVariable("GITHUB_WEBHOOK_SECRET"); 32 | public static string CurseForgeApiKey => Environment.GetEnvironmentVariable("CURSEFORGE_API_KEY"); 33 | public static string ChatGptApiKey => Environment.GetEnvironmentVariable("CHATGPT_API_KEY"); 34 | #endif 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CFPABot/Utils/Downloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Collections.Specialized; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Net.WebSockets; 10 | using System.Numerics; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using CFPABot.Command; 14 | using CFPABot.Controllers; 15 | using GammaLibrary.Extensions; 16 | using Serilog; 17 | 18 | namespace CFPABot.Utils 19 | { 20 | public class Download 21 | { 22 | public static HttpClient hc; 23 | public static HttpClient chc; 24 | 25 | static Download() 26 | { 27 | hc = new(); 28 | chc = new(); 29 | hc.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"); 30 | } 31 | 32 | public static async Task GitHubAPIJson(string url) 33 | { 34 | var jsonhc = new HttpClient(); 35 | jsonhc.DefaultRequestHeaders.Add("User-Agent", "cfpa-bot"); 36 | jsonhc.DefaultRequestHeaders.Add("Authorization", $"bearer {GitHub.GetToken()}"); 37 | return (await jsonhc.GetStringAsync(url)).JsonDeserialize(); 38 | } 39 | 40 | public static async Task String(string url, bool withToken = false) 41 | { 42 | // xswl 43 | try 44 | { 45 | Log.Debug($"网络请求:{url}"); 46 | if (!withToken) 47 | { 48 | return await hc.GetStringAsync(url); 49 | } 50 | else 51 | { 52 | var hc1 = new HttpClient(); 53 | hc1.DefaultRequestHeaders.Add("User-Agent", "cfpa-bot"); 54 | hc1.DefaultRequestHeaders.Add("Authorization", $"bearer {GitHub.GetToken()}"); 55 | return (await hc1.GetStringAsync(url)); 56 | } 57 | } 58 | catch (HttpRequestException e1) 59 | { 60 | try 61 | { 62 | if (e1.StatusCode == HttpStatusCode.TooManyRequests) 63 | { 64 | Log.Warning(e1, $"HTTP 429: {url}"); 65 | await Task.Delay(TimeSpan.FromSeconds(15)); 66 | } 67 | await Task.Delay(500); 68 | return await hc.GetStringAsync(url); 69 | } 70 | catch (HttpRequestException e2) 71 | { 72 | if (e2.StatusCode == HttpStatusCode.TooManyRequests) 73 | { 74 | await Task.Delay(TimeSpan.FromSeconds(60)); 75 | } 76 | await Task.Delay(500); 77 | return await hc.GetStringAsync(url); 78 | } 79 | } 80 | } 81 | 82 | static Dictionary locks = new(); 83 | static async ValueTask AcquireLock(string lockName) 84 | { 85 | Log.Debug($"正在获取锁 {lockName}..."); 86 | SuperUniversalExtremeAwesomeGodlikeSmartLock l; 87 | lock (locks) 88 | { 89 | if (!locks.ContainsKey(lockName)) locks[lockName] = new SuperUniversalExtremeAwesomeGodlikeSmartLock(); 90 | l = locks[lockName]; 91 | } 92 | await l.WaitAsync(); 93 | return l; 94 | } 95 | public static async Task DownloadFile(string url) 96 | { 97 | Log.Debug($"文件下载:{url}"); 98 | Directory.CreateDirectory("temp"); 99 | if (url.Contains("+")) url = url.Replace("https://edge.forgecdn.net", "https://mediafilez.forgecdn.net"); 100 | 101 | var fileName = $"{url.Split("/").Last()}"; 102 | 103 | using var l = await AcquireLock($"download {fileName}"); 104 | 105 | if (File.Exists($"temp/{fileName}")) 106 | { 107 | lastAccessTime[fileName] = DateTime.Now; 108 | return $"temp/{fileName}"; 109 | } 110 | await using var fs = File.OpenWrite($"temp/{fileName}"); 111 | await using var stream = await hc.GetStreamAsync(url); 112 | await stream.CopyToAsync(fs); 113 | Task.Run(async () => 114 | { 115 | await Task.Delay(TimeSpan.FromDays(1)); 116 | if (lastAccessTime.TryGetValue(fileName, out var time) && DateTime.Now - time < TimeSpan.FromDays(0.5)) return; 117 | 118 | var l = await AcquireLock($"download {fileName}"); 119 | 120 | while (!SpinWait.SpinUntil(() => 121 | MyWebhookEventProcessor.commentBuilders.All(c => !c.Value.IsAnyLockAcquired()), 100) || 122 | !SpinWait.SpinUntil(() => CommandProcessor.CurrentRuns == 0, 100)) 123 | { 124 | l.Dispose(); 125 | await Task.Delay(1000); 126 | l = await AcquireLock($"download {fileName}"); 127 | } 128 | File.Delete($"temp/{fileName}"); 129 | lastAccessTime.TryRemove(fileName, out _); 130 | l.Dispose(); 131 | 132 | }); 133 | return $"temp/{fileName}"; 134 | } 135 | 136 | private static ConcurrentDictionary lastAccessTime = new(); 137 | public static async Task LinkExists(string link) 138 | { 139 | try 140 | { 141 | var message = await hc.SendAsync(new HttpRequestMessage(HttpMethod.Head, link)).ConfigureAwait(false); 142 | message.EnsureSuccessStatusCode(); 143 | return true; 144 | } 145 | catch (Exception) 146 | { 147 | return false; 148 | } 149 | } 150 | public static async Task CurseForgeString(string url) 151 | { 152 | // 好像反正 CurseForge API 给了 UserAgent 就要 403 153 | Log.Debug($"网络请求:{url}"); 154 | return await chc.GetStringAsync(url); 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /CFPABot/Utils/ExFormatter.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Formatting.Json; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System; 6 | using CFPABot.DiffEngine; 7 | using CFPABot.Exceptions; 8 | using GammaLibrary.Extensions; 9 | 10 | namespace CFPABot.Utils 11 | { 12 | public class ExFormatter 13 | { 14 | public static string Format(string content, LangFileType? type = null) 15 | { 16 | type ??= LangFileWrapper.GuessType(content); 17 | switch (type) 18 | { 19 | case LangFileType.Lang: 20 | return RemoveEmptyLines(content.Split('\n').Select(x => x.TrimStart(' ')).Connect("\n")); 21 | break; 22 | case LangFileType.Json: 23 | return JsonHelper.FormatJson(content); 24 | break; 25 | } 26 | 27 | throw new CommandException("ExFormatter 遇到了不可能出现的情况???"); 28 | } 29 | 30 | static string RemoveEmptyLines(string s) 31 | { 32 | var list = s.Split('\n').ToList(); 33 | if (list.Count <= 2) return s; 34 | 35 | var flag = false; 36 | var lineCount = list.Count; 37 | for (var i = 0; i < lineCount; i++) 38 | { 39 | var line = list[i]; 40 | 41 | if (line.IsNullOrWhiteSpace()) 42 | { 43 | if (flag) 44 | { 45 | list.RemoveAt(i); 46 | lineCount--; 47 | } 48 | else 49 | { 50 | flag = true; 51 | } 52 | } 53 | else 54 | { 55 | flag = false; 56 | } 57 | } 58 | 59 | return list.Connect("\n"); 60 | } 61 | } 62 | 63 | 64 | // https://stackoverflow.com/questions/4580397/json-formatter-in-c 65 | class JsonHelper 66 | { 67 | private const string INDENT_STRING = " "; 68 | public static string FormatJson(string str) 69 | { 70 | var indent = 0; 71 | var quoted = false; 72 | var sb = new StringBuilder(); 73 | for (var i = 0; i < str.Length; i++) 74 | { 75 | var ch = str[i]; 76 | switch (ch) 77 | { 78 | case '{': 79 | case '[': 80 | sb.Append(ch); 81 | if (!quoted) 82 | { 83 | sb.AppendLine(); 84 | Enumerable.Range(0, ++indent).ForEach(item => sb.Append(INDENT_STRING)); 85 | } 86 | break; 87 | case '\r': continue; 88 | case '\n': 89 | if (sb.ToString().Last() == ',') 90 | { 91 | sb.AppendLine(); 92 | } 93 | 94 | break; 95 | case '}': 96 | case ']': 97 | if (!quoted) 98 | { 99 | sb.AppendLine(); 100 | Enumerable.Range(0, --indent).ForEach(item => sb.Append(INDENT_STRING)); 101 | } 102 | sb.Append(ch); 103 | break; 104 | case '"': 105 | sb.Append(ch); 106 | bool escaped = false; 107 | var index = i; 108 | while (index > 0 && str[--index] == '\\') 109 | escaped = !escaped; 110 | if (!escaped) 111 | quoted = !quoted; 112 | break; 113 | case ',': 114 | sb.Append(ch); 115 | if (!quoted) 116 | { 117 | sb.AppendLine(); 118 | Enumerable.Range(0, indent).ForEach(item => sb.Append(INDENT_STRING)); 119 | } 120 | break; 121 | case ':': 122 | sb.Append(ch); 123 | if (!quoted) 124 | sb.Append(" "); 125 | break; 126 | default: 127 | sb.Append(ch); 128 | break; 129 | } 130 | } 131 | return sb.ToString(); 132 | } 133 | } 134 | 135 | static class Extensions 136 | { 137 | public static void ForEach(this IEnumerable ie, Action action) 138 | { 139 | foreach (var i in ie) 140 | { 141 | action(i); 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /CFPABot/Utils/FileUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace CFPABot.Utils 8 | { 9 | public static class FileUtils 10 | { 11 | public static FileStream OpenFile(string path) 12 | { 13 | return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CFPABot/Utils/GlobalGitRepoCache.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System; 3 | using CFPABot.Exceptions; 4 | using System.Diagnostics; 5 | using System.Threading.Tasks; 6 | using Serilog; 7 | 8 | namespace CFPABot.Utils 9 | { 10 | public class GlobalGitRepoCache 11 | { 12 | 13 | public string WorkingDirectory { get; } 14 | 15 | public GlobalGitRepoCache() 16 | { 17 | WorkingDirectory = $"/app/repo-cache"; 18 | Directory.CreateDirectory(WorkingDirectory); 19 | Clone(); 20 | } 21 | 22 | public Task Clone() 23 | { 24 | return Task.Run(() => 25 | { 26 | if (!File.Exists(WorkingDirectory + "/README.md")) 27 | { 28 | Run( 29 | $"clone https://x-access-token:{GitHub.GetToken()}@github.com/{Constants.Owner}/{Constants.RepoName}.git ."); 30 | } 31 | }); 32 | } 33 | 34 | 35 | public void Run(string args) 36 | { 37 | var process = Process.Start(new ProcessStartInfo("git", args) { RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = WorkingDirectory }); 38 | var stdout = ""; 39 | var stderr = ""; 40 | process.OutputDataReceived += (sender, eventArgs) => { stdout += eventArgs.Data; }; 41 | process.ErrorDataReceived += (sender, eventArgs) => { stderr += eventArgs.Data; }; 42 | process.BeginOutputReadLine(); 43 | process.BeginErrorReadLine(); 44 | process.WaitForExit(); 45 | if (process.ExitCode != 0) 46 | { 47 | // haha 48 | // https://github.com/Cyl18/CFPABot/issues/3 49 | // maybe Regex.Replace(message, "ghs_[0-9a-zA-Z]{36}", "******") 50 | Log.Error($"git.exe {args} exited with {process.ExitCode} - {stdout}{stderr}"); 51 | throw new ProcessException($"git.exe with args `{args}` exited with {process.ExitCode}."); 52 | } 53 | 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CFPABot/Utils/KeyAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using CFPABot.Resources; 8 | using GammaLibrary.Extensions; 9 | using Octokit; 10 | using Serilog; 11 | 12 | namespace CFPABot.Utils 13 | { 14 | public class KeyAnalyzer 15 | { 16 | public static bool Analyze(string modid, string enfile, string cnfile, MCVersion version, 17 | StringBuilder messageStringBuilder, StringBuilder reportStringBuilder) 18 | { 19 | return version switch 20 | { 21 | MCVersion.v1122 => LangChecker(modid, enfile, cnfile, messageStringBuilder, reportStringBuilder, version), 22 | _ => JsonChecker(modid, enfile, cnfile, messageStringBuilder, reportStringBuilder, version) 23 | }; 24 | } 25 | 26 | static bool JsonChecker(string modid, string enfile, string cnfile, StringBuilder sb, 27 | StringBuilder reportStringBuilder, MCVersion mcVersion) 28 | { 29 | try 30 | { 31 | JsonDocument en, cn; 32 | try 33 | { 34 | en = JsonDocument.Parse(enfile, new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip }); 35 | cn = JsonDocument.Parse(cnfile, new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip }); 36 | } 37 | catch (Exception e) 38 | { 39 | sb.AppendLine(string.Format(Locale.Check_Key_JsonSyntaxError, modid, mcVersion.ToVersionString(), e.Message)); 40 | reportStringBuilder.AppendLine($"{e}"); 41 | return true; 42 | } 43 | 44 | if (!en.RootElement.EnumerateObject().Any()) 45 | { 46 | sb.AppendLine(string.Format(Locale.Check_Key_JsonEnFileEmpty, modid, mcVersion.ToVersionString())); 47 | return true; 48 | } 49 | 50 | if (!cn.RootElement.EnumerateObject().Any()) 51 | { 52 | sb.AppendLine(string.Format(Locale.Check_Key_JsonCnFileEmpty, modid, mcVersion.ToVersionString())); 53 | return true; 54 | } 55 | 56 | var ens = en.RootElement.EnumerateObject().Select(p => p.Name).Where(k => !k.StartsWith("_")).ToHashSet(); 57 | var cns = cn.RootElement.EnumerateObject().Select(p => p.Name).Where(k => !k.StartsWith("_")).ToHashSet(); 58 | return AnalyzeCore(ens, cns, modid, sb, reportStringBuilder, mcVersion, enfile, cnfile); 59 | } 60 | catch (Exception e) 61 | { 62 | sb.AppendLine(string.Format(Locale.Check_Key_Error, modid, mcVersion.ToVersionString(), e.Message)); 63 | reportStringBuilder.AppendLine($"语言文件检查失败: {e}"); 64 | return false; 65 | } 66 | } 67 | 68 | 69 | static bool LangChecker(string modid, string enfile, string cnfile, StringBuilder sb, 70 | StringBuilder reportSb, MCVersion version) 71 | { 72 | if (enfile.IsNullOrWhiteSpace()) 73 | { 74 | sb.AppendLine(string.Format(Locale.Check_Key_LangEnFileEmpty, modid, version.ToVersionString())); 75 | return false; 76 | } 77 | if (cnfile.IsNullOrWhiteSpace()) 78 | { 79 | sb.AppendLine(string.Format(Locale.Check_Key_LangCnFileEmpty, modid, version.ToVersionString())); 80 | return false; 81 | } 82 | 83 | return AnalyzeCore( 84 | enfile 85 | .Split("\n") 86 | .Select(line => line.Trim()) // Trim 87 | .Where(line => !line.IsNullOrWhiteSpace()) // 移除空行 88 | .Where(line => !line.StartsWith("#")) // 移除注释 89 | .Where(line => line.Contains("=")) // 保证有等号 90 | .Select(line => line.Split("=").First()) // 提取 Key 91 | .ToHashSet(), 92 | cnfile 93 | .Split("\n") 94 | .Select(line => line.Trim()) // Trim 95 | .Where(line => !line.IsNullOrWhiteSpace()) // 移除空行 96 | .Where(line => !line.StartsWith("#")) // 移除注释 97 | .Where(line => line.Contains("=")) // 保证有等号 98 | .Select(line => line.Split("=").First()) // 提取 Key 99 | .ToHashSet(), 100 | modid, sb, reportSb, version, enfile, cnfile 101 | ); 102 | } 103 | 104 | static bool AnalyzeCore(HashSet enKeys, HashSet cnKeys, string modid, StringBuilder sb, 105 | StringBuilder reportSb, MCVersion mcVersion, string enfile, string cnfile) 106 | { 107 | reportSb.AppendLine($"{modid}-{mcVersion.ToVersionString()} 中文语言文件共有 {cnKeys.Count} 个 Key; 英文语言文件共有 {enKeys.Count} 个 Key"); 108 | var enExcept = new HashSet(enKeys); // en 比 cn 多的 109 | enExcept.ExceptWith(cnKeys); 110 | var cnExcept = new HashSet(cnKeys); // cn 比 en 多的 111 | cnExcept.ExceptWith(enKeys); 112 | if (enExcept.Count == 0 && cnExcept.Count == 0) 113 | { 114 | sb.AppendLine(string.Format(Locale.Check_Key_Success, modid, mcVersion.ToVersionString())); 115 | return false; 116 | } 117 | 118 | sb.AppendLine(string.Format(Locale.Check_Key_NotCorrespond, modid, mcVersion.ToVersionString())); 119 | 120 | if (enExcept.Count > 0) 121 | { 122 | sb.AppendLine($"- 英文语言文件有 {enExcept.Count} 个 Key 多于中文语言文件。例如:\n{enExcept.Take(4).Select(f => $" - 行 {(enfile.Split('\n').ToList().FindIndex(l => l.Contains(f)) + 1)}-`{f}`").Connect("\n")}"); 123 | reportSb.AppendLine($"英文多于中文的 Key: \n{enExcept.Select(k => $" {k}\n").Connect("")}"); 124 | } 125 | 126 | if (cnExcept.Count > 0) 127 | { 128 | sb.AppendLine($"- 中文语言文件有 {cnExcept.Count} 个 Key 多于英文语言文件。例如:\n{cnExcept.Take(4).Select(f => $" - 行 {(cnfile.Split('\n').ToList().FindIndex(l => l.Contains(f)) + 1)}-`{f}`").Connect("\n")}"); 129 | reportSb.AppendLine($"中文多于英文的 Key: \n{cnExcept.Select(k => $" {k}\n").Connect("")}"); 130 | } 131 | 132 | reportSb.AppendLine(); 133 | sb.AppendLine(); 134 | return true; 135 | } 136 | 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /CFPABot/Utils/MailUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Net.Http; 5 | using System.Net.Mail; 6 | using System.Threading.Tasks; 7 | 8 | namespace CFPABot.Utils 9 | { 10 | public class MailUtils 11 | { 12 | static HttpClient hc = new(); 13 | public static async Task SendNotification(string mailAddress, string prUrl) 14 | { 15 | await hc.PostAsync("https://cfpa-home.cyan.cafe:2/api/Mail/SendMail", new FormUrlEncodedContent(new KeyValuePair[] 16 | { 17 | new KeyValuePair("password", Constants.GitHubWebhookSecret), 18 | new KeyValuePair("mailAddress", mailAddress), 19 | new KeyValuePair("prUrl", prUrl) 20 | })); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CFPABot/Utils/ModKeyAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using CFPABot.Resources; 8 | using GammaLibrary.Extensions; 9 | 10 | namespace CFPABot.Utils 11 | { 12 | public class ModInfoForCheck 13 | { 14 | public ModInfoForCheck(string modid, MCVersion version, string downloadModName, string curseForgeSlug) 15 | { 16 | Modid = modid; 17 | Version = version; 18 | DownloadModName = downloadModName; 19 | CurseForgeSlug = curseForgeSlug; 20 | } 21 | 22 | public string Modid { get; set; } 23 | public MCVersion Version { get; set; } 24 | public string DownloadModName { get; set; } 25 | public string CurseForgeSlug { get; set; } 26 | } 27 | 28 | public class ModKeyAnalyzer 29 | { 30 | public static bool Analyze(ModInfoForCheck modInfoForCheck, string enfile, string cnfile, 31 | StringBuilder messageStringBuilder, StringBuilder reportStringBuilder) 32 | { 33 | return modInfoForCheck.Version switch 34 | { 35 | MCVersion.v1122 => LangChecker(modInfoForCheck, enfile, cnfile, messageStringBuilder, reportStringBuilder), 36 | _ => JsonChecker(modInfoForCheck, enfile, cnfile, messageStringBuilder, reportStringBuilder) 37 | }; 38 | } 39 | 40 | static bool JsonChecker(ModInfoForCheck modInfoForCheck, string enfile, string cnfile, StringBuilder sb, 41 | StringBuilder reportStringBuilder) 42 | { 43 | try 44 | { 45 | JsonDocument en, cn; 46 | try 47 | { 48 | en = JsonDocument.Parse(enfile, new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); 49 | cn = JsonDocument.Parse(cnfile, new JsonDocumentOptions() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); 50 | } 51 | catch (Exception e) 52 | { 53 | sb.AppendLine(string.Format(Locale.Check_ModKey_JsonSyntaxError, modInfoForCheck.Modid, modInfoForCheck.Version.ToVersionString(), e.Message)); 54 | reportStringBuilder.AppendLine($"{e}"); 55 | return true; 56 | } 57 | 58 | if (!en.RootElement.EnumerateObject().Any()) 59 | { 60 | return true; 61 | } 62 | 63 | if (!cn.RootElement.EnumerateObject().Any()) 64 | { 65 | return true; 66 | } 67 | 68 | var ens = en.RootElement.EnumerateObject().Select(p => p.Name).Where(k => !k.StartsWith("_")).ToHashSet(); 69 | var cns = cn.RootElement.EnumerateObject().Select(p => p.Name).Where(k => !k.StartsWith("_")).ToHashSet(); 70 | return AnalyzeCore(modInfoForCheck, ens, cns, sb, reportStringBuilder, enfile, cnfile); 71 | } 72 | catch (Exception e) 73 | { 74 | sb.AppendLine(string.Format(Locale.Check_ModKey_Error, modInfoForCheck.Modid, modInfoForCheck.Version.ToVersionString(), e.Message)); 75 | reportStringBuilder.AppendLine($"语言文件检查失败: {e}"); 76 | return false; 77 | } 78 | } 79 | 80 | 81 | static bool LangChecker(ModInfoForCheck modInfoForCheck, string enfile, string cnfile, StringBuilder sb, 82 | StringBuilder reportSb) 83 | { 84 | if (enfile.IsNullOrWhiteSpace()) 85 | { 86 | return false; 87 | } 88 | if (cnfile.IsNullOrWhiteSpace()) 89 | { 90 | return false; 91 | } 92 | 93 | return AnalyzeCore(modInfoForCheck, 94 | enfile 95 | .Split("\n") 96 | .Select(line => line.Trim()) // Trim 97 | .Where(line => !line.IsNullOrWhiteSpace()) // 移除空行 98 | .Where(line => !line.StartsWith("#")) // 移除注释 99 | .Where(line => line.Contains("=")) // 保证有等号 100 | .Select(line => line.Split("=").First()) // 提取 Key 101 | .ToHashSet(), 102 | cnfile 103 | .Split("\n") 104 | .Select(line => line.Trim()) // Trim 105 | .Where(line => !line.IsNullOrWhiteSpace()) // 移除空行 106 | .Where(line => !line.StartsWith("#")) // 移除注释 107 | .Where(line => line.Contains("=")) // 保证有等号 108 | .Select(line => line.Split("=").First()) // 提取 Key 109 | .ToHashSet(), 110 | sb, reportSb, enfile, cnfile 111 | ); 112 | } 113 | 114 | static bool AnalyzeCore(ModInfoForCheck modInfoForCheck, HashSet enKeys, HashSet cnKeys, StringBuilder sb, 115 | StringBuilder reportSb, string enfile, string cnfile) 116 | { 117 | reportSb.AppendLine($"{modInfoForCheck.Modid}-{modInfoForCheck.Version.ToVersionString()} 模组内语言文件共有 {cnKeys.Count} 个 Key;"); 118 | var enExcept = new HashSet(enKeys); // en 比 cn 多的 119 | enExcept.ExceptWith(cnKeys); 120 | var cnExcept = new HashSet(cnKeys); // cn 比 en 多的 121 | cnExcept.ExceptWith(enKeys); 122 | if (enExcept.Count == 0 && cnExcept.Count == 0) 123 | { 124 | sb.AppendLine(string.Format(Locale.Check_ModKey_Success, modInfoForCheck.Modid, modInfoForCheck.Version.ToVersionString())); 125 | return false; 126 | } 127 | 128 | // 这可能是由于机器人自动获取的模组不是最新,语言文件中包含扩展模组,或所提交的语言文件来自模组源代码仓库。可以点击上方的对比按钮来进行更加详细的对比。 129 | sb.AppendLine(string.Format(Locale.Check_ModKey_NotCorrespond, modInfoForCheck.Modid, modInfoForCheck.Version.ToVersionString(), modInfoForCheck.DownloadModName, modInfoForCheck.CurseForgeSlug, modInfoForCheck.Version.ToVersionString())); 130 | 131 | if (enExcept.Count > 0) 132 | { 133 | sb.AppendLine($"- 英文语言文件有 {enExcept.Count} 个 Key 多于模组内语言文件。例如:\n{enExcept.Take(4).Select(f => $" - 行 {(enfile.Split('\n').ToList().FindIndex(l => l.Contains(f)) + 1)}-`{f}`").Connect("\n")}"); 134 | reportSb.AppendLine($"英文多于模组内的 Key: \n{enExcept.Select(k => $" {k}\n").Connect("")}"); 135 | } 136 | 137 | if (cnExcept.Count > 0) 138 | { 139 | sb.AppendLine($"- 模组内语言文件有 {cnExcept.Count} 个 Key 多于英文语言文件。例如:\n{cnExcept.Take(4).Select(f => $" - 行 {(cnfile.Split('\n').ToList().FindIndex(l => l.Contains(f)) + 1)}-`{f}`").Connect("\n")}"); 140 | reportSb.AppendLine($"模组内多于英文的 Key: \n{cnExcept.Select(k => $" {k}\n").Connect("")}"); 141 | } 142 | 143 | reportSb.AppendLine(); 144 | sb.AppendLine(); 145 | return true; 146 | } 147 | 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /CFPABot/Utils/Models/GitHubPRReviewModel.cs: -------------------------------------------------------------------------------- 1 | namespace CFPABot.Utils.Models 2 | { 3 | using System; 4 | using System.Globalization; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; 8 | using N = System.Text.Json.Serialization.JsonIgnoreCondition; 9 | 10 | public partial class Line 11 | { 12 | [J("data")] public GitHubPRReviewData Data { get; set; } 13 | } 14 | public partial class GitHubPRReviewData 15 | { 16 | [J("repository")] public Repository Repository { get; set; } 17 | } 18 | 19 | public partial class Repository 20 | { 21 | [J("pullRequest")] public PullRequest1 PullRequest { get; set; } 22 | } 23 | 24 | public partial class PullRequest1 25 | { 26 | [J("reviewThreads")] public ReviewThreads ReviewThreads { get; set; } 27 | } 28 | 29 | public partial class ReviewThreads 30 | { 31 | [J("edges")] public ReviewThreadsEdge[] Edges { get; set; } 32 | } 33 | 34 | public partial class ReviewThreadsEdge 35 | { 36 | [J("node")] public PurpleNode Node { get; set; } 37 | } 38 | 39 | public partial class PurpleNode 40 | { 41 | [J("isOutdated")] public bool IsOutdated { get; set; } 42 | [J("isResolved")] public bool IsResolved { get; set; } 43 | [J("comments")] public Comments Comments { get; set; } 44 | } 45 | 46 | public partial class Comments 47 | { 48 | [J("edges")] public CommentsEdge[] Edges { get; set; } 49 | } 50 | 51 | public partial class CommentsEdge 52 | { 53 | [J("node")] public FluffyNode Node { get; set; } 54 | } 55 | 56 | public partial class FluffyNode 57 | { 58 | [J("fullDatabaseId")][JsonConverter(typeof(ParseStringConverter))] public long FullDatabaseId { get; set; } 59 | } 60 | 61 | public partial class Line 62 | { 63 | public static Line FromJson(string json) => JsonSerializer.Deserialize(json); 64 | } 65 | 66 | internal class ParseStringConverter : JsonConverter 67 | { 68 | public override bool CanConvert(Type t) => t == typeof(long); 69 | 70 | public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 71 | { 72 | var value = reader.GetString(); 73 | long l; 74 | if (Int64.TryParse(value, out l)) 75 | { 76 | return l; 77 | } 78 | throw new Exception("Cannot unmarshal type long"); 79 | } 80 | 81 | public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) 82 | { 83 | JsonSerializer.Serialize(writer, value.ToString(), options); 84 | return; 85 | } 86 | 87 | public static readonly ParseStringConverter Singleton = new ParseStringConverter(); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /CFPABot/Utils/ModrinthManager.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO.Compression; 3 | using System.IO; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using GammaLibrary.Extensions; 8 | using Modrinth; 9 | using Modrinth.Models; 10 | using Serilog; 11 | 12 | namespace CFPABot.Utils 13 | { 14 | public class ModrinthManager 15 | { 16 | public static ModrinthClient Instance = new ModrinthClient(); 17 | 18 | public static Task GetMod(string slug) 19 | { 20 | 21 | return Instance.Project.GetAsync(slug); 22 | } 23 | 24 | public static async Task GetModID(Project addon, MCVersion? version, bool enforcedLang = false, 25 | bool connect = true) 26 | { 27 | if (version == null) return "未知"; 28 | try 29 | { 30 | var versions = await Instance.Version.GetProjectVersionListAsync(addon.Slug, new []{ version.ToString().Contains("fabric") ? "fabric": "forge"}); 31 | if (versions.FirstOrDefault(f => f.GameVersions.Any(x=> x.StartsWith(version.Value.ToStandardVersionString()))) is { } file) 32 | { 33 | var fileName = await Download.DownloadFile(file.Files.First().Url); // 我好累 34 | await using var fs = FileUtils.OpenFile(fileName); 35 | 36 | var modids = new ZipArchive(fs).Entries 37 | .Where(a => a.FullName.StartsWith("assets")) 38 | .Where(a => !enforcedLang || a.FullName.Contains("lang")) 39 | .Select(a => a.FullName.Split("/")[1]).Distinct().Where(n => !n.IsNullOrWhiteSpace()) 40 | .Where(id => id != "minecraft" && id != "icon.png"); 41 | 42 | return connect ? modids 43 | .Connect(" \\| ", "*", "*") : modids.First(); 44 | } 45 | } 46 | catch (Exception e) 47 | { 48 | Log.Error(e, "GetModID 出错"); 49 | return $"未知"; 50 | } 51 | return "未知"; 52 | } 53 | 54 | public static async Task GetModIDForCheck(Project addon, MCVersion? version) 55 | { 56 | if (version == null) return null; 57 | try 58 | { 59 | var versions = await Instance.Version.GetProjectVersionListAsync(addon.Slug, new []{ version.ToString().Contains("fabric") ? "fabric": "forge"}); 60 | if (versions.Where(x => x.GameVersions.Any(y => y.StartsWith(version.Value.ToStandardVersionString()))).FirstOrDefault() is {} file) 61 | { 62 | var fileName = await Download.DownloadFile(file.Files.Any(x => x.FileName.ToLower().Contains("fabric") && version.ToString().Contains("fabric"))?file.Files.First(x => x.FileName.ToLower().Contains("fabric")).Url: file.Files.First().Url); 63 | await using var fs = FileUtils.OpenFile(fileName); 64 | 65 | var modids = new ZipArchive(fs).Entries 66 | .Where(a => a.FullName.StartsWith("assets")) 67 | .Select(a => a.FullName.Split("/")[1]).Distinct().Where(n => !n.IsNullOrWhiteSpace()) 68 | .Where(id => id != "minecraft") 69 | .ToArray(); 70 | return modids; 71 | } 72 | } 73 | catch (Exception e) 74 | { 75 | Log.Error(e, "GetModID 出错"); 76 | return null; 77 | } 78 | 79 | return null; 80 | } 81 | 82 | public static async Task<(string[] files, string downloadFileName)> GetModEnFile(Project addon, MCVersion? version, LangType type) 83 | { 84 | if (version == null) return (null, null); 85 | try 86 | { 87 | var versions = await Instance.Version.GetProjectVersionListAsync(addon.Slug, new[] { version.ToString().Contains("fabric") ? "fabric" : "forge" }); 88 | if (versions.Where(x => x.GameVersions.Any(y => y.StartsWith(version.Value.ToStandardVersionString()))).FirstOrDefault() is { } file) 89 | { 90 | var d = file.Files.Any(x => x.FileName.ToLower().Contains("fabric") && version.ToString().Contains("fabric")) ? file.Files.First(x => x.FileName.ToLower().Contains("fabric")) : file.Files.First(); 91 | var downloadUrl = d.Url; 92 | var (fs, files) = await GetModLangFiles(downloadUrl, type, version == MCVersion.v1122 ? LangFileType.Lang : LangFileType.Json); 93 | 94 | await using (fs) 95 | { 96 | return (files.Select(f => f.Open().ReadToEnd()).ToArray(), d.FileName); 97 | } 98 | } 99 | } 100 | catch (Exception e) 101 | { 102 | Log.Error(e, "GetModID 出错"); 103 | return (null, null); 104 | } 105 | 106 | return (null, null); 107 | } 108 | 109 | public static async Task<(Stream, IEnumerable)> GetModLangFiles(string downloadUrl, LangType type, LangFileType fileType) 110 | { 111 | var fileName = await Download.DownloadFile(downloadUrl); 112 | var fs = FileUtils.OpenFile(fileName); 113 | 114 | return (fs, GetModLangFilesFromStream(fs, type, fileType)); 115 | } 116 | 117 | public static IEnumerable GetModLangFilesFromStream(Stream fs, LangType type, LangFileType fileType) 118 | { 119 | var files = new ZipArchive(fs).Entries 120 | .Where(f => f.FullName.Contains("lang") && f.FullName.Contains("assets") && 121 | f.FullName.Split('/').All(n => n != "minecraft") && 122 | type == LangType.EN 123 | ? (f.Name.Equals("en_us.lang", StringComparison.OrdinalIgnoreCase) || 124 | f.Name.Equals("en_us.json", StringComparison.OrdinalIgnoreCase)) 125 | : (f.Name.Equals("zh_cn.lang", StringComparison.OrdinalIgnoreCase) || 126 | f.Name.Equals("zh_cn.json", StringComparison.OrdinalIgnoreCase))).ToArray(); 127 | if (files.Length == 2 && files.Any(f => f.Name.EndsWith(".json")) && files.Any(f => f.Name.EndsWith(".lang"))) // storage drawers 128 | { 129 | files = fileType switch 130 | { 131 | LangFileType.Lang => new[] { files.First(f => f.Name.EndsWith(".lang")) }, 132 | LangFileType.Json => new[] { files.First(f => f.Name.EndsWith(".json")) }, 133 | _ => throw new ArgumentOutOfRangeException(nameof(fileType), fileType, null) 134 | }; 135 | } 136 | return files; 137 | } 138 | 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /CFPABot/Utils/PRAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CFPABot.DiffEngine; 6 | using DiffPatch.Data; 7 | 8 | namespace CFPABot.Utils 9 | { 10 | public record ModInfo(string CurseForgeID, string ModDomain, MCVersion Version); 11 | 12 | public static class PRAnalyzer 13 | { 14 | public static List Run(FileDiff[] diffs, bool langOnly = false) 15 | { 16 | var infos = new List(); 17 | foreach (var fileDiff in diffs) 18 | { 19 | var names = fileDiff.To.Split('/'); 20 | if (names.Length < 7) continue; // 超级硬编码 21 | if (names[0] != "projects") continue; 22 | 23 | var version = names[1].ToMCStandardVersion(); 24 | var cfid = names[3]; 25 | var domain = names[4]; // 这里不需要管是不是改的是语言文件 只需要看涉及了啥mod 26 | if (cfid == "1UNKNOWN") continue; 27 | if (langOnly && names[5] != "lang") continue; 28 | 29 | if (!infos.Exists(i => i.ModDomain == domain && i.Version == version && i.CurseForgeID == cfid)) // O(N^2)是吧 我不管了哈哈 30 | { 31 | infos.Add(new ModInfo(cfid, domain, version)); 32 | } 33 | } 34 | 35 | return infos; 36 | } 37 | 38 | public static List RunBleedingEdge(FileDiff[] diffs) 39 | { 40 | var paths = new HashSet(); 41 | foreach (var fileDiff in diffs) 42 | { 43 | var names = fileDiff.To.Split('/'); 44 | if (names.Length < 7) continue; 45 | if (names[0] != "projects") continue; 46 | 47 | paths.Add(new ModPath(fileDiff.To)); 48 | } 49 | return paths.ToList(); 50 | } 51 | 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /CFPABot/Utils/PRRelationAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Octokit; 6 | 7 | namespace CFPABot.Utils 8 | { 9 | 10 | 11 | 12 | public class PRRelationAnalyzer 13 | { 14 | public void Run() 15 | { 16 | 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CFPABot/Utils/RepoAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using GammaLibrary.Enhancements; 8 | using GammaLibrary.Extensions; 9 | using Serilog; 10 | 11 | namespace CFPABot.Utils 12 | { 13 | public record RepoFileAnalyzeResult(string Branch, string FilePath, string FileName, LangType Type, string CommitSha); 14 | 15 | public record RepoAnalyzeResult(string RepoGitHubLink, RepoFileAnalyzeResult[] Results, string Owner, string RepoName); 16 | 17 | public enum LangType 18 | { 19 | CN, EN 20 | } 21 | public enum LangFileType 22 | { 23 | Lang, Json 24 | } 25 | 26 | public static class RepoAnalyzer 27 | { 28 | 29 | static Dictionary locks = new(); 30 | static async ValueTask AcquireLock(string lockName) 31 | { 32 | SuperUniversalExtremeAwesomeGodlikeSmartLock l; 33 | lock (typeof(RepoAnalyzer)) 34 | { 35 | if (!locks.ContainsKey(lockName)) locks[lockName] = new SuperUniversalExtremeAwesomeGodlikeSmartLock(); 36 | l = locks[lockName]; 37 | } 38 | await l.WaitAsync(); 39 | return l; 40 | } 41 | /* 42 | public static async Task Analyze(string githubLink) 43 | { 44 | using var l = await AcquireLock(githubLink); 45 | 46 | var repoPath = "caches/" + Guid.NewGuid().ToString("N"); 47 | var sp = githubLink.Split('/'); 48 | var owner = sp[^2]; 49 | var repoName = sp[^1]; 50 | var resultPath = $"config/repo_analyze_results/{owner}.{repoName}.json"; 51 | if (File.Exists(resultPath)) return JsonSerializer.Deserialize(await File.ReadAllTextAsync(resultPath)); 52 | 53 | var githubBranches = await GitHub.Instance.Repository.Branch.GetAll(owner, repoName); 54 | 55 | Repository.Clone(githubLink + ".git", repoPath); 56 | var repo = new Repository(repoPath); 57 | var results = new List(); 58 | 59 | foreach (var branchName in githubBranches.Select(b => b.Name)) 60 | { 61 | SwitchBranch(repo, branchName); 62 | foreach (var filePath in Directory.EnumerateFiles(repoPath, "*.*", SearchOption.AllDirectories)) 63 | { 64 | var fileName = Path.GetFileName(filePath); 65 | var relativePath = Path.GetRelativePath(repoPath, filePath); 66 | if (fileName.Equals("en_us.lang", StringComparison.OrdinalIgnoreCase) || fileName.Equals("en_us.json", StringComparison.OrdinalIgnoreCase)) 67 | { 68 | results.Add(new RepoFileAnalyzeResult(branchName, relativePath, fileName, LangType.EN, repo.Branches[branchName].Tip.Sha)); 69 | } 70 | if (fileName.Equals("zh_cn.lang", StringComparison.OrdinalIgnoreCase) || fileName.Equals("zh_cn.json", StringComparison.OrdinalIgnoreCase)) 71 | { 72 | results.Add(new RepoFileAnalyzeResult(branchName, relativePath, fileName, LangType.CN, repo.Branches[branchName].Tip.Sha)); 73 | } 74 | } 75 | } 76 | repo.Dispose(); 77 | try 78 | { 79 | Directory.Delete(repoPath, true); 80 | } 81 | catch (Exception e) 82 | { 83 | Log.Error(e, $"删除 repo 失败: {githubLink}"); 84 | } 85 | 86 | var result = new RepoAnalyzeResult(githubLink, results.ToArray(), owner, repoName); 87 | File.WriteAllText(resultPath, result.ToJsonString()); 88 | return result; 89 | } 90 | 91 | 92 | // https://stackoverflow.com/questions/46588604/checking-out-remote-branch-using-libgit2sharp 93 | static Branch SwitchBranch(Repository repo, string branchName) 94 | { 95 | var trackedBranch = repo.Branches[$"origin/{branchName}"]; 96 | if (repo.Branches[branchName] == null) 97 | { 98 | trackedBranch = repo.CreateBranch(branchName, trackedBranch.Tip); 99 | } 100 | 101 | 102 | repo.Branches.Update(trackedBranch, b => b.UpstreamBranch = trackedBranch.CanonicalName); 103 | return Commands.Checkout(repo, branchName); 104 | }*/ 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /CFPABot/Utils/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using GammaLibrary.Extensions; 8 | 9 | namespace CFPABot.Utils 10 | { 11 | public static class StreamExtensions 12 | { 13 | public static async Task ReadToEndAsync1(this Stream stream, Encoding encoding = null) 14 | { 15 | using var streamReader = stream.CreateStreamReader(encoding); 16 | return await streamReader.ReadToEndAsync(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CFPABot/Utils/TermManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | using GammaLibrary.Extensions; 8 | 9 | namespace CFPABot.Utils 10 | { 11 | public class TermManager 12 | { 13 | public static async Task Init() 14 | { 15 | var file = await Assembly.GetExecutingAssembly().GetManifestResourceStream(Assembly.GetExecutingAssembly().GetManifestResourceNames() 16 | .Single(str => str.EndsWith("Minecraft-Terms-104816.json"))).ReadToEndAsync1(); 17 | var termsSource = JsonSerializer.Deserialize(file); 18 | var list = new List(); 19 | foreach (var source in termsSource) 20 | { 21 | if (source.English1.NotNullNorWhiteSpace()) 22 | { 23 | var entry = new TermEntry() 24 | { 25 | Comment = source.Comment, 26 | Paraphrase = source.Paraphrase, 27 | Type = source.Type, 28 | Source = source.Source 29 | }; 30 | entry.English = source.English1.ToLower(); 31 | entry.Chineses = new[] {source.Chinese1.ToLower(), source.Chinese2.ToLower(), source.Chinese3.ToLower() } 32 | .Where(l => l.NotNullNorWhiteSpace()).ToArray(); 33 | list.Add(entry); 34 | } 35 | if (source.English2.NotNullNorWhiteSpace()) 36 | { 37 | var entry = new TermEntry() 38 | { 39 | Comment = source.Comment, 40 | Paraphrase = source.Paraphrase, 41 | Type = source.Type, 42 | Source = source.Source 43 | }; 44 | entry.English = source.English2.ToLower(); 45 | entry.Chineses = new[] { source.Chinese1.ToLower(), source.Chinese2.ToLower(), source.Chinese3.ToLower() } 46 | .Where(l => l.NotNullNorWhiteSpace()).ToArray(); 47 | list.Add(entry); 48 | } 49 | if (source.English3.NotNullNorWhiteSpace()) 50 | { 51 | var entry = new TermEntry() 52 | { 53 | Comment = source.Comment, 54 | Paraphrase = source.Paraphrase, 55 | Type = source.Type, 56 | Source = source.Source 57 | }; 58 | entry.English = source.English3.ToLower(); 59 | entry.Chineses = new[] { source.Chinese1.ToLower(), source.Chinese2.ToLower(), source.Chinese3.ToLower() } 60 | .Where(l => l.NotNullNorWhiteSpace()).ToArray(); 61 | list.Add(entry); 62 | } 63 | } 64 | 65 | Terms = list.ToArray(); 66 | } 67 | 68 | public static TermEntry[] Terms { get; private set; } 69 | } 70 | public partial class TermEntrySource 71 | { 72 | public string Type { get; set; } 73 | public string English1 { get; set; } 74 | public string English2 { get; set; } 75 | public string English3 { get; set; } 76 | public string Chinese1 { get; set; } 77 | public string Chinese2 { get; set; } 78 | public string Chinese3 { get; set; } 79 | public string Source { get; set; } 80 | public string Comment { get; set; } 81 | public string Paraphrase { get; set; } 82 | } 83 | 84 | public partial class TermEntry 85 | { 86 | public string Type { get; set; } 87 | public string English { get; set; } 88 | public string[] Chineses { get; set; } 89 | public string Source { get; set; } 90 | public string Comment { get; set; } 91 | public string Paraphrase { get; set; } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /CFPABot/Utils/WeeklyReportHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using System.Threading.Tasks; 4 | using Octokit; 5 | 6 | namespace CFPABot.Utils 7 | { 8 | public class WeeklyReportHelper 9 | { 10 | Regex PRTitleRegex = new Regex("\\(#(\\d{4,5})\\)", RegexOptions.Compiled); 11 | public static async Task GenerateDefault(DateOnly date) 12 | { 13 | var dateTime = date.ToDateTime(new TimeOnly(12, 0)); 14 | var commits = await GitHub.Instance.Repository.Commit.GetAll(Constants.RepoID, new CommitRequest(){Since = new DateTimeOffset(dateTime.AddDays(-7), TimeSpan.FromHours(8))}); 15 | 16 | throw new NotImplementedException(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CFPABot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CFPABot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CFPABot 2 | https://github.com/CFPAOrg/Minecraft-Mod-Language-Package 的 *PR 管理*及一些*网页工具* 3 | 4 | ***但是这一切真的值得吗((*** 5 | 6 | https://cfpa.cyan.cafe/Azusa/ 7 | 8 | > 其实代码是一堆 sh\*t 山 我自己都看不懂( 9 | 10 | > 除了 token 以外的内容均开源,直接使用下面的部署指南就可以部署 11 | 12 | ## 命令列表 13 | 14 | 每一行会当作单独的命令执行,也就是说你可以在同一个 Comment 内执行多条命令。 15 | 所有命令仅维护者和 PR 提交者可用。 16 | 17 | - `/mv-recursive [a] [b]` 将 a 移动到 b,文件夹或文件均可,若路径包含空格可使用引号包裹 a 和 b。 18 | - `/update-en [CurseForge项目名] [游戏版本]` 更新指定模组的英文文件。 19 | 游戏版本可为仓库中的五个值 `1.12.2` `1.16` `1.18` `1.16-fabric` `1.18-fabric` 20 | - `/sort-keys [文件路径]` 重排键序。适用于 MCreator。 21 | - `/add-mapping [slug] [curseForgeProjectID]` 你用不到的。 22 | 23 | 例如:`/update-en xaeros-world-map 1.12.2` 24 | 25 | 善用 PR files 内的文件路径复制功能。 26 | 27 | ## Overview 28 | 29 | ![Snipaste_2022-05-03_18-36-47](https://user-images.githubusercontent.com/14993992/166440710-e0088f7d-c88a-4984-ab7d-a88161fc83f8.png) 30 | 31 | ## 自己部署 32 | 33 | > 如果有哪一天我似了() 可以用下面的方法自己部署 34 | 35 | - 参照 `build-from-codespace.sh` 构建 docker image 36 | - 修改 `docker-compose.yml` 中的环境变量 37 | - 在 `config/` 放置 `cfpa-bot.pem`,这是 GitHub App 的私钥 38 | - GitHub App Webhook 设置 `https://你的域名/api/WebhookListener`, Webhook Secret 为 `docker-compose.yml` 中的 `GITHUB_WEBHOOK_SECRET` 39 | - project hex 需要放置 Packer: 在主库中 `dotnet publish .\src\Packer\Packer.csproj -o ./ -r linux-x64 -p:PublishSingleFile=true` 40 | - 初始 `config/mappings.json` 的生成方法: 41 | 42 | ```csharp 43 | // 版本更新也行 44 | // 45 | 46 | var apiClient = new ApiClient("CURSEFORGE_API_TOKEN", "你的邮箱"); 47 | for (int i = 0; i < 50; i++) 48 | { 49 | var addons = await apiClient.GetModsByIdListAsync(new GetModsByIdsListRequestBody() { ModIds = Enumerable.Range(i * 20000 + 1, 20000).Select(x => (uint)x).ToList()}); 50 | List addonsData = addons.Data; 51 | AddMapping(addonsData); 52 | Console.WriteLine($"初始化 Mapping: {i + 1}/50"); 53 | } 54 | 55 | ModIDMappingMetadata.Save(); 56 | 57 | static void AddMapping(List addons) 58 | { 59 | foreach (var addon in addons.Where(s => s.GameId == 432 && s.Links.WebsiteUrl.StartsWith("https://www.curseforge.com/minecraft/mc-mods/"))) 60 | lock (ModIDMappingMetadata.Instance) 61 | { 62 | ModIDMappingMetadata.Instance.Mapping[addon.Slug] = (int)addon.Id; 63 | } 64 | } 65 | 66 | [ConfigurationPath("mappings.json")] 67 | public class ModIDMappingMetadata : Configuration 68 | { 69 | public Dictionary Mapping { get; set; } = new(); 70 | public DateTime LastUpdate { get; set; } 71 | [JsonIgnore] public int LastID => Mapping.Values.Max(); 72 | } 73 | ``` 74 | - 把代码所有的 `cfpa.cyan.cafe` 换成你的域名 (有 4 个地方要改) 75 | - 如果需要 Azusa, nginx 配置反代时需要配置 WebSocket 76 | - 最后 `docker-compose up -d` 即可 -------------------------------------------------------------------------------- /build-from-codespace.sh: -------------------------------------------------------------------------------- 1 | git pull 2 | docker build -f "CFPABot/Dockerfile" -t docker.cyan.cafe/cfpabot . 3 | docker image push docker.cyan.cafe/cfpabot:latest -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | cfpabot: 5 | restart: unless-stopped 6 | image: YOUR_DOCKER_IMAGE 7 | volumes: 8 | - ./config:/app/config 9 | - ./logs:/app/logs 10 | - ./wwwroot:/app/wwwroot 11 | - ./Packer:/app/Packer 12 | - ./project-hex:/app/project-hex 13 | ports: 14 | - 19003:80 15 | environment: 16 | - GITHUB_WEBHOOK_SECRET= # webhook 密钥 GitHub App 配置 17 | - GITHUB_OAUTH_TOKEN=ghp_ # 用于 gist 上传的个人账号 token 18 | - CURSEFORGE_API_KEY= # https://console.curseforge.com/?#/login 申请, $需要换成$$ 19 | - CFPA_HELPER_GITHUB_OAUTH_CLIENT_SECRET= # CFPA Azusa 网页的 GitHub OAuth Client Secret,如果需要自己部署需要同时改 Constant.cs 中的 ClientId 20 | --------------------------------------------------------------------------------