├── .travis.yml ├── src └── VerifyBot │ ├── Models │ ├── EntityFramework │ │ ├── User.cs │ │ └── VerifyDatabase.cs │ ├── Configuration.cs │ └── UserStrings.cs │ ├── Factories │ ├── UserStringsFactory.cs │ ├── ConfigurationFactory.cs │ └── DiscordClientFactory.cs │ ├── Properties │ ├── AssemblyInfo.cs │ └── PublishProfiles │ │ ├── release-publish.ps1 │ │ └── publish-module.psm1 │ ├── Migrations │ ├── VerifyContextModelSnapshot.cs │ ├── 20160801152723_initial.cs │ └── 20160801152723_initial.Designer.cs │ ├── strings.json │ ├── VerifyBot.csproj │ ├── Services │ ├── LookupService.cs │ ├── WorldVerificationService.cs │ ├── RemindVerifyService.cs │ ├── StatisticsService.cs │ ├── ReverifyService.cs │ └── VerifyService.cs │ ├── Manager.cs │ ├── .gitignore │ └── Program.cs ├── README.md ├── LICENSE ├── VerifyBot.sln └── .gitignore /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | mono: none 3 | dotnet: 2.0.2 4 | dist: trusty 5 | solution: VerifyBot.sln 6 | script: 7 | - dotnet restore 8 | -------------------------------------------------------------------------------- /src/VerifyBot/Models/EntityFramework/User.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace VerifyBot.Models 4 | { 5 | public class User 6 | { 7 | [Key] 8 | public string AccountID { get; set; } 9 | 10 | public string APIKey { get; set; } 11 | 12 | public ulong DiscordID { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/VerifyBot/Models/Configuration.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace VerifyBot.Models 4 | { 5 | public class Configuration 6 | { 7 | public ulong ServerId { get; set; } 8 | 9 | public string VerifyRole { get; set; } 10 | 11 | public string DiscordToken { get; set; } 12 | 13 | public List WorldIds { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/VerifyBot/Factories/UserStringsFactory.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.IO; 3 | using VerifyBot.Models; 4 | 5 | namespace VerifyBot.Factories 6 | { 7 | public static class UserStringsFactory 8 | { 9 | private const string UserStringsFile = "strings.json"; 10 | 11 | public static UserStrings Get() 12 | { 13 | if (!File.Exists(UserStringsFile)) 14 | { 15 | throw new FileNotFoundException($"Could not find the {UserStringsFile} file"); 16 | } 17 | 18 | var file = File.ReadAllText(UserStringsFile); 19 | var userStrings = JsonConvert.DeserializeObject(file); 20 | 21 | return userStrings; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/VerifyBot/Factories/ConfigurationFactory.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.IO; 3 | using VerifyBot.Models; 4 | 5 | namespace VerifyBot.Factories 6 | { 7 | public static class ConfigurationFactory 8 | { 9 | private const string ConfigurationFile = "secrets.json"; 10 | 11 | public static Configuration Get() 12 | { 13 | if (!File.Exists(ConfigurationFile)) 14 | { 15 | throw new FileNotFoundException($"Could not find the {ConfigurationFile} file"); 16 | } 17 | 18 | var file = File.ReadAllText(ConfigurationFile); 19 | var userStrings = JsonConvert.DeserializeObject(file); 20 | 21 | return userStrings; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/VerifyBot/Models/UserStrings.cs: -------------------------------------------------------------------------------- 1 | namespace VerifyBot.Models 2 | { 3 | public class UserStrings 4 | { 5 | public string AccountAlreadyVerified { get; set; } 6 | 7 | public string AccountNameDoesNotMatch { get; set; } 8 | 9 | public string AccountNotInAPI { get; set; } 10 | 11 | public string AccountNotOnServer { get; set; } 12 | 13 | public string EndMessage { get; set; } 14 | 15 | public string ErrorMessage { get; set; } 16 | 17 | public string InitialMessage { get; set; } 18 | 19 | public string InvalidAPIKey { get; set; } 20 | 21 | public string NotValidLevel { get; set; } 22 | 23 | public string ParseError { get; set; } 24 | 25 | public string VerificationReminder { get; set; } 26 | } 27 | } -------------------------------------------------------------------------------- /src/VerifyBot/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("VerifyBot")] 10 | [assembly: AssemblyTrademark("")] 11 | 12 | // Setting ComVisible to false makes the types in this assembly not visible 13 | // to COM components. If you need to access a type in this assembly from 14 | // COM, set the ComVisible attribute to true on that type. 15 | [assembly: ComVisible(false)] 16 | 17 | // The following GUID is for the ID of the typelib if this project is exposed to COM 18 | [assembly: Guid("ae213aca-3fc0-406d-82b5-2329ccabeb6d")] -------------------------------------------------------------------------------- /src/VerifyBot/Properties/PublishProfiles/release-publish.ps1: -------------------------------------------------------------------------------- 1 | [cmdletbinding(SupportsShouldProcess=$true)] 2 | param($publishProperties=@{}, $packOutput, $pubProfilePath) 3 | 4 | # to learn more about this file visit https://go.microsoft.com/fwlink/?LinkId=524327 5 | 6 | try{ 7 | if ($publishProperties['ProjectGuid'] -eq $null){ 8 | $publishProperties['ProjectGuid'] = 'ae213aca-3fc0-406d-82b5-2329ccabeb6d' 9 | } 10 | 11 | $publishModulePath = Join-Path (Split-Path $MyInvocation.MyCommand.Path) 'publish-module.psm1' 12 | Import-Module $publishModulePath -DisableNameChecking -Force 13 | 14 | # call Publish-AspNet to perform the publish operation 15 | Publish-AspNet -publishProperties $publishProperties -packOutput $packOutput -pubProfilePath $pubProfilePath 16 | } 17 | catch{ 18 | "An error occurred during publish.`n{0}" -f $_.Exception.Message | Write-Error 19 | } -------------------------------------------------------------------------------- /src/VerifyBot/Migrations/VerifyContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Infrastructure; 3 | using VerifyBot.Models; 4 | 5 | namespace VerifyBot.Migrations 6 | { 7 | [DbContext(typeof(VerifyDatabase))] 8 | partial class VerifyContextModelSnapshot : ModelSnapshot 9 | { 10 | protected override void BuildModel(ModelBuilder modelBuilder) 11 | { 12 | modelBuilder 13 | .HasAnnotation("ProductVersion", "1.0.0-rtm-21431"); 14 | 15 | modelBuilder.Entity("VerifyBot.Models.User", b => 16 | { 17 | b.Property("AccountID"); 18 | 19 | b.Property("APIKey"); 20 | 21 | b.Property("DiscordID"); 22 | 23 | b.HasKey("AccountID"); 24 | 25 | b.ToTable("Users"); 26 | }); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VerifyBot 2 | 3 | **Note: This software is no longer maintained. Please do not message me asking for help** 4 | 5 | [![Build Status](https://travis-ci.org/seasniffer/VerifyBot.svg?branch=master)](https://travis-ci.org/seasniffer/VerifyBot) 6 | 7 | A bot that uses the Discord.NET and Guild Wars 2 API's to verify what world a users account is on. Made for Jade Quarry Discord but open source for all to use. 8 | 9 | You'll need a verified rank in Discord and a #verify text channel. When a user types !verify the bot will message them with instructions. Once a user is verified an entry is made into a SQLite database and the user is given the verified rank. 10 | 11 | You'll need to add a secrets.json file to the directory that the executable is running from. Here are the required fields. 12 | ``` 13 | { 14 | "WorldIds": [ 1008, 1001, 1013 ], 15 | "ServerId": your_discord_server_id_here, 16 | "DiscordToken": "your_discord_bot_token_here", 17 | "VerifyRole": "your_verified_role_name_here" 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /src/VerifyBot/Migrations/20160801152723_initial.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace VerifyBot.Migrations 4 | { 5 | public partial class initial : Migration 6 | { 7 | protected override void Down(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.DropTable( 10 | name: "Users"); 11 | } 12 | 13 | protected override void Up(MigrationBuilder migrationBuilder) 14 | { 15 | migrationBuilder.CreateTable( 16 | name: "Users", 17 | columns: table => new 18 | { 19 | AccountID = table.Column(nullable: false), 20 | APIKey = table.Column(nullable: true), 21 | DiscordID = table.Column(nullable: false) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_Users", x => x.AccountID); 26 | }); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/VerifyBot/Migrations/20160801152723_initial.Designer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using VerifyBot.Models; 7 | 8 | namespace VerifyBot.Migrations 9 | { 10 | [DbContext(typeof(VerifyDatabase))] 11 | [Migration("20160801152723_initial")] 12 | partial class initial 13 | { 14 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 15 | { 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "1.0.0-rtm-21431"); 18 | 19 | modelBuilder.Entity("VerifyBot.Models.User", b => 20 | { 21 | b.Property("AccountID"); 22 | 23 | b.Property("APIKey"); 24 | 25 | b.Property("DiscordID"); 26 | 27 | b.HasKey("AccountID"); 28 | 29 | b.ToTable("Users"); 30 | }); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Calvin Siemandel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/VerifyBot/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AccountAlreadyVerified": "Account is already verified. If you are having issues message a verifier", 3 | "AccountNameDoesNotMatch": "API Key account does not match supplied account name.", 4 | "AccountNotInAPI": "Could not find that account in the GW2 API.", 5 | "AccountNotOnServer": "Account is not on a valid server.", 6 | "EndMessage": "Verification Process Complete. Welcome!", 7 | "ErrorMessage": "Error processing your verification request. Please try again.", 8 | "InvalidAPIKey": "Bad API Key", 9 | "NotValidLevel": "This account does not have a WvW-eligible character. (Level 60+)", 10 | "ParseError": "Could not parse your response.", 11 | "VerificationReminder": "Hello, friend!\r\n\r\nIt appears that you are not verified yet. Below are some instructions on how to get started with our verification process. If you have any questions, feel free to ask in the discord-help channel.\r\n\r\n**Instructions on how to verify**\r\nhttps://docs.google.com/document/d/14i3S1KxjlZoiJfkBsf2r6HRediVl1TXNZJo7F-xmVgs - English\r\nhttps://docs.google.com/document/d/1FO7kg_CIIjoZaUqZXKPJ8LUfpfmftfbQ2lrOIZsNrBI - Chinese", 12 | "InitialMessage": "Welcome to the JQ Discord!\r\n\r\n**Instructions on how to verify**\r\nhttps://docs.google.com/document/d/14i3S1KxjlZoiJfkBsf2r6HRediVl1TXNZJo7F-xmVgs - English\r\nhttps://docs.google.com/document/d/1FO7kg_CIIjoZaUqZXKPJ8LUfpfmftfbQ2lrOIZsNrBI - Chinese" 13 | } -------------------------------------------------------------------------------- /src/VerifyBot/Models/EntityFramework/VerifyDatabase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace VerifyBot.Models 7 | { 8 | public class VerifyDatabase : DbContext 9 | { 10 | public DbSet Users { get; set; } 11 | 12 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 13 | { 14 | optionsBuilder.UseSqlite("Filename=Users.db"); 15 | } 16 | 17 | public async Task AddOrUpdateUser(string accountId, string apiKey, ulong discordId) 18 | { 19 | try 20 | { 21 | var existingUser = Users.FirstOrDefault(x => x.AccountID == accountId); 22 | 23 | if (existingUser != null) 24 | { 25 | existingUser.DiscordID = discordId; 26 | } 27 | else 28 | { 29 | Users.Add(new User() 30 | { 31 | AccountID = accountId, 32 | APIKey = apiKey, 33 | DiscordID = discordId 34 | }); 35 | } 36 | 37 | await SaveChangesAsync(); 38 | } 39 | catch (Exception ex) 40 | { 41 | Console.WriteLine(ex); 42 | throw; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /VerifyBot.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26403.7 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4A7FA39B-5AA1-46E3-84DE-71980024640E}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{82D36770-1205-4B99-A966-447EB2C41407}" 9 | ProjectSection(SolutionItems) = preProject 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VerifyBot", "src\VerifyBot\VerifyBot.csproj", "{AE213ACA-3FC0-406D-82B5-2329CCABEB6D}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {AE213ACA-3FC0-406D-82B5-2329CCABEB6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {AE213ACA-3FC0-406D-82B5-2329CCABEB6D}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {AE213ACA-3FC0-406D-82B5-2329CCABEB6D}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {AE213ACA-3FC0-406D-82B5-2329CCABEB6D}.Release|Any CPU.Build.0 = Release|Any CPU 25 | EndGlobalSection 26 | GlobalSection(SolutionProperties) = preSolution 27 | HideSolutionNode = FALSE 28 | EndGlobalSection 29 | GlobalSection(NestedProjects) = preSolution 30 | {AE213ACA-3FC0-406D-82B5-2329CCABEB6D} = {4A7FA39B-5AA1-46E3-84DE-71980024640E} 31 | EndGlobalSection 32 | EndGlobal 33 | -------------------------------------------------------------------------------- /src/VerifyBot/VerifyBot.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | VerifyBot 6 | Exe 7 | VerifyBot 8 | 2.0 9 | false 10 | false 11 | false 12 | 13 | 14 | true 15 | 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 | -------------------------------------------------------------------------------- /src/VerifyBot/Factories/DiscordClientFactory.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | //using Discord.Net.Providers.WS4Net; 3 | using Discord.WebSocket; 4 | using System; 5 | using System.Threading.Tasks; 6 | using VerifyBot.Models; 7 | 8 | namespace VerifyBot.Factories 9 | { 10 | public static class DiscordClientFactory 11 | { 12 | public static async Task Get(Configuration config) 13 | { 14 | DiscordSocketClient client; 15 | 16 | if (System.Runtime.InteropServices.RuntimeInformation.OSDescription.Contains("Microsoft Windows 6")) 17 | { 18 | client = new DiscordSocketClient(new DiscordSocketConfig() 19 | { 20 | LogLevel = LogSeverity.Info 21 | //WebSocketProvider = WS4NetProvider.Instance 22 | }); 23 | } 24 | else 25 | { 26 | client = new DiscordSocketClient(); 27 | } 28 | 29 | client.Log += (m) => 30 | { 31 | Console.WriteLine(m.ToString()); 32 | return Task.CompletedTask; 33 | }; 34 | 35 | await client.LoginAsync(TokenType.Bot, config.DiscordToken); 36 | await client.StartAsync(); 37 | 38 | var ready = false; 39 | client.Ready += () => 40 | { 41 | ready = true; 42 | return Task.CompletedTask; 43 | }; 44 | 45 | Console.WriteLine("Waiting for DiscordSocketClient to initialize..."); 46 | 47 | while (!ready) 48 | { 49 | await Task.Delay(1000); 50 | Console.WriteLine("Waiting..."); 51 | } 52 | 53 | Console.WriteLine(" DiscordSocketClient initialized."); 54 | 55 | return client; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/VerifyBot/Services/LookupService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using VerifyBot.Models; 6 | 7 | namespace VerifyBot.Services 8 | { 9 | public class LookupService 10 | { 11 | private readonly Manager manager; 12 | 13 | private readonly UserStrings strings; 14 | 15 | public LookupService(Manager manager, UserStrings strings) 16 | { 17 | this.manager = manager; 18 | this.strings = strings; 19 | } 20 | 21 | public async Task LookupAsync(string accountName) 22 | { 23 | var counts = new Dictionary(); 24 | var discordUsers = await manager.GetDiscordUsers(); 25 | 26 | foreach (var discordUser in discordUsers) 27 | { 28 | var dbUser = await manager.GetDatabaseUser(discordUser.Id); 29 | 30 | if (dbUser == null) 31 | { 32 | continue; 33 | } 34 | 35 | try 36 | { 37 | var verifier = VerifyService.Create(dbUser.AccountID, dbUser.APIKey, manager, discordUser, strings); 38 | 39 | await verifier.LoadAccount(); 40 | 41 | if (verifier.AccountName == accountName) 42 | { 43 | Console.WriteLine($"Account {accountName} found, Discord Name: {discordUser.Nickname ?? discordUser.Username}"); 44 | return; 45 | } 46 | } 47 | catch (Exception) 48 | { 49 | Console.WriteLine($"Could not load information for user {discordUser.Username} ({dbUser.APIKey})"); 50 | } 51 | } 52 | 53 | Console.WriteLine($"Account {accountName} not found"); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/VerifyBot/Services/WorldVerificationService.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System; 3 | using System.Threading.Tasks; 4 | using VerifyBot.Models; 5 | 6 | namespace VerifyBot.Services 7 | { 8 | public class WorldVerificationService 9 | { 10 | private readonly Manager manager; 11 | 12 | private readonly UserStrings strings; 13 | 14 | public WorldVerificationService(Manager manager, UserStrings strings) 15 | { 16 | this.manager = manager; 17 | this.strings = strings; 18 | } 19 | 20 | public async Task Process(IMessage e) 21 | { 22 | try 23 | { 24 | Console.WriteLine($"Message: {e.Content}"); 25 | 26 | Console.WriteLine($"Begin verification for {e.Author.Username}"); 27 | await e.Channel.SendMessageAsync("Starting Verification Process..."); 28 | 29 | if (e.Author.Status == UserStatus.Invisible) 30 | { 31 | await e.Channel.SendMessageAsync("You cannot be set to invisible while Verifying. Please change your discord status to Online"); 32 | return; 33 | } 34 | 35 | var request = await VerifyService.CreateFromRequestMessage(e, manager, this.strings); 36 | 37 | if (request == null) 38 | { 39 | return; 40 | } 41 | 42 | await request.Validate(false); 43 | 44 | if (!request.IsValid) 45 | return; 46 | 47 | await manager.VerifyUser(request.Requestor.Id, request.Account.Id, request.APIKey); 48 | 49 | await e.Channel.SendMessageAsync(this.strings.EndMessage); 50 | Console.WriteLine($"{e.Author.Username} Verified."); 51 | } 52 | catch (Exception ex) { 53 | 54 | await e.Channel.SendMessageAsync(this.strings.ErrorMessage); 55 | Console.WriteLine($"Error: {ex.ToString()}"); 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/VerifyBot/Services/RemindVerifyService.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using VerifyBot.Models; 6 | 7 | namespace VerifyBot.Services 8 | { 9 | public class RemindVerifyService 10 | { 11 | private readonly Manager manager; 12 | 13 | private readonly UserStrings strings; 14 | 15 | public RemindVerifyService(Manager manager, UserStrings strings) 16 | { 17 | this.manager = manager; 18 | this.strings = strings; 19 | } 20 | 21 | public async Task Process() 22 | { 23 | try 24 | { 25 | await RemindUsers(); 26 | } 27 | catch (Exception ex) 28 | { 29 | Console.WriteLine($"Error while checking: {ex}"); 30 | } 31 | } 32 | 33 | public async Task SendInstructions(IGuildUser user) 34 | { 35 | var channel = await user.GetOrCreateDMChannelAsync(); 36 | 37 | if (manager.IsUserVerified(user)) 38 | { 39 | await channel.SendMessageAsync(this.strings.AccountAlreadyVerified); 40 | } 41 | else 42 | { 43 | await channel.SendMessageAsync(this.strings.VerificationReminder); 44 | Console.WriteLine($"Instructed {user.Nickname} ({user.Id})"); 45 | } 46 | 47 | await channel.CloseAsync(); 48 | } 49 | 50 | private async Task RemindUsers() 51 | { 52 | var verifyRoleId = manager.VerifyRoleId; 53 | 54 | var allUsers = await manager.GetDiscordUsers(); 55 | var unverifiedUsers = allUsers.Where(u => !manager.IsUserVerified(u)); 56 | 57 | foreach (var user in unverifiedUsers) 58 | { 59 | var channel = await user.GetOrCreateDMChannelAsync(); 60 | await channel.SendMessageAsync(this.strings.VerificationReminder); 61 | Console.WriteLine($"reminded {user.Nickname} ({user.Id})"); 62 | await channel.CloseAsync(); 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/VerifyBot/Services/StatisticsService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using VerifyBot.Models; 5 | 6 | namespace VerifyBot.Services 7 | { 8 | public class StatisticsService 9 | { 10 | private readonly Manager manager; 11 | 12 | private readonly UserStrings strings; 13 | 14 | public StatisticsService(Manager manager, UserStrings strings) 15 | { 16 | this.manager = manager; 17 | this.strings = strings; 18 | } 19 | 20 | public async Task GetStatistics() 21 | { 22 | try 23 | { 24 | Console.WriteLine("Calculating Statistics"); 25 | 26 | var counts = new Dictionary(); 27 | var discordUsers = await manager.GetDiscordUsers(); 28 | 29 | foreach (var discordUser in discordUsers) 30 | { 31 | var dbUser = await manager.GetDatabaseUser(discordUser.Id); 32 | 33 | if (dbUser == null) 34 | { 35 | continue; 36 | } 37 | try 38 | { 39 | var verifier = VerifyService.Create(dbUser.AccountID, dbUser.APIKey, manager, discordUser, strings); 40 | 41 | await verifier.LoadAccount(); 42 | 43 | if (!counts.ContainsKey(verifier.World)) 44 | { 45 | counts.Add(verifier.World, 0); 46 | } 47 | 48 | counts[verifier.World] = counts[verifier.World] + 1; 49 | } 50 | catch (Exception) 51 | { 52 | Console.WriteLine($"Could not load information for user {discordUser.Username} ({dbUser.APIKey})"); 53 | } 54 | } 55 | 56 | foreach (var value in counts) 57 | { 58 | Console.WriteLine($"World [{value.Key}]: {value.Value}"); 59 | } 60 | } 61 | catch (Exception ex) 62 | { 63 | Console.WriteLine($"Exception: {ex.Message}"); 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/VerifyBot/Services/ReverifyService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using VerifyBot.Models; 5 | 6 | namespace VerifyBot.Services 7 | { 8 | public class ReverifyService 9 | { 10 | private readonly Manager manager; 11 | 12 | private readonly UserStrings strings; 13 | 14 | public ReverifyService(Manager manager, UserStrings strings) 15 | { 16 | this.manager = manager; 17 | this.strings = strings; 18 | } 19 | 20 | public async Task Process() 21 | { 22 | try 23 | { 24 | await CheckUsers(); 25 | } 26 | catch (Exception ex) 27 | { 28 | Console.WriteLine($"Error while checking: {ex}"); 29 | } 30 | } 31 | 32 | private async Task CheckUsers() 33 | { 34 | Console.WriteLine("Reverification process beginning"); 35 | 36 | var discordUsers = await manager.GetDiscordUsers(); 37 | var verifiedNonBotUsers = discordUsers.Where(u => !(u.IsBot || !manager.IsUserVerified(u))); 38 | 39 | foreach (var discordUser in verifiedNonBotUsers) 40 | { 41 | Console.WriteLine($"Verifying user {discordUser.Nickname ?? discordUser.Username}"); 42 | 43 | var dbUser = await manager.GetDatabaseUser(discordUser.Id); 44 | if (dbUser == null) 45 | { 46 | try 47 | { 48 | if (discordUser.GuildPermissions.Administrator) 49 | { 50 | continue; 51 | } 52 | 53 | await manager.UnverifyUser(discordUser, dbUser); 54 | continue; 55 | } 56 | catch (Exception ex) 57 | { 58 | Console.WriteLine($"Error while checking user {discordUser.Nickname ?? discordUser.Username}: {ex.Message}"); 59 | } 60 | } 61 | 62 | var attempts = 0; 63 | while (attempts < 3) 64 | { 65 | try 66 | { 67 | var verifier = VerifyService.Create(dbUser.AccountID, dbUser.APIKey, manager, discordUser, strings); 68 | await verifier.Validate(true); 69 | 70 | if (verifier.IsValid) 71 | Console.WriteLine($"User {discordUser.Nickname ?? discordUser.Username} is still valid"); 72 | else 73 | await manager.UnverifyUser(discordUser, dbUser); 74 | 75 | break; 76 | } 77 | catch (Exception) 78 | { 79 | Console.WriteLine($"Error reverifying user {discordUser.Nickname ?? discordUser.Username} ({discordUser.Id})"); 80 | //Console.WriteLine($"Error: {ex}"); 81 | attempts++; 82 | } 83 | } 84 | } 85 | 86 | Console.WriteLine("Reverification process complete"); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /src/VerifyBot/Manager.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.WebSocket; 3 | using DL.GuildWars2Api.Models.V2; 4 | using Microsoft.EntityFrameworkCore; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using VerifyBot.Models; 10 | 11 | namespace VerifyBot 12 | { 13 | public class Manager 14 | { 15 | private readonly VerifyDatabase db = new VerifyDatabase(); 16 | 17 | private bool isInitialized; 18 | 19 | public Manager(DiscordSocketClient client, Configuration config) 20 | { 21 | this.DiscordClient = client; 22 | this.Config = config; 23 | 24 | Initialize().Wait(); 25 | } 26 | 27 | public IRole VerifyRole { get; private set; } 28 | 29 | public ulong VerifyRoleId { get { return VerifyRole.Id; } } 30 | 31 | private Configuration Config { get; set; } 32 | 33 | private IGuild Discord { get; set; } 34 | 35 | private IDiscordClient DiscordClient { get; set; } 36 | 37 | public async Task GetDatabaseUser(ulong discordId) 38 | { 39 | return await db.Users.FirstOrDefaultAsync(x => x.DiscordID == discordId); 40 | } 41 | 42 | public async Task GetDiscordUser(ulong id) 43 | { 44 | return await Discord.GetUserAsync(id); 45 | } 46 | 47 | public async Task> GetDiscordUsers() 48 | { 49 | return await Discord.GetUsersAsync(); 50 | } 51 | 52 | public bool IsAccountOnOurWorld(Account account) 53 | { 54 | return Config.WorldIds.Contains(account.WorldId); 55 | } 56 | 57 | public bool IsUserVerified(IGuildUser user) 58 | { 59 | if ((user?.RoleIds?.Count ?? 0) == 0) 60 | { 61 | return false; 62 | } 63 | 64 | return user.RoleIds.Contains(VerifyRoleId); 65 | } 66 | 67 | public async Task UnverifyUser(IGuildUser discordUser, User dbUser = null) 68 | { 69 | try 70 | { 71 | //// Can't remove @everyone role. 72 | var everyone = Discord.Roles.FirstOrDefault(x => x.Name == "@everyone"); 73 | var rolesToRemove = new List(); 74 | 75 | foreach (var roleId in discordUser.RoleIds) 76 | { 77 | if (roleId == everyone.Id) 78 | { 79 | continue; 80 | } 81 | 82 | rolesToRemove.Add(Discord.GetRole(roleId)); 83 | } 84 | 85 | await discordUser.RemoveRolesAsync(rolesToRemove); 86 | 87 | if (dbUser == null) 88 | dbUser = await GetDatabaseUser(discordUser.Id); 89 | 90 | if (dbUser != null) 91 | { 92 | db.Users.Remove(dbUser); 93 | await db.SaveChangesAsync(); 94 | Console.WriteLine($"User {discordUser.Nickname ?? discordUser.Username} is no longer valid"); 95 | } 96 | else 97 | { 98 | Console.WriteLine($"Manually verified user {discordUser.Nickname ?? discordUser.Username} is no longer valid"); 99 | } 100 | } 101 | catch (Exception ex) 102 | { 103 | Console.WriteLine(ex); 104 | } 105 | } 106 | 107 | public async Task VerifyUser(ulong discordId, string accountId, string apiKey) 108 | { 109 | if (!isInitialized) 110 | { 111 | await Initialize(); 112 | } 113 | 114 | if (VerifyRole == null) 115 | { 116 | throw new NullReferenceException("Verified User Role isn't set."); 117 | } 118 | 119 | var user = await GetDiscordUser(discordId); 120 | 121 | if (user == null) 122 | { 123 | /// User is offline, notify 124 | 125 | } 126 | 127 | if (!IsUserVerified(user)) 128 | await user.AddRoleAsync(VerifyRole); 129 | 130 | await db.AddOrUpdateUser(accountId, apiKey, discordId); 131 | } 132 | 133 | private async Task Initialize() 134 | { 135 | Discord = await DiscordClient.GetGuildAsync(Config.ServerId); 136 | 137 | // set verify role id 138 | VerifyRole = Discord.Roles.Where(x => x.Name == Config.VerifyRole)?.FirstOrDefault(); 139 | 140 | if (VerifyRole == null) 141 | { 142 | var msg = $"Unable to find server role matching verify role config of '{Config.VerifyRole}'."; 143 | Console.WriteLine(msg); 144 | throw new InvalidOperationException(msg); 145 | } 146 | 147 | isInitialized = true; 148 | } 149 | } 150 | } -------------------------------------------------------------------------------- /src/VerifyBot/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | /secrets.json 5 | /secrets.txt 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | artifacts/ 49 | 50 | *_i.c 51 | *_p.c 52 | *_i.h 53 | *.ilk 54 | *.meta 55 | *.obj 56 | *.pch 57 | *.pdb 58 | *.pgc 59 | *.pgd 60 | *.rsp 61 | *.sbr 62 | *.tlb 63 | *.tli 64 | *.tlh 65 | *.tmp 66 | *.tmp_proj 67 | *.log 68 | *.vspscc 69 | *.vssscc 70 | .builds 71 | *.pidb 72 | *.svclog 73 | *.scc 74 | 75 | # Chutzpah Test files 76 | _Chutzpah* 77 | 78 | # Visual C++ cache files 79 | ipch/ 80 | *.aps 81 | *.ncb 82 | *.opendb 83 | *.opensdf 84 | *.sdf 85 | *.cachefile 86 | *.VC.db 87 | *.VC.VC.opendb 88 | 89 | # Visual Studio profiler 90 | *.psess 91 | *.vsp 92 | *.vspx 93 | *.sap 94 | 95 | # TFS 2012 Local Workspace 96 | $tf/ 97 | 98 | # Guidance Automation Toolkit 99 | *.gpState 100 | 101 | # ReSharper is a .NET coding add-in 102 | _ReSharper*/ 103 | *.[Rr]e[Ss]harper 104 | *.DotSettings.user 105 | 106 | # JustCode is a .NET coding add-in 107 | .JustCode 108 | 109 | # TeamCity is a build add-in 110 | _TeamCity* 111 | 112 | # DotCover is a Code Coverage Tool 113 | *.dotCover 114 | 115 | # NCrunch 116 | _NCrunch_* 117 | .*crunch*.local.xml 118 | nCrunchTemp_* 119 | 120 | # MightyMoose 121 | *.mm.* 122 | AutoTest.Net/ 123 | 124 | # Web workbench (sass) 125 | .sass-cache/ 126 | 127 | # Installshield output folder 128 | [Ee]xpress/ 129 | 130 | # DocProject is a documentation generator add-in 131 | DocProject/buildhelp/ 132 | DocProject/Help/*.HxT 133 | DocProject/Help/*.HxC 134 | DocProject/Help/*.hhc 135 | DocProject/Help/*.hhk 136 | DocProject/Help/*.hhp 137 | DocProject/Help/Html2 138 | DocProject/Help/html 139 | 140 | # Click-Once directory 141 | publish/ 142 | 143 | # Publish Web Output 144 | *.[Pp]ublish.xml 145 | *.azurePubxml 146 | # TODO: Comment the next line if you want to checkin your web deploy settings 147 | # but database connection strings (with potential passwords) will be unencrypted 148 | *.pubxml 149 | *.publishproj 150 | 151 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 152 | # checkin your Azure Web App publish settings, but sensitive information contained 153 | # in these scripts will be unencrypted 154 | PublishScripts/ 155 | 156 | # NuGet Packages 157 | *.nupkg 158 | # The packages folder can be ignored because of Package Restore 159 | **/packages/* 160 | # except build/, which is used as an MSBuild target. 161 | !**/packages/build/ 162 | # Uncomment if necessary however generally it will be regenerated when neededvim 163 | #!**/packages/repositories.config 164 | # NuGet v3's project.json files produces more ignoreable files 165 | *.nuget.props 166 | *.nuget.targets 167 | 168 | # Microsoft Azure Build Output 169 | csx/ 170 | *.build.csdef 171 | 172 | # Microsoft Azure Emulator 173 | ecf/ 174 | rcf/ 175 | 176 | # Windows Store app package directories and files 177 | AppPackages/ 178 | BundleArtifacts/ 179 | Package.StoreAssociation.xml 180 | _pkginfo.txt 181 | 182 | # Visual Studio cache files 183 | # files ending in .cache can be ignored 184 | *.[Cc]ache 185 | # but keep track of directories ending in .cache 186 | !*.[Cc]ache/ 187 | 188 | # Others 189 | ClientBin/ 190 | ~$* 191 | *~ 192 | *.dbmdl 193 | *.dbproj.schemaview 194 | *.pfx 195 | *.publishsettings 196 | node_modules/ 197 | orleans.codegen.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | 257 | # ========================= 258 | # Operating System Files 259 | # ========================= 260 | 261 | # OSX 262 | # ========================= 263 | 264 | .DS_Store 265 | .AppleDouble 266 | .LSOverride 267 | 268 | # Thumbnails 269 | ._* 270 | 271 | # Files that might appear in the root of a volume 272 | .DocumentRevisions-V100 273 | .fseventsd 274 | .Spotlight-V100 275 | .TemporaryItems 276 | .Trashes 277 | .VolumeIcon.icns 278 | 279 | # Directories potentially created on remote AFP share 280 | .AppleDB 281 | .AppleDesktop 282 | Network Trash Folder 283 | Temporary Items 284 | .apdisk 285 | 286 | # Windows 287 | # ========================= 288 | 289 | # Windows image file caches 290 | Thumbs.db 291 | ehthumbs.db 292 | 293 | # Folder config file 294 | Desktop.ini 295 | 296 | # Recycle Bin used on file shares 297 | $RECYCLE.BIN/ 298 | 299 | # Windows Installer files 300 | *.cab 301 | *.msi 302 | *.msm 303 | *.msp 304 | 305 | # Windows shortcuts 306 | *.lnk 307 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.pch 68 | *.pdb 69 | *.pgc 70 | *.pgd 71 | *.rsp 72 | *.sbr 73 | *.tlb 74 | *.tli 75 | *.tlh 76 | *.tmp 77 | *.tmp_proj 78 | *.log 79 | *.vspscc 80 | *.vssscc 81 | .builds 82 | *.pidb 83 | *.svclog 84 | *.scc 85 | 86 | # Chutzpah Test files 87 | _Chutzpah* 88 | 89 | # Visual C++ cache files 90 | ipch/ 91 | *.aps 92 | *.ncb 93 | *.opendb 94 | *.opensdf 95 | *.sdf 96 | *.cachefile 97 | *.VC.db 98 | *.VC.VC.opendb 99 | 100 | # Visual Studio profiler 101 | *.psess 102 | *.vsp 103 | *.vspx 104 | *.sap 105 | 106 | # Visual Studio Trace Files 107 | *.e2e 108 | 109 | # TFS 2012 Local Workspace 110 | $tf/ 111 | 112 | # Guidance Automation Toolkit 113 | *.gpState 114 | 115 | # ReSharper is a .NET coding add-in 116 | _ReSharper*/ 117 | *.[Rr]e[Ss]harper 118 | *.DotSettings.user 119 | 120 | # JustCode is a .NET coding add-in 121 | .JustCode 122 | 123 | # TeamCity is a build add-in 124 | _TeamCity* 125 | 126 | # DotCover is a Code Coverage Tool 127 | *.dotCover 128 | 129 | # AxoCover is a Code Coverage Tool 130 | .axoCover/* 131 | !.axoCover/settings.json 132 | 133 | # Visual Studio code coverage results 134 | *.coverage 135 | *.coveragexml 136 | 137 | # NCrunch 138 | _NCrunch_* 139 | .*crunch*.local.xml 140 | nCrunchTemp_* 141 | 142 | # MightyMoose 143 | *.mm.* 144 | AutoTest.Net/ 145 | 146 | # Web workbench (sass) 147 | .sass-cache/ 148 | 149 | # Installshield output folder 150 | [Ee]xpress/ 151 | 152 | # DocProject is a documentation generator add-in 153 | DocProject/buildhelp/ 154 | DocProject/Help/*.HxT 155 | DocProject/Help/*.HxC 156 | DocProject/Help/*.hhc 157 | DocProject/Help/*.hhk 158 | DocProject/Help/*.hhp 159 | DocProject/Help/Html2 160 | DocProject/Help/html 161 | 162 | # Click-Once directory 163 | publish/ 164 | 165 | # Publish Web Output 166 | *.[Pp]ublish.xml 167 | *.azurePubxml 168 | # Note: Comment the next line if you want to checkin your web deploy settings, 169 | # but database connection strings (with potential passwords) will be unencrypted 170 | *.pubxml 171 | *.publishproj 172 | 173 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 174 | # checkin your Azure Web App publish settings, but sensitive information contained 175 | # in these scripts will be unencrypted 176 | PublishScripts/ 177 | 178 | # NuGet Packages 179 | *.nupkg 180 | # The packages folder can be ignored because of Package Restore 181 | **/[Pp]ackages/* 182 | # except build/, which is used as an MSBuild target. 183 | !**/[Pp]ackages/build/ 184 | # Uncomment if necessary however generally it will be regenerated when needed 185 | #!**/[Pp]ackages/repositories.config 186 | # NuGet v3's project.json files produces more ignorable files 187 | *.nuget.props 188 | *.nuget.targets 189 | 190 | # Microsoft Azure Build Output 191 | csx/ 192 | *.build.csdef 193 | 194 | # Microsoft Azure Emulator 195 | ecf/ 196 | rcf/ 197 | 198 | # Windows Store app package directories and files 199 | AppPackages/ 200 | BundleArtifacts/ 201 | Package.StoreAssociation.xml 202 | _pkginfo.txt 203 | *.appx 204 | 205 | # Visual Studio cache files 206 | # files ending in .cache can be ignored 207 | *.[Cc]ache 208 | # but keep track of directories ending in .cache 209 | !*.[Cc]ache/ 210 | 211 | # Others 212 | ClientBin/ 213 | ~$* 214 | *~ 215 | *.dbmdl 216 | *.dbproj.schemaview 217 | *.jfm 218 | *.pfx 219 | *.publishsettings 220 | orleans.codegen.cs 221 | 222 | # Including strong name files can present a security risk 223 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 224 | #*.snk 225 | 226 | # Since there are multiple workflows, uncomment next line to ignore bower_components 227 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 228 | #bower_components/ 229 | 230 | # RIA/Silverlight projects 231 | Generated_Code/ 232 | 233 | # Backup & report files from converting an old project file 234 | # to a newer Visual Studio version. Backup files are not needed, 235 | # because we have git ;-) 236 | _UpgradeReport_Files/ 237 | Backup*/ 238 | UpgradeLog*.XML 239 | UpgradeLog*.htm 240 | 241 | # SQL Server files 242 | *.mdf 243 | *.ldf 244 | *.ndf 245 | 246 | # Business Intelligence projects 247 | *.rdl.data 248 | *.bim.layout 249 | *.bim_*.settings 250 | 251 | # Microsoft Fakes 252 | FakesAssemblies/ 253 | 254 | # GhostDoc plugin setting file 255 | *.GhostDoc.xml 256 | 257 | # Node.js Tools for Visual Studio 258 | .ntvs_analysis.dat 259 | node_modules/ 260 | 261 | # TypeScript v1 declaration files 262 | typings/ 263 | 264 | # Visual Studio 6 build log 265 | *.plg 266 | 267 | # Visual Studio 6 workspace options file 268 | *.opt 269 | 270 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 271 | *.vbw 272 | 273 | # Visual Studio LightSwitch build output 274 | **/*.HTMLClient/GeneratedArtifacts 275 | **/*.DesktopClient/GeneratedArtifacts 276 | **/*.DesktopClient/ModelManifest.xml 277 | **/*.Server/GeneratedArtifacts 278 | **/*.Server/ModelManifest.xml 279 | _Pvt_Extensions 280 | 281 | # Paket dependency manager 282 | .paket/paket.exe 283 | paket-files/ 284 | 285 | # FAKE - F# Make 286 | .fake/ 287 | 288 | # JetBrains Rider 289 | .idea/ 290 | *.sln.iml 291 | 292 | # CodeRush 293 | .cr/ 294 | 295 | # Python Tools for Visual Studio (PTVS) 296 | __pycache__/ 297 | *.pyc 298 | 299 | # Cake - Uncomment if you are using it 300 | # tools/** 301 | # !tools/packages.config 302 | 303 | # Tabs Studio 304 | *.tss 305 | 306 | # Telerik's JustMock configuration file 307 | *.jmconfig 308 | 309 | # BizTalk build output 310 | *.btp.cs 311 | *.btm.cs 312 | *.odx.cs 313 | *.xsd.cs 314 | 315 | # OpenCover UI analysis results 316 | OpenCover/ 317 | 318 | # Azure Stream Analytics local run output 319 | ASALocalRun/ 320 | 321 | # MSBuild Binary and Structured Log 322 | *.binlog -------------------------------------------------------------------------------- /src/VerifyBot/Services/VerifyService.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using DL.GuildWars2Api; 3 | using DL.GuildWars2Api.Models.V2; 4 | using System; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | using VerifyBot.Models; 9 | 10 | namespace VerifyBot.Services 11 | { 12 | public class VerifyService 13 | { 14 | private const int APIKeyLength = 72; 15 | private static readonly Regex AccountNameApiKeyRegex = new Regex(@"\s*(.+?\.\d+)\s+(.*?-.*?-.*?-.*?-.*)\s*$"); 16 | private readonly UserStrings strings; 17 | 18 | public VerifyService(string accountId, string accountName, string apiKey, Manager manager, IUser requestor, UserStrings strings, IMessageChannel channel) 19 | { 20 | AccoutId = accountId; 21 | AccountName = accountName; 22 | APIKey = apiKey; 23 | Requestor = requestor; 24 | Channel = channel; 25 | Manager = manager; 26 | 27 | this.strings = strings; 28 | 29 | API = new ApiFacade(APIKey); 30 | 31 | HasValidCharacter = false; 32 | } 33 | 34 | public Account Account { get; private set; } 35 | 36 | public string AccoutId { get; } 37 | 38 | public string AccountName { get; } 39 | 40 | public string APIKey { get; } 41 | 42 | public IMessageChannel Channel { get; } 43 | 44 | public int World 45 | { 46 | get 47 | { 48 | return this.Account?.WorldId ?? -999; 49 | } 50 | } 51 | 52 | public bool IsValid => IsValidAccount && HasValidCharacter; 53 | 54 | public IUser Requestor { get; } 55 | 56 | private ApiFacade API { get; } 57 | 58 | private bool HasValidCharacter { get; set; } 59 | 60 | private bool IsValidAccount => Account != null; 61 | 62 | private Manager Manager { get; } 63 | 64 | public static VerifyService Create(string accountName, string apiKey, Manager manager, IUser requestor, UserStrings strings, IMessageChannel channel = null) 65 | { 66 | return new VerifyService(accountName, null, apiKey, manager, requestor, strings, channel); 67 | } 68 | 69 | public static async Task CreateFromRequestMessage(IMessage requestMessage, Manager manager, UserStrings strings) 70 | { 71 | var tokens = AccountNameApiKeyRegex.Split(requestMessage.Content.Trim()); 72 | 73 | if (tokens.Length != 4) 74 | { 75 | await requestMessage.Channel.SendMessageAsync(strings.ParseError); 76 | Console.WriteLine($"Could not verify {requestMessage.Author.Username} - Bad # of arguments"); 77 | return null; 78 | } 79 | 80 | if (tokens[2].Length != APIKeyLength) 81 | { 82 | await requestMessage.Channel.SendMessageAsync(strings.InvalidAPIKey); 83 | Console.WriteLine($"Could not verify {requestMessage.Author.Username} - Bad API Key"); 84 | return null; 85 | } 86 | 87 | return new VerifyService(null, tokens[1], tokens[2], manager, requestMessage.Author, strings, requestMessage.Channel); 88 | } 89 | 90 | public async Task SendMessageAsync(string message) 91 | { 92 | if (Channel == null) 93 | return null; // Task.FromResult(null); 94 | return await Channel.SendMessageAsync(message); 95 | } 96 | 97 | public async Task Validate(bool isReverify) 98 | { 99 | await ValidateAccount(isReverify); 100 | if (IsValidAccount) 101 | await ValidateCharacters(); 102 | } 103 | 104 | public async Task LoadAccount() 105 | { 106 | var account = await API.V2.Authenticated.GetAccountAsync(); 107 | 108 | if (account != null) 109 | { 110 | Account = account; 111 | } 112 | } 113 | 114 | private async Task ValidateAccount(bool isReverify) 115 | { 116 | try 117 | { 118 | var account = await API.V2.Authenticated.GetAccountAsync(); 119 | 120 | if (account == null) 121 | { 122 | await SendMessageAsync(this.strings.AccountNotInAPI); 123 | Console.WriteLine($"Could not verify {Requestor.Username} - Cannont access account in GW2 API."); 124 | return; 125 | } 126 | 127 | if (isReverify) 128 | { 129 | if (account.Id != AccoutId) 130 | { 131 | Console.WriteLine($"Could not verify {Requestor.Username} - API Key account does not match supplied account ID."); 132 | return; 133 | } 134 | } 135 | else 136 | { 137 | if (account.Name != AccountName) 138 | { 139 | await SendMessageAsync(this.strings.AccountNameDoesNotMatch); 140 | Console.WriteLine($"Could not verify {Requestor.Username} - API Key account does not match supplied account. (Case matters)"); 141 | return; 142 | } 143 | } 144 | 145 | if (!Manager.IsAccountOnOurWorld(account)) 146 | { 147 | await SendMessageAsync(this.strings.AccountNotOnServer); 148 | Console.WriteLine($"Could not verify {Requestor.Username} - Not on Server."); 149 | return; 150 | } 151 | 152 | Account = account; 153 | } 154 | catch (Exception ex) 155 | { 156 | Console.WriteLine($"Exception: {ex.Message}"); 157 | return; 158 | } 159 | } 160 | 161 | private async Task ValidateCharacters() 162 | { 163 | if (Account.Access.Count() == 0) 164 | { 165 | var characters = await API.V2.Authenticated.GetCharactersAsync(); 166 | 167 | var isWvWLevel = false; 168 | foreach (var character in characters) 169 | { 170 | var characterObj = await API.V2.Authenticated.GetCharacterAsync(character); 171 | 172 | if (characterObj.Level >= 60) 173 | { 174 | isWvWLevel = true; 175 | break; 176 | } 177 | } 178 | 179 | if (!isWvWLevel) 180 | { 181 | await SendMessageAsync(this.strings.NotValidLevel); 182 | Console.WriteLine($"Could not verify {Requestor.Username} - Not elgible for WvW."); 183 | return; 184 | } 185 | } 186 | 187 | HasValidCharacter = true; 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/VerifyBot/Program.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.WebSocket; 3 | using SimpleInjector; 4 | using System; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using VerifyBot.Factories; 9 | using VerifyBot.Models; 10 | using VerifyBot.Services; 11 | 12 | namespace VerifyBot 13 | { 14 | public class Program 15 | { 16 | private const int dayInterval = 86400000; 17 | private Container container; 18 | private Timer reminderTimer; 19 | private Timer reverifyTimer; 20 | 21 | private DiscordSocketClient _client; 22 | 23 | public async Task Run() 24 | { 25 | try 26 | { 27 | this.CheckIfDatabaseExists(); 28 | this.LoadContainerAsync(); 29 | 30 | Console.WriteLine("Creating new client"); 31 | 32 | _client = container.GetInstance(); 33 | 34 | _client.MessageReceived += MessageReceived; 35 | _client.UserJoined += UserJoined; 36 | _client.Disconnected += Disconnected; 37 | 38 | Console.WriteLine("client created & events wired"); 39 | 40 | this.reverifyTimer = new Timer(this.RunVerification, container.GetInstance(), dayInterval, dayInterval); 41 | this.reminderTimer = new Timer(this.RemindVerify, container.GetInstance(), dayInterval, dayInterval * 2); 42 | 43 | Console.WriteLine("Verifybot running"); 44 | 45 | while (true) 46 | { 47 | var line = Console.ReadLine(); 48 | 49 | if (line.Equals("reverify")) 50 | { 51 | await container.GetInstance().Process(); 52 | } 53 | 54 | if (line.Equals("stats")) 55 | { 56 | await container.GetInstance().GetStatistics(); 57 | } 58 | 59 | if (line.Equals("quit")) 60 | { 61 | return; 62 | } 63 | 64 | if (line.StartsWith("lookup ")) 65 | { 66 | var username = line.Replace("lookup ", ""); 67 | Console.WriteLine($"Searching for Discord accounts with the GW2 account name of {username}"); 68 | await container.GetInstance().LookupAsync(username); 69 | 70 | } 71 | } 72 | } 73 | catch (Exception ex) 74 | { 75 | Console.WriteLine($"Aplication crashing. Reason: {ex}"); 76 | } 77 | } 78 | 79 | private async Task Disconnected(Exception arg) 80 | { 81 | Console.WriteLine($"Client disconnected: {arg.Message}"); 82 | Environment.Exit(1); 83 | 84 | try 85 | { 86 | var config = container.GetInstance(); 87 | 88 | await _client.LoginAsync(TokenType.Bot, config.DiscordToken); 89 | await _client.StartAsync(); 90 | 91 | var ready = false; 92 | _client.Ready += () => 93 | { 94 | ready = true; 95 | return Task.CompletedTask; 96 | }; 97 | 98 | Console.WriteLine("Waiting for DiscordSocketClient to initialize..."); 99 | 100 | while (!ready) 101 | { 102 | await Task.Delay(1000); 103 | Console.WriteLine("Waiting..."); 104 | } 105 | 106 | Console.WriteLine(" DiscordSocketClient initialized."); 107 | } 108 | catch (Exception ex) 109 | { 110 | Console.WriteLine($"Serious exception, program will now close. Exception: {ex.Message}"); 111 | Environment.Exit(-1); 112 | } 113 | } 114 | 115 | private static void Main(string[] args) => new Program().Run().GetAwaiter().GetResult(); 116 | 117 | private void CheckIfDatabaseExists() 118 | { 119 | var path = System.IO.Path.Combine(AppContext.BaseDirectory, "Users.db"); 120 | 121 | if (!System.IO.File.Exists(path)) 122 | { 123 | Console.WriteLine("Database does not exist. Run the following command: dotnet ef database update"); 124 | throw new Exception("No Database"); 125 | } 126 | } 127 | 128 | private void LoadContainerAsync() 129 | { 130 | this.container = new Container(); 131 | 132 | //// Configuration services 133 | container.Register(ConfigurationFactory.Get, Lifestyle.Singleton); 134 | 135 | //// Client object 136 | container.Register(() => DiscordClientFactory.Get(ConfigurationFactory.Get()).Result, Lifestyle.Singleton); 137 | 138 | //// Userstrings 139 | container.Register(UserStringsFactory.Get, Lifestyle.Singleton); 140 | 141 | //// Manager service 142 | container.Register(Lifestyle.Transient); 143 | 144 | //// verify services 145 | container.Register(Lifestyle.Transient); 146 | container.Register(Lifestyle.Transient); 147 | container.Register(Lifestyle.Transient); 148 | container.Register(Lifestyle.Transient); 149 | 150 | container.Verify(); 151 | } 152 | 153 | private Task Log(LogMessage msg) 154 | { 155 | Console.WriteLine(msg.ToString()); 156 | return Task.CompletedTask; 157 | } 158 | 159 | private async Task MessageReceived(SocketMessage message) 160 | { 161 | try 162 | { 163 | if (message.Author.IsBot) 164 | { 165 | return; 166 | } 167 | 168 | if (message.Channel is IDMChannel) 169 | { 170 | await container.GetInstance().Process(message); 171 | } 172 | 173 | if (message.Channel is IGuildChannel && message.Content.Contains("!verify")) 174 | { 175 | if (message.Author is IGuildUser) 176 | { 177 | await container.GetInstance().SendInstructions(message.Author as IGuildUser); 178 | } 179 | } 180 | } 181 | catch (Exception ex) 182 | { 183 | Console.WriteLine($"Error occured while processing message: {ex.Message}"); 184 | } 185 | } 186 | 187 | private async void RemindVerify(object service) 188 | { 189 | var remind = service as RemindVerifyService; 190 | 191 | if (remind == null) 192 | { 193 | return; 194 | } 195 | 196 | await remind.Process(); 197 | } 198 | 199 | private async void RunVerification(object service) 200 | { 201 | var verify = service as ReverifyService; 202 | 203 | if (verify == null) 204 | { 205 | Console.WriteLine("Service Object is null"); 206 | return; 207 | } 208 | 209 | await verify.Process(); 210 | 211 | Environment.Exit(-1); 212 | } 213 | 214 | private async Task UserJoined(SocketGuildUser userCandidate) 215 | { 216 | try 217 | { 218 | var user = userCandidate as IGuildUser; 219 | 220 | if (user == null) 221 | { 222 | return; 223 | } 224 | 225 | var strings = this.container.GetInstance(); 226 | var pm = await user.GetOrCreateDMChannelAsync(); 227 | 228 | await pm.SendMessageAsync(strings.InitialMessage); 229 | } 230 | catch (Exception ex) 231 | { 232 | Console.WriteLine($"Error occured when sending initial message: {ex.Message}"); 233 | } 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/VerifyBot/Properties/PublishProfiles/publish-module.psm1: -------------------------------------------------------------------------------- 1 | # WARNING: DO NOT MODIFY this file. Visual Studio will override it. 2 | param() 3 | 4 | $script:AspNetPublishHandlers = @{} 5 | 6 | <# 7 | These settings can be overridden with environment variables. 8 | The name of the environment variable should use "Publish" as a 9 | prefix and the names below. For example: 10 | 11 | $env:PublishMSDeployUseChecksum = $true 12 | #> 13 | $global:AspNetPublishSettings = New-Object -TypeName PSCustomObject @{ 14 | MsdeployDefaultProperties = @{ 15 | 'MSDeployUseChecksum'=$false 16 | 'SkipExtraFilesOnServer'=$true 17 | 'retryAttempts' = 20 18 | 'EnableMSDeployBackup' = $false 19 | 'DeleteExistingFiles' = $false 20 | 'AllowUntrustedCertificate'= $false 21 | 'MSDeployPackageContentFoldername'='website\' 22 | 'EnvironmentName' = 'Production' 23 | 'AuthType'='Basic' 24 | 'MSDeployPublishMethod'='WMSVC' 25 | } 26 | } 27 | 28 | function InternalOverrideSettingsFromEnv{ 29 | [cmdletbinding()] 30 | param( 31 | [Parameter(Position=0)] 32 | [object[]]$settings = ($global:AspNetPublishSettings,$global:AspNetPublishSettings.MsdeployDefaultProperties), 33 | 34 | [Parameter(Position=1)] 35 | [string]$prefix = 'Publish' 36 | ) 37 | process{ 38 | foreach($settingsObj in $settings){ 39 | if($settingsObj -eq $null){ 40 | continue 41 | } 42 | 43 | $settingNames = $null 44 | if($settingsObj -is [hashtable]){ 45 | $settingNames = $settingsObj.Keys 46 | } 47 | else{ 48 | $settingNames = ($settingsObj | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name) 49 | 50 | } 51 | 52 | foreach($name in @($settingNames)){ 53 | $fullname = ('{0}{1}' -f $prefix,$name) 54 | if(Test-Path "env:$fullname"){ 55 | $settingsObj.$name = ((get-childitem "env:$fullname").Value) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | InternalOverrideSettingsFromEnv -prefix 'Publish' -settings $global:AspNetPublishSettings,$global:AspNetPublishSettings.MsdeployDefaultProperties 63 | 64 | function Register-AspnetPublishHandler{ 65 | [cmdletbinding()] 66 | param( 67 | [Parameter(Mandatory=$true,Position=0)] 68 | $name, 69 | [Parameter(Mandatory=$true,Position=1)] 70 | [ScriptBlock]$handler, 71 | [switch]$force 72 | ) 73 | process{ 74 | if(!($script:AspNetPublishHandlers[$name]) -or $force ){ 75 | 'Adding handler for [{0}]' -f $name | Write-Verbose 76 | $script:AspNetPublishHandlers[$name] = $handler 77 | } 78 | elseif(!($force)){ 79 | 'Ignoring call to Register-AspnetPublishHandler for [name={0}], because a handler with that name exists and -force was not passed.' -f $name | Write-Verbose 80 | } 81 | } 82 | } 83 | 84 | function Get-AspnetPublishHandler{ 85 | [cmdletbinding()] 86 | param( 87 | [Parameter(Mandatory=$true,Position=0)] 88 | $name 89 | ) 90 | process{ 91 | $foundHandler = $script:AspNetPublishHandlers[$name] 92 | 93 | if(!$foundHandler){ 94 | throw ('AspnetPublishHandler with name "{0}" was not found' -f $name) 95 | } 96 | 97 | $foundHandler 98 | } 99 | } 100 | 101 | function GetInternal-ExcludeFilesArg{ 102 | [cmdletbinding()] 103 | param( 104 | $publishProperties 105 | ) 106 | process{ 107 | $excludeFiles = $publishProperties['ExcludeFiles'] 108 | foreach($exclude in $excludeFiles){ 109 | if($exclude){ 110 | [string]$objName = $exclude['objectname'] 111 | 112 | if([string]::IsNullOrEmpty($objName)){ 113 | $objName = 'filePath' 114 | } 115 | 116 | $excludePath = $exclude['absolutepath'] 117 | 118 | # output the result to the return list 119 | ('-skip:objectName={0},absolutePath=''{1}''' -f $objName, $excludePath) 120 | } 121 | } 122 | } 123 | } 124 | 125 | function GetInternal-ReplacementsMSDeployArgs{ 126 | [cmdletbinding()] 127 | param( 128 | $publishProperties 129 | ) 130 | process{ 131 | foreach($replace in ($publishProperties['Replacements'])){ 132 | if($replace){ 133 | $typeValue = $replace['type'] 134 | if(!$typeValue){ $typeValue = 'TextFile' } 135 | 136 | $file = $replace['file'] 137 | $match = $replace['match'] 138 | $newValue = $replace['newValue'] 139 | 140 | if($file -and $match -and $newValue){ 141 | $setParam = ('-setParam:type={0},scope={1},match={2},value={3}' -f $typeValue,$file, $match,$newValue) 142 | 'Adding setparam [{0}]' -f $setParam | Write-Verbose 143 | 144 | # return it 145 | $setParam 146 | } 147 | else{ 148 | 'Skipping replacement because its missing a required value.[file="{0}",match="{1}",newValue="{2}"]' -f $file,$match,$newValue | Write-Verbose 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | <# 156 | .SYNOPSIS 157 | Returns an array of msdeploy arguments that are used across different providers. 158 | For example this will handle useChecksum, AppOffline etc. 159 | This will also add default properties if they are missing. 160 | #> 161 | function GetInternal-SharedMSDeployParametersFrom{ 162 | [cmdletbinding()] 163 | param( 164 | [Parameter(Mandatory=$true,Position=0)] 165 | [HashTable]$publishProperties, 166 | [Parameter(Mandatory=$true,Position=1)] 167 | [System.IO.FileInfo]$packOutput 168 | ) 169 | process{ 170 | $sharedArgs = New-Object psobject -Property @{ 171 | ExtraArgs = @() 172 | DestFragment = '' 173 | EFMigrationData = @{} 174 | } 175 | 176 | # add default properties if they are missing 177 | foreach($propName in $global:AspNetPublishSettings.MsdeployDefaultProperties.Keys){ 178 | if($publishProperties["$propName"] -eq $null){ 179 | $defValue = $global:AspNetPublishSettings.MsdeployDefaultProperties["$propName"] 180 | 'Adding default property to publishProperties ["{0}"="{1}"]' -f $propName,$defValue | Write-Verbose 181 | $publishProperties["$propName"] = $defValue 182 | } 183 | } 184 | 185 | if($publishProperties['MSDeployUseChecksum'] -eq $true){ 186 | $sharedArgs.ExtraArgs += '-usechecksum' 187 | } 188 | 189 | if($publishProperties['EnableMSDeployAppOffline'] -eq $true){ 190 | $sharedArgs.ExtraArgs += '-enablerule:AppOffline' 191 | } 192 | 193 | if($publishProperties['WebPublishMethod'] -eq 'MSDeploy'){ 194 | if($publishProperties['SkipExtraFilesOnServer'] -eq $true){ 195 | $sharedArgs.ExtraArgs += '-enableRule:DoNotDeleteRule' 196 | } 197 | } 198 | 199 | if($publishProperties['WebPublishMethod'] -eq 'FileSystem'){ 200 | if($publishProperties['DeleteExistingFiles'] -eq $false){ 201 | $sharedArgs.ExtraArgs += '-enableRule:DoNotDeleteRule' 202 | } 203 | } 204 | 205 | if($publishProperties['retryAttempts']){ 206 | $sharedArgs.ExtraArgs += ('-retryAttempts:{0}' -f ([int]$publishProperties['retryAttempts'])) 207 | } 208 | 209 | if($publishProperties['EncryptWebConfig'] -eq $true){ 210 | $sharedArgs.ExtraArgs += '-EnableRule:EncryptWebConfig' 211 | } 212 | 213 | if($publishProperties['EnableMSDeployBackup'] -eq $false){ 214 | $sharedArgs.ExtraArgs += '-disablerule:BackupRule' 215 | } 216 | 217 | if($publishProperties['AllowUntrustedCertificate'] -eq $true){ 218 | $sharedArgs.ExtraArgs += '-allowUntrusted' 219 | } 220 | 221 | # add excludes 222 | $sharedArgs.ExtraArgs += (GetInternal-ExcludeFilesArg -publishProperties $publishProperties) 223 | # add replacements 224 | $sharedArgs.ExtraArgs += (GetInternal-ReplacementsMSDeployArgs -publishProperties $publishProperties) 225 | 226 | # add EF Migration 227 | if (($publishProperties['EfMigrations'] -ne $null) -and $publishProperties['EfMigrations'].Count -gt 0){ 228 | if (!(Test-Path -Path $publishProperties['ProjectPath'])) { 229 | throw 'ProjectPath property needs to be defined in the pubxml for EF migration.' 230 | } 231 | try { 232 | # generate T-SQL files 233 | $EFSqlFiles = GenerateInternal-EFMigrationScripts -projectPath $publishProperties['ProjectPath'] -packOutput $packOutput -EFMigrations $publishProperties['EfMigrations'] 234 | $sharedArgs.EFMigrationData.Add('EFSqlFiles',$EFSqlFiles) 235 | } 236 | catch { 237 | throw ('An error occurred while generating EF migrations. {0} {1}' -f $_.Exception,(Get-PSCallStack)) 238 | } 239 | } 240 | # add connection string update 241 | if (($publishProperties['DestinationConnectionStrings'] -ne $null) -and $publishProperties['DestinationConnectionStrings'].Count -gt 0) { 242 | try { 243 | # create/update appsettings.[environment].json 244 | GenerateInternal-AppSettingsFile -packOutput $packOutput -environmentName $publishProperties['EnvironmentName'] -connectionStrings $publishProperties['DestinationConnectionStrings'] 245 | } 246 | catch { 247 | throw ('An error occurred while generating the publish appsettings file. {0} {1}' -f $_.Exception,(Get-PSCallStack)) 248 | } 249 | } 250 | 251 | if(-not [string]::IsNullOrWhiteSpace($publishProperties['ProjectGuid'])) { 252 | AddInternal-ProjectGuidToWebConfig -publishProperties $publishProperties -packOutput $packOutput 253 | } 254 | 255 | # return the args 256 | $sharedArgs 257 | } 258 | } 259 | 260 | <# 261 | .SYNOPSIS 262 | This will publish the folder based on the properties in $publishProperties 263 | 264 | .PARAMETER publishProperties 265 | This is a hashtable containing the publish properties. See the examples here for more info on how to use this parameter. 266 | 267 | .PARAMETER packOutput 268 | The folder path to the output of the dnu publish command. This folder contains the files 269 | that will be published. 270 | 271 | .PARAMETER pubProfilePath 272 | Path to a publish profile (.pubxml file) to import publish properties from. If the same property exists in 273 | publishProperties and the publish profile then publishProperties will win. 274 | 275 | .EXAMPLE 276 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 277 | 'WebPublishMethod'='MSDeploy' 278 | 'MSDeployServiceURL'='contoso.scm.azurewebsites.net:443';` 279 | 'DeployIisAppPath'='contoso';'Username'='$contoso';'Password'="$env:PublishPwd"} 280 | 281 | .EXAMPLE 282 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 283 | 'WebPublishMethod'='FileSystem' 284 | 'publishUrl'="$publishDest" 285 | } 286 | 287 | .EXAMPLE 288 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 289 | 'WebPublishMethod'='MSDeploy' 290 | 'MSDeployServiceURL'='contoso.scm.azurewebsites.net:443';` 291 | 'DeployIisAppPath'='contoso';'Username'='$contoso';'Password'="$env:PublishPwd" 292 | 'ExcludeFiles'=@( 293 | @{'absolutepath'='test.txt'}, 294 | @{'absolutepath'='references.js'} 295 | )} 296 | 297 | .EXAMPLE 298 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 299 | 'WebPublishMethod'='FileSystem' 300 | 'publishUrl'="$publishDest" 301 | 'ExcludeFiles'=@( 302 | @{'absolutepath'='test.txt'}, 303 | @{'absolutepath'='_references.js'}) 304 | 'Replacements' = @( 305 | @{'file'='test.txt$';'match'='REPLACEME';'newValue'='updatedValue'}) 306 | } 307 | 308 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 309 | 'WebPublishMethod'='FileSystem' 310 | 'publishUrl'="$publishDest" 311 | 'ExcludeFiles'=@( 312 | @{'absolutepath'='test.txt'}, 313 | @{'absolutepath'='c:\\full\\path\\ok\\as\\well\\_references.js'}) 314 | 'Replacements' = @( 315 | @{'file'='test.txt$';'match'='REPLACEME';'newValue'='updatedValue'}) 316 | } 317 | 318 | .EXAMPLE 319 | Publish-AspNet -packOutput $packOutput -publishProperties @{ 320 | 'WebPublishMethod'='FileSystem' 321 | 'publishUrl'="$publishDest" 322 | 'EnableMSDeployAppOffline'='true' 323 | 'AppOfflineTemplate'='offline-template.html' 324 | 'MSDeployUseChecksum'='true' 325 | } 326 | #> 327 | function Publish-AspNet{ 328 | param( 329 | [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 330 | [hashtable]$publishProperties = @{}, 331 | 332 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 333 | [System.IO.FileInfo]$packOutput, 334 | 335 | [Parameter(Position=2,ValueFromPipelineByPropertyName=$true)] 336 | [System.IO.FileInfo]$pubProfilePath 337 | ) 338 | process{ 339 | if($publishProperties['WebPublishMethodOverride']){ 340 | 'Overriding publish method from $publishProperties[''WebPublishMethodOverride''] to [{0}]' -f ($publishProperties['WebPublishMethodOverride']) | Write-Verbose 341 | $publishProperties['WebPublishMethod'] = $publishProperties['WebPublishMethodOverride'] 342 | } 343 | 344 | if(-not [string]::IsNullOrWhiteSpace($pubProfilePath)){ 345 | $profileProperties = Get-PropertiesFromPublishProfile -filepath $pubProfilePath 346 | foreach($key in $profileProperties.Keys){ 347 | if(-not ($publishProperties.ContainsKey($key))){ 348 | 'Adding properties from publish profile [''{0}''=''{1}'']' -f $key,$profileProperties[$key] | Write-Verbose 349 | $publishProperties.Add($key,$profileProperties[$key]) 350 | } 351 | } 352 | } 353 | 354 | if(!([System.IO.Path]::IsPathRooted($packOutput))){ 355 | $packOutput = [System.IO.Path]::GetFullPath((Join-Path $pwd $packOutput)) 356 | } 357 | 358 | $pubMethod = $publishProperties['WebPublishMethod'] 359 | 'Publishing with publish method [{0}]' -f $pubMethod | Write-Output 360 | 361 | # get the handler based on WebPublishMethod, and call it. 362 | &(Get-AspnetPublishHandler -name $pubMethod) $publishProperties $packOutput 363 | } 364 | } 365 | 366 | <# 367 | .SYNOPSIS 368 | 369 | Inputs: 370 | 371 | Example of $xmlDocument: '' 372 | Example of $providerDataArray: 373 | 374 | [System.Collections.ArrayList]$providerDataArray = @() 375 | 376 | $iisAppSourceKeyValue=@{"iisApp" = @{"path"='c:\temp\pathtofiles';"appOfflineTemplate" ='offline-template.html'}} 377 | $providerDataArray.Add($iisAppSourceKeyValue) 378 | 379 | $dbfullsqlKeyValue=@{"dbfullsql" = @{"path"="c:\Temp\PathToSqlFile"}} 380 | $providerDataArray.Add($dbfullsqlKeyValue) 381 | 382 | $dbfullsqlKeyValue=@{"dbfullsql" = @{"path"="c:\Temp\PathToSqlFile2"}} 383 | $providerDataArray.Add($dbfullsqlKeyValue) 384 | 385 | Manifest File content: 386 | 387 | 388 | 389 | 390 | 391 | 392 | #> 393 | function AddInternal-ProviderDataToManifest { 394 | [cmdletbinding()] 395 | param( 396 | [Parameter(Mandatory=$true, Position=0)] 397 | [XML]$xmlDocument, 398 | [Parameter(Position=1)] 399 | [System.Collections.ArrayList]$providerDataArray 400 | ) 401 | process { 402 | $siteNode = $xmlDocument.SelectSingleNode("/sitemanifest") 403 | if ($siteNode -eq $null) { 404 | throw 'sitemanifest element is missing in the xml object' 405 | } 406 | foreach ($providerData in $providerDataArray) { 407 | foreach ($providerName in $providerData.Keys) { 408 | $providerValue = $providerData[$providerName] 409 | $xmlNode = $xmlDocument.CreateElement($providerName) 410 | foreach ($providerValueKey in $providerValue.Keys) { 411 | $xmlNode.SetAttribute($providerValueKey, $providerValue[$providerValueKey]) | Out-Null 412 | } 413 | $siteNode.AppendChild($xmlNode) | Out-Null 414 | } 415 | } 416 | } 417 | } 418 | 419 | function AddInternal-ProjectGuidToWebConfig { 420 | [cmdletbinding()] 421 | param( 422 | [Parameter(Position=0)] 423 | [HashTable]$publishProperties, 424 | [Parameter(Position=1)] 425 | [System.IO.FileInfo]$packOutput 426 | ) 427 | process { 428 | try { 429 | [Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") | Out-Null 430 | $webConfigPath = Join-Path $packOutput 'web.config' 431 | $projectGuidCommentValue = 'ProjectGuid: {0}' -f $publishProperties['ProjectGuid'] 432 | $xDoc = [System.Xml.Linq.XDocument]::Load($webConfigPath) 433 | $allNodes = $xDoc.DescendantNodes() 434 | $projectGuidComment = $allNodes | Where-Object { $_.NodeType -eq [System.Xml.XmlNodeType]::Comment -and $_.Value -eq $projectGuidCommentValue } | Select -First 1 435 | if($projectGuidComment -ne $null) { 436 | if($publishProperties['IgnoreProjectGuid'] -eq $true) { 437 | $projectGuidComment.Remove() | Out-Null 438 | $xDoc.Save($webConfigPath) | Out-Null 439 | } 440 | } 441 | else { 442 | if(-not ($publishProperties['IgnoreProjectGuid'] -eq $true)) { 443 | $projectGuidComment = New-Object -TypeName System.Xml.Linq.XComment -ArgumentList $projectGuidCommentValue 444 | $xDoc.LastNode.AddAfterSelf($projectGuidComment) | Out-Null 445 | $xDoc.Save($webConfigPath) | Out-Null 446 | } 447 | } 448 | } 449 | catch { 450 | } 451 | } 452 | } 453 | 454 | <# 455 | .SYNOPSIS 456 | 457 | Example of $EFMigrations: 458 | $EFMigrations = @{'CarContext'='Car Context ConnectionString';'MovieContext'='Movie Context Connection String'} 459 | 460 | #> 461 | 462 | function GenerateInternal-EFMigrationScripts { 463 | [cmdletbinding()] 464 | param( 465 | [Parameter(Mandatory=$true,Position=0)] 466 | [System.IO.FileInfo]$projectPath, 467 | [Parameter(Mandatory=$true,Position=1)] 468 | [System.IO.FileInfo]$packOutput, 469 | [Parameter(Position=2)] 470 | [HashTable]$EFMigrations 471 | ) 472 | process { 473 | $files = @{} 474 | $dotnetExePath = GetInternal-DotNetExePath 475 | foreach ($dbContextName in $EFMigrations.Keys) { 476 | try 477 | { 478 | $tempDir = GetInternal-PublishTempPath -packOutput $packOutput 479 | $efScriptFile = Join-Path $tempDir ('{0}.sql' -f $dbContextName) 480 | $arg = ('ef migrations script --idempotent --output {0} --context {1}' -f 481 | $efScriptFile, 482 | $dbContextName) 483 | 484 | Execute-Command $dotnetExePath $arg $projectPath | Out-Null 485 | if (Test-Path -Path $efScriptFile) { 486 | if (!($files.ContainsKey($dbContextName))) { 487 | $files.Add($dbContextName, $efScriptFile) | Out-Null 488 | } 489 | } 490 | } 491 | catch 492 | { 493 | throw 'error occured when executing dotnet.exe to generate EF T-SQL file' 494 | } 495 | } 496 | # return files object 497 | $files 498 | } 499 | } 500 | 501 | <# 502 | .SYNOPSIS 503 | 504 | Example of $connectionStrings: 505 | $connectionStrings = @{'DefaultConnection'='Default ConnectionString';'CarConnection'='Car Connection String'} 506 | 507 | #> 508 | function GenerateInternal-AppSettingsFile { 509 | [cmdletbinding()] 510 | param( 511 | [Parameter(Mandatory = $true,Position=0)] 512 | [System.IO.FileInfo]$packOutput, 513 | [Parameter(Mandatory = $true,Position=1)] 514 | [string]$environmentName, 515 | [Parameter(Position=2)] 516 | [HashTable]$connectionStrings 517 | ) 518 | process { 519 | $configProdJsonFile = 'appsettings.{0}.json' -f $environmentName 520 | $configProdJsonFilePath = Join-Path -Path $packOutput -ChildPath $configProdJsonFile 521 | 522 | if ([string]::IsNullOrEmpty($configProdJsonFilePath)) { 523 | throw ('The path of {0} is empty' -f $configProdJsonFilePath) 524 | } 525 | 526 | if(!(Test-Path -Path $configProdJsonFilePath)) { 527 | # create new file 528 | '{}' | out-file -encoding utf8 -filePath $configProdJsonFilePath -Force 529 | } 530 | 531 | $jsonObj = ConvertFrom-Json -InputObject (Get-Content -Path $configProdJsonFilePath -Raw) 532 | # update when there exists one or more connection strings 533 | if ($connectionStrings -ne $null) { 534 | foreach ($name in $connectionStrings.Keys) { 535 | #check for hierarchy style 536 | if ($jsonObj.ConnectionStrings.$name) { 537 | $jsonObj.ConnectionStrings.$name = $connectionStrings[$name] 538 | continue 539 | } 540 | #check for horizontal style 541 | $horizontalName = 'ConnectionStrings.{0}:' -f $name 542 | if ($jsonObj.$horizontalName) { 543 | $jsonObj.$horizontalName = $connectionStrings[$name] 544 | continue 545 | } 546 | # create new one 547 | if (!($jsonObj.ConnectionStrings)) { 548 | $contentForDefaultConnection = '{}' 549 | $jsonObj | Add-Member -name 'ConnectionStrings' -value (ConvertFrom-Json -InputObject $contentForDefaultConnection) -MemberType NoteProperty | Out-Null 550 | } 551 | if (!($jsonObj.ConnectionStrings.$name)) { 552 | $jsonObj.ConnectionStrings | Add-Member -name $name -value $connectionStrings[$name] -MemberType NoteProperty | Out-Null 553 | } 554 | } 555 | } 556 | 557 | $jsonObj | ConvertTo-Json | out-file -encoding utf8 -filePath $configProdJsonFilePath -Force 558 | 559 | #return the path of config.[environment].json 560 | $configProdJsonFilePath 561 | } 562 | } 563 | 564 | <# 565 | .SYNOPSIS 566 | 567 | Inputs: 568 | Example of $providerDataArray: 569 | 570 | [System.Collections.ArrayList]$providerDataArray = @() 571 | 572 | $iisAppSourceKeyValue=@{"iisApp" = @{"path"='c:\temp\pathtofiles';"appOfflineTemplate" ='offline-template.html'}} 573 | $providerDataArray.Add($iisAppSourceKeyValue) 574 | 575 | $dbfullsqlKeyValue=@{"dbfullsql" = @{"path"="c:\Temp\PathToSqlFile"}} 576 | $providerDataArray.Add($dbfullsqlKeyValue) 577 | 578 | $dbfullsqlKeyValue=@{"dbfullsql" = @{"path"="c:\Temp\PathToSqlFile2"}} 579 | $providerDataArray.Add($dbfullsqlKeyValue) 580 | 581 | Manifest File content: 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | #> 590 | 591 | function GenerateInternal-ManifestFile { 592 | [cmdletbinding()] 593 | param( 594 | [Parameter(Mandatory=$true,Position=0)] 595 | [System.IO.FileInfo]$packOutput, 596 | [Parameter(Mandatory=$true,Position=1)] 597 | $publishProperties, 598 | [Parameter(Mandatory=$true,Position=2)] 599 | [System.Collections.ArrayList]$providerDataArray, 600 | [Parameter(Mandatory=$true,Position=3)] 601 | [ValidateNotNull()] 602 | $manifestFileName 603 | ) 604 | process{ 605 | $xmlDocument = [xml]'' 606 | AddInternal-ProviderDataToManifest -xmlDocument $xmlDocument -providerDataArray $providerDataArray | Out-Null 607 | $publishTempDir = GetInternal-PublishTempPath -packOutput $packOutput 608 | $XMLFile = Join-Path $publishTempDir $manifestFileName 609 | $xmlDocument.OuterXml | out-file -encoding utf8 -filePath $XMLFile -Force 610 | 611 | # return 612 | [System.IO.FileInfo]$XMLFile 613 | } 614 | } 615 | 616 | function GetInternal-PublishTempPath { 617 | [cmdletbinding()] 618 | param( 619 | [Parameter(Mandatory=$true, Position=0)] 620 | [System.IO.FileInfo]$packOutput 621 | ) 622 | process { 623 | $tempDir = [io.path]::GetTempPath() 624 | $packOutputFolderName = Split-Path $packOutput -Leaf 625 | $publishTempDir = [io.path]::combine($tempDir,'PublishTemp','obj',$packOutputFolderName) 626 | if (!(Test-Path -Path $publishTempDir)) { 627 | New-Item -Path $publishTempDir -type directory | Out-Null 628 | } 629 | # return 630 | [System.IO.FileInfo]$publishTempDir 631 | } 632 | } 633 | 634 | function Publish-AspNetMSDeploy{ 635 | param( 636 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 637 | $publishProperties, 638 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 639 | $packOutput 640 | ) 641 | process{ 642 | if($publishProperties){ 643 | $publishPwd = $publishProperties['Password'] 644 | 645 | $sharedArgs = GetInternal-SharedMSDeployParametersFrom -publishProperties $publishProperties -packOutput $packOutput 646 | $iisAppPath = $publishProperties['DeployIisAppPath'] 647 | 648 | # create source manifest 649 | 650 | # e.g 651 | # 652 | # 653 | # 654 | # 655 | # 656 | # 657 | 658 | [System.Collections.ArrayList]$providerDataArray = @() 659 | $iisAppValues = @{"path"=$packOutput}; 660 | $iisAppSourceKeyValue=@{"iisApp" = $iisAppValues} 661 | $providerDataArray.Add($iisAppSourceKeyValue) | Out-Null 662 | 663 | if ($sharedArgs.EFMigrationData -ne $null -and $sharedArgs.EFMigrationData.Contains('EFSqlFiles')) { 664 | foreach ($sqlFile in $sharedArgs.EFMigrationData['EFSqlFiles'].Values) { 665 | $dbFullSqlSourceKeyValue=@{"dbFullSql" = @{"path"=$sqlFile}} 666 | $providerDataArray.Add($dbFullSqlSourceKeyValue) | Out-Null 667 | } 668 | } 669 | 670 | [System.IO.FileInfo]$sourceXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'SourceManifest.xml' 671 | 672 | $providerDataArray.Clear() | Out-Null 673 | # create destination manifest 674 | 675 | # e.g 676 | # 677 | # 678 | # 679 | # 680 | # 681 | 682 | $iisAppValues = @{"path"=$iisAppPath}; 683 | if(-not [string]::IsNullOrWhiteSpace($publishProperties['AppOfflineTemplate'])){ 684 | $iisAppValues.Add("appOfflineTemplate", $publishProperties['AppOfflineTemplate']) | Out-Null 685 | } 686 | 687 | $iisAppDestinationKeyValue=@{"iisApp" = $iisAppValues} 688 | $providerDataArray.Add($iisAppDestinationKeyValue) | Out-Null 689 | 690 | if ($publishProperties['EfMigrations'] -ne $null -and $publishProperties['EfMigrations'].Count -gt 0) { 691 | foreach ($connectionString in $publishProperties['EfMigrations'].Values) { 692 | $dbFullSqlDestinationKeyValue=@{"dbFullSql" = @{"path"=$connectionString}} 693 | $providerDataArray.Add($dbFullSqlDestinationKeyValue) | Out-Null 694 | } 695 | } 696 | 697 | 698 | [System.IO.FileInfo]$destXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'DestinationManifest.xml' 699 | 700 | <# 701 | "C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe" 702 | -source:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\SourceManifest.xml' 703 | -dest:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\DestManifest.xml',ComputerName='https://contoso.scm.azurewebsites.net/msdeploy.axd',UserName='$contoso',Password='',IncludeAcls='False',AuthType='Basic' 704 | -verb:sync 705 | -enableRule:DoNotDeleteRule 706 | -retryAttempts=2" 707 | #> 708 | 709 | if(-not [string]::IsNullOrWhiteSpace($publishProperties['MSDeployPublishMethod'])){ 710 | $serviceMethod = $publishProperties['MSDeployPublishMethod'] 711 | } 712 | 713 | $msdeployComputerName= InternalNormalize-MSDeployUrl -serviceUrl $publishProperties['MSDeployServiceURL'] -siteName $iisAppPath -serviceMethod $publishProperties['MSDeployPublishMethod'] 714 | if($publishProperties['UseMSDeployServiceURLAsIs'] -eq $true){ 715 | $msdeployComputerName = $publishProperties['MSDeployServiceURL'] 716 | } 717 | 718 | $publishArgs = @() 719 | #use manifest to publish 720 | $publishArgs += ('-source:manifest=''{0}''' -f $sourceXMLFile.FullName) 721 | $publishArgs += ('-dest:manifest=''{0}'',ComputerName=''{1}'',UserName=''{2}'',Password=''{3}'',IncludeAcls=''False'',AuthType=''{4}''{5}' -f 722 | $destXMLFile.FullName, 723 | $msdeployComputerName, 724 | $publishProperties['UserName'], 725 | $publishPwd, 726 | $publishProperties['AuthType'], 727 | $sharedArgs.DestFragment) 728 | $publishArgs += '-verb:sync' 729 | $publishArgs += $sharedArgs.ExtraArgs 730 | 731 | $command = '"{0}" {1}' -f (Get-MSDeploy),($publishArgs -join ' ') 732 | 733 | if (! [String]::IsNullOrEmpty($publishPwd)) { 734 | $command.Replace($publishPwd,'{PASSWORD-REMOVED-FROM-LOG}') | Print-CommandString 735 | } 736 | Execute-Command -exePath (Get-MSDeploy) -arguments ($publishArgs -join ' ') 737 | } 738 | else{ 739 | throw 'publishProperties is empty, cannot publish' 740 | } 741 | } 742 | } 743 | 744 | function Escape-TextForRegularExpressions{ 745 | [cmdletbinding()] 746 | param( 747 | [Parameter(Position=0,Mandatory=$true)] 748 | [string]$text 749 | ) 750 | process{ 751 | [regex]::Escape($text) 752 | } 753 | } 754 | 755 | function Publish-AspNetMSDeployPackage{ 756 | param( 757 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 758 | $publishProperties, 759 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 760 | $packOutput 761 | ) 762 | process{ 763 | if($publishProperties){ 764 | $packageDestinationFilepath = $publishProperties['DesktopBuildPackageLocation'] 765 | 766 | if(!$packageDestinationFilepath){ 767 | throw ('The package destination property (DesktopBuildPackageLocation) was not found in the publish properties') 768 | } 769 | 770 | if(!([System.IO.Path]::IsPathRooted($packageDestinationFilepath))){ 771 | $packageDestinationFilepath = [System.IO.Path]::GetFullPath((Join-Path $pwd $packageDestinationFilepath)) 772 | } 773 | 774 | # if the dir doesn't exist create it 775 | $pkgDir = ((new-object -typename System.IO.FileInfo($packageDestinationFilepath)).Directory) 776 | if(!(Test-Path -Path $pkgDir)) { 777 | New-Item $pkgDir -type Directory | Out-Null 778 | } 779 | 780 | <# 781 | "C:\Program Files (x86)\IIS\Microsoft Web Deploy V3\msdeploy.exe" 782 | -source:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\SourceManifest.xml' 783 | -dest:package=c:\temp\path\contosoweb.zip 784 | -verb:sync 785 | -enableRule:DoNotDeleteRule 786 | -retryAttempts=2 787 | #> 788 | 789 | $sharedArgs = GetInternal-SharedMSDeployParametersFrom -publishProperties $publishProperties -packOutput $packOutput 790 | 791 | # create source manifest 792 | 793 | # e.g 794 | # 795 | # 796 | # 797 | # 798 | 799 | [System.Collections.ArrayList]$providerDataArray = @() 800 | $iisAppSourceKeyValue=@{"iisApp" = @{"path"=$packOutput}} 801 | $providerDataArray.Add($iisAppSourceKeyValue) | Out-Null 802 | 803 | [System.IO.FileInfo]$sourceXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'SourceManifest.xml' 804 | 805 | $publishArgs = @() 806 | $publishArgs += ('-source:manifest=''{0}''' -f $sourceXMLFile.FullName) 807 | $publishArgs += ('-dest:package=''{0}''' -f $packageDestinationFilepath) 808 | $publishArgs += '-verb:sync' 809 | $packageContentFolder = $publishProperties['MSDeployPackageContentFoldername'] 810 | if(!$packageContentFolder){ $packageContentFolder = 'website' } 811 | $publishArgs += ('-replace:match=''{0}'',replace=''{1}''' -f (Escape-TextForRegularExpressions $packOutput), $packageContentFolder ) 812 | $publishArgs += $sharedArgs.ExtraArgs 813 | 814 | $command = '"{0}" {1}' -f (Get-MSDeploy),($publishArgs -join ' ') 815 | $command | Print-CommandString 816 | Execute-Command -exePath (Get-MSDeploy) -arguments ($publishArgs -join ' ') 817 | } 818 | else{ 819 | throw 'publishProperties is empty, cannot publish' 820 | } 821 | } 822 | } 823 | 824 | function Publish-AspNetFileSystem{ 825 | param( 826 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 827 | $publishProperties, 828 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 829 | $packOutput 830 | ) 831 | process{ 832 | $pubOut = $publishProperties['publishUrl'] 833 | 834 | if([string]::IsNullOrWhiteSpace($pubOut)){ 835 | throw ('publishUrl is a required property for FileSystem publish but it was empty.') 836 | } 837 | 838 | # if it's a relative path then update it to a full path 839 | if(!([System.IO.Path]::IsPathRooted($pubOut))){ 840 | $pubOut = [System.IO.Path]::GetFullPath((Join-Path $pwd $pubOut)) 841 | $publishProperties['publishUrl'] = "$pubOut" 842 | } 843 | 844 | 'Publishing files to {0}' -f $pubOut | Write-Output 845 | 846 | # we use msdeploy.exe because it supports incremental publish/skips/replacements/etc 847 | # msdeploy.exe -verb:sync -source:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\SourceManifest.xml' -dest:manifest='C:\Users\testuser\AppData\Local\Temp\PublishTemp\obj\DestManifest.xml' 848 | 849 | $sharedArgs = GetInternal-SharedMSDeployParametersFrom -publishProperties $publishProperties -packOutput $packOutput 850 | 851 | # create source manifest 852 | 853 | # e.g 854 | # 855 | # 856 | # 857 | # 858 | 859 | [System.Collections.ArrayList]$providerDataArray = @() 860 | $contentPathValues = @{"path"=$packOutput}; 861 | $contentPathSourceKeyValue=@{"contentPath" = $contentPathValues} 862 | $providerDataArray.Add($contentPathSourceKeyValue) | Out-Null 863 | 864 | [System.IO.FileInfo]$sourceXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'SourceManifest.xml' 865 | 866 | $providerDataArray.Clear() | Out-Null 867 | # create destination manifest 868 | 869 | # e.g 870 | # 871 | # 872 | # 873 | $contentPathValues = @{"path"=$publishProperties['publishUrl']}; 874 | if(-not [string]::IsNullOrWhiteSpace($publishProperties['AppOfflineTemplate'])){ 875 | $contentPathValues.Add("appOfflineTemplate", $publishProperties['AppOfflineTemplate']) | Out-Null 876 | } 877 | $contentPathDestinationKeyValue=@{"contentPath" = $contentPathValues} 878 | $providerDataArray.Add($contentPathDestinationKeyValue) | Out-Null 879 | 880 | [System.IO.FileInfo]$destXMLFile = GenerateInternal-ManifestFile -packOutput $packOutput -publishProperties $publishProperties -providerDataArray $providerDataArray -manifestFileName 'DestinationManifest.xml' 881 | 882 | $publishArgs = @() 883 | $publishArgs += ('-source:manifest=''{0}''' -f $sourceXMLFile.FullName) 884 | $publishArgs += ('-dest:manifest=''{0}''{1}' -f $destXMLFile.FullName, $sharedArgs.DestFragment) 885 | $publishArgs += '-verb:sync' 886 | $publishArgs += $sharedArgs.ExtraArgs 887 | 888 | $command = '"{0}" {1}' -f (Get-MSDeploy),($publishArgs -join ' ') 889 | $command | Print-CommandString 890 | Execute-Command -exePath (Get-MSDeploy) -arguments ($publishArgs -join ' ') 891 | 892 | # copy sql script to script folder 893 | if (($sharedArgs.EFMigrationData['EFSqlFiles'] -ne $null) -and ($sharedArgs.EFMigrationData['EFSqlFiles'].Count -gt 0)) { 894 | $scriptsDir = Join-Path $pubOut 'efscripts' 895 | 896 | if (!(Test-Path -Path $scriptsDir)) { 897 | New-Item -Path $scriptsDir -type directory | Out-Null 898 | } 899 | 900 | foreach ($sqlFile in $sharedArgs.EFMigrationData['EFSqlFiles'].Values) { 901 | Copy-Item $sqlFile -Destination $scriptsDir -Force -Recurse | Out-Null 902 | } 903 | } 904 | } 905 | } 906 | 907 | <# 908 | .SYNOPSIS 909 | This can be used to read a publish profile to extract the property values into a hashtable. 910 | 911 | .PARAMETER filepath 912 | Path to the publish profile to get the properties from. Currenlty this only supports reading 913 | .pubxml files. 914 | 915 | .EXAMPLE 916 | Get-PropertiesFromPublishProfile -filepath c:\projects\publish\devpublish.pubxml 917 | #> 918 | function Get-PropertiesFromPublishProfile{ 919 | [cmdletbinding()] 920 | param( 921 | [Parameter(Position=0,Mandatory=$true)] 922 | [ValidateNotNull()] 923 | [ValidateScript({Test-Path $_})] 924 | [System.IO.FileInfo]$filepath 925 | ) 926 | begin{ 927 | Add-Type -AssemblyName System.Core 928 | Add-Type -AssemblyName Microsoft.Build 929 | } 930 | process{ 931 | 'Reading publish properties from profile [{0}]' -f $filepath | Write-Verbose 932 | # use MSBuild to get the project and read properties 933 | $projectCollection = (New-Object Microsoft.Build.Evaluation.ProjectCollection) 934 | if(!([System.IO.Path]::IsPathRooted($filepath))){ 935 | $filepath = [System.IO.Path]::GetFullPath((Join-Path $pwd $filepath)) 936 | } 937 | $project = ([Microsoft.Build.Construction.ProjectRootElement]::Open([string]$filepath.Fullname, $projectCollection)) 938 | 939 | $properties = @{} 940 | foreach($property in $project.Properties){ 941 | $properties[$property.Name]=$property.Value 942 | } 943 | 944 | $properties 945 | } 946 | } 947 | 948 | function Print-CommandString{ 949 | [cmdletbinding()] 950 | param( 951 | [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] 952 | $command 953 | ) 954 | process{ 955 | 'Executing command [{0}]' -f $command | Write-Output 956 | } 957 | } 958 | 959 | function Execute-CommandString{ 960 | [cmdletbinding()] 961 | param( 962 | [Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)] 963 | [string[]]$command, 964 | 965 | [switch] 966 | $useInvokeExpression, 967 | 968 | [switch] 969 | $ignoreErrors 970 | ) 971 | process{ 972 | foreach($cmdToExec in $command){ 973 | 'Executing command [{0}]' -f $cmdToExec | Write-Verbose 974 | if($useInvokeExpression){ 975 | try { 976 | Invoke-Expression -Command $cmdToExec 977 | } 978 | catch { 979 | if(-not $ignoreErrors){ 980 | $msg = ('The command [{0}] exited with exception [{1}]' -f $cmdToExec, $_.ToString()) 981 | throw $msg 982 | } 983 | } 984 | } 985 | else { 986 | cmd.exe /D /C $cmdToExec 987 | 988 | if(-not $ignoreErrors -and ($LASTEXITCODE -ne 0)){ 989 | $msg = ('The command [{0}] exited with code [{1}]' -f $cmdToExec, $LASTEXITCODE) 990 | throw $msg 991 | } 992 | } 993 | } 994 | } 995 | } 996 | 997 | function Execute-Command { 998 | [cmdletbinding()] 999 | param( 1000 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 1001 | [String]$exePath, 1002 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 1003 | [String]$arguments, 1004 | [Parameter(Position=2)] 1005 | [System.IO.FileInfo]$workingDirectory 1006 | ) 1007 | process{ 1008 | $psi = New-Object -TypeName System.Diagnostics.ProcessStartInfo 1009 | $psi.CreateNoWindow = $true 1010 | $psi.UseShellExecute = $false 1011 | $psi.RedirectStandardOutput = $true 1012 | $psi.RedirectStandardError=$true 1013 | $psi.FileName = $exePath 1014 | $psi.Arguments = $arguments 1015 | if($workingDirectory -and (Test-Path -Path $workingDirectory)) { 1016 | $psi.WorkingDirectory = $workingDirectory 1017 | } 1018 | 1019 | $process = New-Object -TypeName System.Diagnostics.Process 1020 | $process.StartInfo = $psi 1021 | $process.EnableRaisingEvents=$true 1022 | 1023 | # Register the event handler for error 1024 | $stdErrEvent = Register-ObjectEvent -InputObject $process -EventName 'ErrorDataReceived' -Action { 1025 | if (! [String]::IsNullOrEmpty($EventArgs.Data)) { 1026 | $EventArgs.Data | Write-Error 1027 | } 1028 | } 1029 | 1030 | # Starting process. 1031 | $process.Start() | Out-Null 1032 | $process.BeginErrorReadLine() | Out-Null 1033 | $output = $process.StandardOutput.ReadToEnd() 1034 | $process.WaitForExit() | Out-Null 1035 | $output | Write-Output 1036 | 1037 | # UnRegister the event handler for error 1038 | Unregister-Event -SourceIdentifier $stdErrEvent.Name | Out-Null 1039 | } 1040 | } 1041 | 1042 | 1043 | function GetInternal-DotNetExePath { 1044 | process { 1045 | $dotnetinstallpath = $env:dotnetinstallpath 1046 | if (!$dotnetinstallpath) { 1047 | $DotNetRegItem = Get-ItemProperty -Path 'hklm:\software\dotnet\setup\' 1048 | if ($env:DOTNET_HOME) { 1049 | $dotnetinstallpath = Join-Path $env:DOTNET_HOME -ChildPath 'dotnet.exe' 1050 | } 1051 | elseif ($DotNetRegItem -and $DotNetRegItem.InstallDir){ 1052 | $dotnetinstallpath = Join-Path $DotNetRegItem.InstallDir -ChildPath 'dotnet.exe' 1053 | } 1054 | } 1055 | if (!(Test-Path $dotnetinstallpath)) { 1056 | throw 'Unable to find dotnet.exe, please install it and try again' 1057 | } 1058 | # return 1059 | [System.IO.FileInfo]$dotnetinstallpath 1060 | } 1061 | } 1062 | 1063 | function Get-MSDeploy{ 1064 | [cmdletbinding()] 1065 | param() 1066 | process{ 1067 | $installPath = $env:msdeployinstallpath 1068 | 1069 | if(!$installPath){ 1070 | $keysToCheck = @('hklm:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\3','hklm:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\2','hklm:\SOFTWARE\Microsoft\IIS Extensions\MSDeploy\1') 1071 | 1072 | foreach($keyToCheck in $keysToCheck){ 1073 | if(Test-Path $keyToCheck){ 1074 | $installPath = (Get-itemproperty $keyToCheck -Name InstallPath -ErrorAction SilentlyContinue | select -ExpandProperty InstallPath -ErrorAction SilentlyContinue) 1075 | } 1076 | 1077 | if($installPath){ 1078 | break; 1079 | } 1080 | } 1081 | } 1082 | 1083 | if(!$installPath){ 1084 | throw "Unable to find msdeploy.exe, please install it and try again" 1085 | } 1086 | 1087 | [string]$msdInstallLoc = (join-path $installPath 'msdeploy.exe') 1088 | 1089 | "Found msdeploy.exe at [{0}]" -f $msdInstallLoc | Write-Verbose 1090 | 1091 | $msdInstallLoc 1092 | } 1093 | } 1094 | 1095 | function InternalNormalize-MSDeployUrl{ 1096 | [cmdletbinding()] 1097 | param( 1098 | [Parameter(Position=0,Mandatory=$true)] 1099 | [string]$serviceUrl, 1100 | 1101 | [string] $siteName, 1102 | 1103 | [ValidateSet('WMSVC','RemoteAgent','InProc')] 1104 | [string]$serviceMethod = 'WMSVC' 1105 | ) 1106 | process{ 1107 | $tempUrl = $serviceUrl 1108 | $resultUrl = $serviceUrl 1109 | 1110 | $httpsStr = 'https://' 1111 | $httpStr = 'http://' 1112 | $msdeployAxd = 'msdeploy.axd' 1113 | 1114 | if(-not [string]::IsNullOrWhiteSpace($serviceUrl)){ 1115 | if([string]::Compare($serviceMethod,'WMSVC',[StringComparison]::OrdinalIgnoreCase) -eq 0){ 1116 | # if no http or https then add one 1117 | if(-not ($serviceUrl.StartsWith($httpStr,[StringComparison]::OrdinalIgnoreCase) -or 1118 | $serviceUrl.StartsWith($httpsStr,[StringComparison]::OrdinalIgnoreCase)) ){ 1119 | 1120 | $serviceUrl = [string]::Concat($httpsStr,$serviceUrl.TrimStart()) 1121 | } 1122 | [System.Uri]$serviceUri = New-Object -TypeName 'System.Uri' $serviceUrl 1123 | [System.UriBuilder]$serviceUriBuilder = New-Object -TypeName 'System.UriBuilder' $serviceUrl 1124 | 1125 | # if it's https and the port was not passed in override it to 8172 1126 | if( ([string]::Compare('https',$serviceUriBuilder.Scheme,[StringComparison]::OrdinalIgnoreCase) -eq 0) -and 1127 | -not $serviceUrl.Contains((':{0}' -f $serviceUriBuilder.Port)) ) { 1128 | $serviceUriBuilder.Port = 8172 1129 | } 1130 | 1131 | # if no path then add one 1132 | if([string]::Compare('/',$serviceUriBuilder.Path,[StringComparison]::OrdinalIgnoreCase) -eq 0){ 1133 | $serviceUriBuilder.Path = $msdeployAxd 1134 | } 1135 | 1136 | if ([string]::IsNullOrEmpty($serviceUriBuilder.Query) -and -not([string]::IsNullOrEmpty($siteName))) 1137 | { 1138 | $serviceUriBuilder.Query = "site=" + $siteName; 1139 | } 1140 | 1141 | $resultUrl = $serviceUriBuilder.Uri.AbsoluteUri 1142 | } 1143 | elseif([string]::Compare($serviceMethod,'RemoteAgent',[StringComparison]::OrdinalIgnoreCase) -eq 0){ 1144 | [System.UriBuilder]$serviceUriBuilder = New-Object -TypeName 'System.UriBuilder' $serviceUrl 1145 | # http://{computername}/MSDEPLOYAGENTSERVICE 1146 | # remote agent must use http 1147 | $serviceUriBuilder.Scheme = 'http' 1148 | $serviceUriBuilder.Path = '/MSDEPLOYAGENTSERVICE' 1149 | 1150 | $resultUrl = $serviceUriBuilder.Uri.AbsoluteUri 1151 | } 1152 | else{ 1153 | # see if it's for localhost 1154 | [System.Uri]$serviceUri = New-Object -TypeName 'System.Uri' $serviceUrl 1155 | $resultUrl = $serviceUri.AbsoluteUri 1156 | } 1157 | } 1158 | 1159 | # return the result to the caller 1160 | $resultUrl 1161 | } 1162 | } 1163 | 1164 | function InternalRegister-AspNetKnownPublishHandlers{ 1165 | [cmdletbinding()] 1166 | param() 1167 | process{ 1168 | 'Registering MSDeploy handler' | Write-Verbose 1169 | Register-AspnetPublishHandler -name 'MSDeploy' -force -handler { 1170 | [cmdletbinding()] 1171 | param( 1172 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 1173 | $publishProperties, 1174 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 1175 | $packOutput 1176 | ) 1177 | 1178 | Publish-AspNetMSDeploy -publishProperties $publishProperties -packOutput $packOutput 1179 | } 1180 | 1181 | 'Registering MSDeploy package handler' | Write-Verbose 1182 | Register-AspnetPublishHandler -name 'Package' -force -handler { 1183 | [cmdletbinding()] 1184 | param( 1185 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 1186 | $publishProperties, 1187 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 1188 | $packOutput 1189 | ) 1190 | 1191 | Publish-AspNetMSDeployPackage -publishProperties $publishProperties -packOutput $packOutput 1192 | } 1193 | 1194 | 'Registering FileSystem handler' | Write-Verbose 1195 | Register-AspnetPublishHandler -name 'FileSystem' -force -handler { 1196 | [cmdletbinding()] 1197 | param( 1198 | [Parameter(Mandatory = $true,Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] 1199 | $publishProperties, 1200 | [Parameter(Mandatory = $true,Position=1,ValueFromPipelineByPropertyName=$true)] 1201 | $packOutput 1202 | ) 1203 | 1204 | Publish-AspNetFileSystem -publishProperties $publishProperties -packOutput $packOutput 1205 | } 1206 | } 1207 | } 1208 | 1209 | <# 1210 | .SYNOPSIS 1211 | Used for testing purposes only. 1212 | #> 1213 | function InternalReset-AspNetPublishHandlers{ 1214 | [cmdletbinding()] 1215 | param() 1216 | process{ 1217 | $script:AspNetPublishHandlers = @{} 1218 | InternalRegister-AspNetKnownPublishHandlers 1219 | } 1220 | } 1221 | 1222 | Export-ModuleMember -function Get-*,Publish-*,Register-*,Enable-* 1223 | if($env:IsDeveloperMachine){ 1224 | # you can set the env var to expose all functions to importer. easy for development. 1225 | # this is required for executing pester test cases, it's set by build.ps1 1226 | Export-ModuleMember -function * 1227 | } 1228 | 1229 | # register the handlers so that Publish-AspNet can be called 1230 | InternalRegister-AspNetKnownPublishHandlers 1231 | 1232 | --------------------------------------------------------------------------------