0% found this document useful (0 votes)
160 views136 pages

OpenBullet2 Core Selected Detailed Documentation

The document outlines the structure of the OpenBullet 2 application, including its database context, entities, and exceptions. It defines various entities such as Proxies, ProxyGroups, Jobs, and Guests, along with their properties and relationships. Additionally, it includes extension methods for managing database context and IP address operations.

Uploaded by

joeblow232123
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
160 views136 pages

OpenBullet2 Core Selected Detailed Documentation

The document outlines the structure of the OpenBullet 2 application, including its database context, entities, and exceptions. It defines various entities such as Proxies, ProxyGroups, Jobs, and Guests, along with their properties and relationships. Additionally, it includes extension methods for managing database context and IP address operations.

Uploaded by

joeblow232123
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 136

File: ApplicationDbContext.

cs
■using Microsoft.EntityFrameworkCore;
using OpenBullet2.Core.Entities;

namespace OpenBullet2.Core;

/// <summary>
/// The <see cref="DbContext"/> for the OpenBullet 2 core domain.
/// </summary>
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions options)
: base(options)
{

public DbSet<ProxyEntity> Proxies { get; set; }


public DbSet<ProxyGroupEntity> ProxyGroups { get; set; }
public DbSet<WordlistEntity> Wordlists { get; set; }
public DbSet<JobEntity> Jobs { get; set; }
public DbSet<RecordEntity> Records { get; set; }
public DbSet<HitEntity> Hits { get; set; }
public DbSet<GuestEntity> Guests { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)


{
modelBuilder.Entity<ProxyGroupEntity>()
.HasMany(g => g.Proxies)
.WithOne(u => u.Group)
.OnDelete(DeleteBehavior.Cascade);

modelBuilder.Entity<ProxyGroupEntity>()
.HasOne(g => g.Owner)
.WithMany(u => u.ProxyGroups)
.OnDelete(DeleteBehavior.SetNull);

base.OnModelCreating(modelBuilder);
}
}

File: ApplicationDbContextFactory.cs
■using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace OpenBullet2.Core;
public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
var dbContextBuilder = new DbContextOptionsBuilder();

var sensitiveLogging = false;


var connectionString = "Data Source=UserData/OpenBullet.db;";

dbContextBuilder.EnableSensitiveDataLogging(sensitiveLogging);
dbContextBuilder.UseSqlite(connectionString);

return new ApplicationDbContext(dbContextBuilder.Options);


}
}
File: OpenBullet2.Core.csproj
■<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BCrypt.Net-Core" Version="1.6.0"/>
<PackageReference Include="MaxMind.GeoIP2" Version="5.2.0"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.6"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.6"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.6"/>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="8.0.0"/>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0"/>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.5"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\RuriLib\RuriLib.csproj"/>
</ItemGroup>

</Project>

File: Entities/Entity.cs
■namespace OpenBullet2.Core.Entities;

/// <summary>
/// This is the base class for an entity that is saved to a database.
/// </summary>
public class Entity
{
public int Id { get; set; }
}

File: Entities/GuestEntity.cs
■using System;
using System.Collections.Generic;

namespace OpenBullet2.Core.Entities;
/// <summary>
/// This entity stores a guest user of OpenBullet 2.
/// </summary>
public class GuestEntity : Entity
{
/// <summary>
/// The username that the guest uses to log in.
/// </summary>
public string Username { get; set; }

/// <summary>
/// The bcrypt hash of the password of the guest.
/// </summary>
public string PasswordHash { get; set; }

/// <summary>
/// The time when access will expire for this guest.
/// </summary>
public DateTime AccessExpiration { get; set; }

/// <summary>
/// A comma-separated list of IPv4 or IPv6 addresses that the guest
/// is allowed to use when connecting to the remote instance of OpenBullet 2.
/// These can include masked IP ranges and static DNS.
/// </summary>
public string AllowedAddresses { get; set; }

/// <summary>
/// The proxy groups that the guest owns.
/// </summary>
public ICollection<ProxyGroupEntity> ProxyGroups { get; set; }
}
File: Entities/HitEntity.cs
■using System;

namespace OpenBullet2.Core.Entities;

/// <summary>
/// This entity stores a hit from a job in the database.
/// </summary>
public class HitEntity : Entity
{
/// <summary>
/// The data that was provided to the bot to get the hit.
/// </summary>
public string Data { get; set; } = string.Empty;

/// <summary>
/// The variables captured by the bot.
/// </summary>
public string CapturedData { get; set; } = string.Empty;

/// <summary>
/// The string representation of the proxy that was used to get the hit (blank if none).
/// </summary>
public string Proxy { get; set; } = string.Empty;

/// <summary>
/// The exact date when the hit was found.
/// </summary>
public DateTime Date { get; set; }

/// <summary>
/// The type of hit, for example SUCCESS, NONE, CUSTOM etc.
/// </summary>
public string Type { get; set; }

/// <summary>
/// The ID of the owner of this hit (0 if admin).
/// </summary>
public int OwnerId { get; set; } = 0;

/// <summary>
/// The ID of the config that was used to get the hit.
/// </summary>
public string ConfigId { get; set; } = null;

/// <summary>
/// The name of the config that was used to get the hit.
/// Needed to identify the name even if the config was deleted.
/// </summary>
public string ConfigName { get; set; } = string.Empty;

/// <summary>
/// The category of the config that was used to get the hit.
/// Needed to identify the category even if the config was deleted.
/// </summary>
public string ConfigCategory { get; set; } = string.Empty;

/// <summary>
/// The ID of the wordlist that was used to get the hit, -1 if no wordlist was used, < -1 for other data p
/// </summary>
public int WordlistId { get; set; } = -1;

/// <summary>
/// The name of the wordlist that was used to get the hit, blank if no wordlist was used.
/// Needed to identify the name even if the wordlist was deleted. If <see cref="WordlistId"/> is less th
/// this field contains information about the data pool that was used.
/// </summary>
public string WordlistName { get; set; } = string.Empty;

/// <summary>
/// Gets a unique hash of the hit.
/// </summary>
/// <param name="ignoreWordlistName">Whether the wordlist name should affect the generated ha
public int GetHashCode(bool ignoreWordlistName = true)
{
var id = ignoreWordlistName
? Data + ConfigName
: Data + ConfigName + WordlistName;

return id.GetHashCode();
}
}
File: Entities/JobEntity.cs
■using OpenBullet2.Core.Models.Jobs;
using System;

namespace OpenBullet2.Core.Entities;

/// <summary>
/// This entity stores a job of the OpenBullet 2 instance.
/// </summary>
public class JobEntity : Entity
{
/// <summary>
/// The creation date and time of the job.
/// </summary>
public DateTime CreationDate { get; set; }

/// <summary>
/// The type of job, used to know how to deserialize the options.
/// </summary>
public JobType JobType { get; set; }

/// <summary>
/// The job options as a json string.
/// </summary>
public string JobOptions { get; set; }

/// <summary>
/// The owner of this job. Null if admin.
/// </summary>
public GuestEntity Owner { get; set; }
}

File: Entities/ProxyEntity.cs
■using RuriLib.Models.Proxies;
using System;
using System.Text;

namespace OpenBullet2.Core.Entities;

/// <summary>
/// This entity stores a proxy that belongs to a proxy group.
/// </summary>
public class ProxyEntity : Entity
{
/// <summary>
/// The host on which the proxy server is running.
/// </summary>
public string Host { get; set; }
/// <summary>
/// The port on which the proxy server is listening.
/// </summary>
public int Port { get; set; }

/// <summary>
/// The protocol used by the proxy server to open a proxy tunnel.
/// </summary>
public ProxyType Type { get; set; }

/// <summary>
/// The username, if required by the proxy server.
/// </summary>
public string Username { get; set; }

/// <summary>
/// The password, if required by the proxy server.
/// </summary>
public string Password { get; set; }

/// <summary>
/// The country of the proxy, detected after checking it with a geolocalization service.
/// </summary>
public string Country { get; set; }

/// <summary>
/// The working status of the proxy.
/// </summary>
public ProxyWorkingStatus Status { get; set; }

/// <summary>
/// The ping of the proxy in milliseconds.
/// </summary>
public int Ping { get; set; }

/// <summary>
/// The last time the proxy was checked.
/// </summary>
public DateTime LastChecked { get; set; }

/// <summary>
/// The proxy group to which the proxy belongs to.
/// </summary>
public ProxyGroupEntity Group { get; set; }

/// <summary>
/// Returns a string representation of the proxy.
/// For example <code>(Socks5)192.168.1.1:8080:username:password</code>
/// </summary>
public override string ToString()
{
var sb = new StringBuilder();

if (Type != ProxyType.Http)
{
sb.Append($"({Type})");
}

sb.Append($"{Host}:{Port}");

if (!string.IsNullOrWhiteSpace(Username))
{
sb.Append($":{Username}:{Password}");
}

return sb.ToString();
}
}
File: Entities/ProxyGroupEntity.cs
■using System.Collections.Generic;

namespace OpenBullet2.Core.Entities;

/// <summary>
/// This entity stores a group that identifies a collection of proxies.
/// </summary>
public class ProxyGroupEntity : Entity
{
/// <summary>
/// The name of the group.
/// </summary>
public string Name { get; set; }

/// <summary>
/// The owner of this group (null if admin).
/// </summary>
public GuestEntity Owner { get; set; }

/// <summary>
/// The proxies in this group.
/// </summary>
public ICollection<ProxyEntity> Proxies { get; set; }
}

File: Entities/RecordEntity.cs
■namespace OpenBullet2.Core.Entities;

/// <summary>
/// This entity stores a record that matches a given config ID and wordlist ID
/// to a checkpoint in the checking process, identified by the amount of data
/// lines processed up to the point when it was last saved.
/// </summary>
public class RecordEntity : Entity
{
/// <summary>
/// The ID of the config that was running.
/// </summary>
public string ConfigId { get; set; }

/// <summary>
/// The ID of the wordlist that was being used.
/// </summary>
public int WordlistId { get; set; }

/// <summary>
/// The amount of data lines processed until the last save.
/// </summary>
public int Checkpoint { get; set; }
}
File: Entities/WordlistEntity.cs
■namespace OpenBullet2.Core.Entities;

/// <summary>
/// This entity stores the metadata of a wordlist in OpenBullet 2.
/// </summary>
public class WordlistEntity : Entity
{
/// <summary>
/// The name of the wordlist.
/// </summary>
public string Name { get; set; }

/// <summary>
/// The path to the file on disk that contains the lines of the wordlist.
/// </summary>
public string FileName { get; set; }

/// <summary>
/// The purpose of the wordlist.
/// </summary>
public string Purpose { get; set; }

/// <summary>
/// The total amount of lines of the wordlist, usually calculated during import.
/// </summary>
public int Total { get; set; }

/// <summary>
/// The Wordlist Type.
/// </summary>
public string Type { get; set; }

/// <summary>
/// The owner of the wordlist (null if admin).
/// </summary>
public GuestEntity Owner { get; set; }
}

File: Exceptions/EntityNotFoundException.cs
using System;

namespace OpenBullet2.Core.Exceptions;

/// <summary>
/// Represents an exception thrown when an entity is not found.
/// </summary>
public class EntityNotFoundException : Exception
{
/// <summary>
/// Creates a new <see cref="EntityNotFoundException"/> with a message.
/// </summary>
public EntityNotFoundException(string message) : base(message) { }
}
File: Exceptions/MissingUserAgentsException.cs
using System;

namespace OpenBullet2.Core.Exceptions;

/// <summary>
/// Thrown when no valid user agents are found in the user-agents.json file.
/// </summary>
public class MissingUserAgentsException : Exception
{
/// <summary>
/// Creates a new MissingUserAgentsException.
/// </summary>
public MissingUserAgentsException(string message) : base(message) { }
}

File: Exceptions/UnsupportedFileTypeException.cs
using System;

namespace OpenBullet2.Core.Exceptions;

/// <summary>
/// The exception that is thrown when a file type is not supported.
/// </summary>
public class UnsupportedFileTypeException : Exception
{
/// <summary>
/// Creates a new <see cref="UnsupportedFileTypeException"/> with a message.
/// </summary>
public UnsupportedFileTypeException(string message) : base(message) { }
}

File: Extensions/DbContextExtensions.cs
■using Microsoft.EntityFrameworkCore;
using OpenBullet2.Core.Entities;
using System.Linq;

namespace OpenBullet2.Core.Extensions;

public static class DbContextExtensions


{
public static void DetachLocal<T>(this DbContext context, int id) where T : Entity
{
var local = context.Set<T>().Local.FirstOrDefault(entry => entry.Id == id);

if (local is not null)


{
context.Entry(local).State = EntityState.Detached;
}
}
}
File: Extensions/IPAddressExtensions.cs
■using System;
using System.Net;

namespace OpenBullet2.Core.Extensions;

public static class IPAddressExtensions


{
public static IPAddress GetBroadcastAddress(this IPAddress address, IPAddress subnetMask)
{
var ipAdressBytes = address.GetAddressBytes();
var subnetMaskBytes = subnetMask.GetAddressBytes();

if (ipAdressBytes.Length != subnetMaskBytes.Length)
{
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
}

var broadcastAddress = new byte[ipAdressBytes.Length];

for (var i = 0; i < broadcastAddress.Length; i++)


{
broadcastAddress[i] = (byte)(ipAdressBytes[i] | (subnetMaskBytes[i] ^ 255));
}

return new IPAddress(broadcastAddress);


}

public static IPAddress GetNetworkAddress(this IPAddress address, IPAddress subnetMask)


{
var ipAdressBytes = address.GetAddressBytes();
var subnetMaskBytes = subnetMask.GetAddressBytes();

if (ipAdressBytes.Length != subnetMaskBytes.Length)
{
throw new ArgumentException("Lengths of IP address and subnet mask do not match.");
}

var broadcastAddress = new byte[ipAdressBytes.Length];

for (var i = 0; i < broadcastAddress.Length; i++)


{
broadcastAddress[i] = (byte)(ipAdressBytes[i] & (subnetMaskBytes[i]));
}

return new IPAddress(broadcastAddress);


}

public static bool IsInSameSubnet(this IPAddress address2, IPAddress address, IPAddress subnetMask)
{
var network1 = address.GetNetworkAddress(subnetMask);
var network2 = address2.GetNetworkAddress(subnetMask);

return network1.Equals(network2);
}
}
File: Extensions/StringExtensions.cs
■using System.Globalization;
using System.Text;

namespace OpenBullet2.Core.Extensions;

public static class StringExtensions


{
public static string BeautifyName(this string name)
{
StringBuilder sb = new();

foreach (var c in name)


{
// Replace anything, but letters and digits, with space
if (!char.IsLetterOrDigit(c))
{
sb.Append(' ');
}
else
{
sb.Append(c);
}
}

return CultureInfo.CurrentCulture.TextInfo
.ToTitleCase(sb.ToString().ToLower());
}
}

File: Extensions/TimespanExtensions.cs
■using System;

namespace OpenBullet2.Core.Extensions;

public static class TimespanExtensions


{
public static string ToReadableAgeString(this TimeSpan span) => string.Format("{0:0}", span.Days / 365.25);

public static string ToReadableString(this TimeSpan span)


{
var formatted = string.Format("{0}{1}{2}{3}",
span.Duration().Days > 0 ? string.Format("{0:0} day{1}, ", span.Days, span.Days == 1 ? string.Empty : "s") : string.E
span.Duration().Hours > 0 ? string.Format("{0:0} hour{1}, ", span.Hours, span.Hours == 1 ? string.Empty : "s") : strin
span.Duration().Minutes > 0 ? string.Format("{0:0} minute{1}, ", span.Minutes, span.Minutes == 1 ? string.Empty : "
span.Duration().Seconds > 0 ? string.Format("{0:0} second{1}", span.Seconds, span.Seconds == 1 ? string.Empty :

if (formatted.EndsWith(", "))
{
formatted = formatted.Substring(0, formatted.Length - 2);
}

if (string.IsNullOrEmpty(formatted))
{
formatted = "0 seconds";
}

return formatted;
}
}
File: Helpers/Firewall.cs
■using OpenBullet2.Core.Extensions;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Helpers;

public static class Firewall


{
// TODO: Write unit tests.
/// <summary>
/// Checks if an <paramref name="ip"/> is allowed according to a whitelist of <paramref name="allowed"/>
/// IPs. Supports individual IPv4, individual IPv6, masked IPv4 range, dynamic DNS.
/// </summary>
public static async Task<bool> CheckIpValidityAsync(
IPAddress ip, IEnumerable<string> allowed)
{
foreach (var addr in allowed)
{
try
{
// Check if standard IPv4 or IPv6
if (Regex.Match(addr, @"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|$)){4}$").Success ||
Regex.Match(addr, @"^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[
{
if (ip.Equals(IPAddress.Parse(addr)))
{
return true;
}

continue;
}

// Check if masked IPv4


if (addr.Contains('/'))
{
var split = addr.Split('/');
var maskLength = int.Parse(split[1]);
var toCompare = IPAddress.Parse(split[0]);
var mask = SubnetMask.CreateByNetBitLength(maskLength);

if (ip.IsInSameSubnet(toCompare, mask))
{
return true;
}

continue;
}

// Otherwise it must be a dynamic DNS


var resolved = await Dns.GetHostEntryAsync(addr);
if (resolved.AddressList.Any(a => a.Equals(ip)))
{
return true;
}
}
catch
{

}
}

return false;
}
}
File: Helpers/ImageEditor.cs
■using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Processing;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;

namespace OpenBullet2.Core.Helpers;

public static class ImageEditor


{
public static byte[] ToCompatibleFormat(byte[] bytes)
{
// ICO magic numbers
if (bytes[0] == 0x00 && bytes[1] == 0x00 && bytes[2] == 0x01 && bytes[3] == 0x00)
{
using var ms = new MemoryStream(bytes);
var icon = new Icon(ms);
var bitmap = icon.ToBitmap();

using var ms2 = new MemoryStream();


bitmap.Save(ms, ImageFormat.Png);
return ms.ToArray();
}

return bytes;
}

public static string ResizeBase64(string base64, int width, int height)


{
using var image = SixLabors.ImageSharp.Image.Load(Convert.FromBase64String(base64));

image.Mutate(x => x
.Resize(width, height));

using var ms = new MemoryStream();


image.Save(ms, new PngEncoder());
return Convert.ToBase64String(ms.ToArray());
}
}

File: Helpers/Mapper.cs
■using OpenBullet2.Core.Entities;
using RuriLib.Models.Proxies;
namespace OpenBullet2.Core.Helpers;

// TODO: Refactor these methods, make it so that you can call Map<TInput,TOutput>
public static class Mapper
{
/// <summary>
/// Maps a <see cref="Proxy"/> to a <see cref="ProxyEntity"/>.
/// </summary>
public static ProxyEntity MapProxyToProxyEntity(Proxy proxy) => new()
{
Country = proxy.Country,
Host = proxy.Host,
Port = proxy.Port,
LastChecked = proxy.LastChecked,
Username = proxy.Username,
Password = proxy.Password,
Ping = proxy.Ping,
Status = proxy.WorkingStatus,
Type = proxy.Type
};
}
File: Helpers/RootUtils.cs
■namespace OpenBullet2.Core.Helpers;

public static class RootUtils


{
public static string RootWarning =>
@"
====================================================
THIS PROGRAM SHOULD NOT RUN AS ROOT / ADMINISTRATOR.
====================================================

This is due to the fact that configs can contain C# code that is not picked up by your antivirus.
This can lead to information leaks, malware, system takeover and more.
Please consider creating a user with limited privileges and running it from there.
";
}

File: Helpers/SubnetMask.cs
■using System;
using System.Net;

namespace OpenBullet2.Core.Helpers;

public static class SubnetMask


{
public static readonly IPAddress ClassA = IPAddress.Parse("255.0.0.0");
public static readonly IPAddress ClassB = IPAddress.Parse("255.255.0.0");
public static readonly IPAddress ClassC = IPAddress.Parse("255.255.255.0");

public static IPAddress CreateByHostBitLength(int hostpartLength)


{
var hostPartLength = hostpartLength;
var netPartLength = 32 - hostPartLength;

if (netPartLength < 2)
{
throw new ArgumentException("Number of hosts is too large for IPv4");
}

var binaryMask = new byte[4];

for (var i = 0; i < 4; i++)


{
if (i * 8 + 8 <= netPartLength)
{
binaryMask[i] = 255;
}
else if (i * 8 > netPartLength)
{
binaryMask[i] = 0;
}
else
{
var oneLength = netPartLength - i * 8;
var binaryDigit = string.Empty.PadLeft(oneLength, '1').PadRight(8, '0');
binaryMask[i] = Convert.ToByte(binaryDigit, 2);
}
}
return new IPAddress(binaryMask);
}

public static IPAddress CreateByNetBitLength(int netpartLength)


{
var hostPartLength = 32 - netpartLength;
return CreateByHostBitLength(hostPartLength);
}

public static IPAddress CreateByHostNumber(int numberOfHosts)


{
var maxNumber = numberOfHosts + 1;
var b = Convert.ToString(maxNumber, 2);
return CreateByHostBitLength(b.Length);
}
}
File: Logging/MemoryJobLogger.cs
■using OpenBullet2.Core.Models.Settings;
using OpenBullet2.Core.Services;
using RuriLib.Logging;
using System;
using System.Collections.Generic;

namespace OpenBullet2.Logging;

public struct JobLogEntry


{
public LogKind kind;
public string message;
public string color;
public DateTime date;

public JobLogEntry(LogKind kind, string message, string color)


{
this.kind = kind;
this.message = message;
this.color = color;
date = DateTime.Now;
}
}

/// <summary>
/// An in-memory logger for job operations.
/// </summary>
public class MemoryJobLogger
{
private readonly Dictionary<int, List<JobLogEntry>> logs = new();
private readonly object locker = new();
private readonly OpenBulletSettings settings;
public event EventHandler<int> NewLog; // The integer is the id of the job for which a new log came

public MemoryJobLogger(OpenBulletSettingsService settingsService)


{
settings = settingsService.Settings;
}

public IEnumerable<JobLogEntry> GetLog(int jobId)


{
lock (locker)
{
// Return a copy so we can keep modifying the original one without worrying about thread safety
return logs.ContainsKey(jobId) ? logs[jobId].ToArray() : new List<JobLogEntry>();
}
}

public void Log(int jobId, string message, LogKind kind = LogKind.Custom, string color = "white")
{
if (!settings.GeneralSettings.EnableJobLogging)
{
return;
}

var entry = new JobLogEntry(kind, message, color);


var maxBufferSize = settings.GeneralSettings.LogBufferSize;

lock (locker)
{
if (!logs.ContainsKey(jobId))
{
logs[jobId] = new List<JobLogEntry> { entry };
}
else
{
lock (logs[jobId])
{
logs[jobId].Add(entry);

if (logs[jobId].Count > maxBufferSize && maxBufferSize > 0)


{
logs[jobId].RemoveRange(0, logs[jobId].Count - maxBufferSize);
}
}
}
}

NewLog?.Invoke(this, jobId);
}

public void LogInfo(int jobId, string message) => Log(jobId, message, LogKind.Info, "var(--fg-primary
public void LogSuccess(int jobId, string message) => Log(jobId, message, LogKind.Success, "var(--
public void LogWarning(int jobId, string message) => Log(jobId, message, LogKind.Warning, "var(--
public void LogError(int jobId, string message) => Log(jobId, message, LogKind.Error, "var(--fg-fail)"

public void Clear(int jobId)


{
if (!logs.ContainsKey(jobId))
{
return;
}

logs[jobId].Clear();
}
}
File: Migrations/20201228145119_Initial.Designer.cs
■// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OpenBullet2.Core;

namespace OpenBullet2.Core.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20201228145119_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "5.0.8");

modelBuilder.Entity("OpenBullet2.Core.Entities.GuestEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<DateTime>("AccessExpiration")
.HasColumnType("TEXT");

b.Property<string>("AllowedAddresses")
.HasColumnType("TEXT");

b.Property<string>("PasswordHash")
.HasColumnType("TEXT");

b.Property<string>("Username")
.HasColumnType("TEXT");

b.HasKey("Id");

b.ToTable("Guests");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.HitEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("CapturedData")
.HasColumnType("TEXT");

b.Property<string>("ConfigCategory")
.HasColumnType("TEXT");

b.Property<string>("ConfigId")
.HasColumnType("TEXT");

b.Property<string>("ConfigName")
.HasColumnType("TEXT");

b.Property<string>("Data")
.HasColumnType("TEXT");

b.Property<DateTime>("Date")
.HasColumnType("TEXT");

b.Property<int>("OwnerId")
.HasColumnType("INTEGER");

b.Property<string>("Proxy")
.HasColumnType("TEXT");

b.Property<string>("Type")
.HasColumnType("TEXT");

b.Property<int>("WordlistId")
.HasColumnType("INTEGER");

b.Property<string>("WordlistName")
.HasColumnType("TEXT");

b.HasKey("Id");

b.ToTable("Hits");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.JobEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");

b.Property<string>("JobOptions")
.HasColumnType("TEXT");

b.Property<int>("JobType")
.HasColumnType("INTEGER");

b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.HasKey("Id");

b.HasIndex("OwnerId");
b.ToTable("Jobs");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("Country")
.HasColumnType("TEXT");

b.Property<int?>("GroupId")
.HasColumnType("INTEGER");

b.Property<string>("Host")
.HasColumnType("TEXT");

b.Property<DateTime>("LastChecked")
.HasColumnType("TEXT");

b.Property<string>("Password")
.HasColumnType("TEXT");

b.Property<int>("Ping")
.HasColumnType("INTEGER");

b.Property<int>("Port")
.HasColumnType("INTEGER");

b.Property<int>("Status")
.HasColumnType("INTEGER");

b.Property<int>("Type")
.HasColumnType("INTEGER");

b.Property<string>("Username")
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("GroupId");

b.ToTable("Proxies");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyGroupEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.HasKey("Id");

b.HasIndex("OwnerId");

b.ToTable("ProxyGroups");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.RecordEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<int>("Checkpoint")
.HasColumnType("INTEGER");

b.Property<string>("ConfigId")
.HasColumnType("TEXT");

b.Property<int>("WordlistId")
.HasColumnType("INTEGER");

b.HasKey("Id");

b.ToTable("Records");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.WordlistEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("FileName")
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasColumnType("TEXT");

b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.Property<string>("Purpose")
.HasColumnType("TEXT");

b.Property<int>("Total")
.HasColumnType("INTEGER");

b.Property<string>("Type")
.HasColumnType("TEXT");
b.HasKey("Id");

b.HasIndex("OwnerId");

b.ToTable("Wordlists");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.JobEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany()
.HasForeignKey("OwnerId");

b.Navigation("Owner");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.ProxyGroupEntity", "Group")
.WithMany()
.HasForeignKey("GroupId");

b.Navigation("Group");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyGroupEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany()
.HasForeignKey("OwnerId");

b.Navigation("Owner");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.WordlistEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany()
.HasForeignKey("OwnerId");

b.Navigation("Owner");
});
#pragma warning restore 612, 618
}
}
}
File: Migrations/20201228145119_Initial.cs
■using System;
using Microsoft.EntityFrameworkCore.Migrations;

namespace OpenBullet2.Core.Migrations;

public partial class Initial : Migration


{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Guests",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Username = table.Column<string>(type: "TEXT", nullable: true),
PasswordHash = table.Column<string>(type: "TEXT", nullable: true),
AccessExpiration = table.Column<DateTime>(type: "TEXT", nullable: false),
AllowedAddresses = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Guests", x => x.Id);
});

migrationBuilder.CreateTable(
name: "Hits",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Data = table.Column<string>(type: "TEXT", nullable: true),
CapturedData = table.Column<string>(type: "TEXT", nullable: true),
Proxy = table.Column<string>(type: "TEXT", nullable: true),
Date = table.Column<DateTime>(type: "TEXT", nullable: false),
Type = table.Column<string>(type: "TEXT", nullable: true),
OwnerId = table.Column<int>(type: "INTEGER", nullable: false),
ConfigId = table.Column<string>(type: "TEXT", nullable: true),
ConfigName = table.Column<string>(type: "TEXT", nullable: true),
ConfigCategory = table.Column<string>(type: "TEXT", nullable: true),
WordlistId = table.Column<int>(type: "INTEGER", nullable: false),
WordlistName = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Hits", x => x.Id);
});

migrationBuilder.CreateTable(
name: "Records",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ConfigId = table.Column<string>(type: "TEXT", nullable: true),
WordlistId = table.Column<int>(type: "INTEGER", nullable: false),
Checkpoint = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Records", x => x.Id);
});

migrationBuilder.CreateTable(
name: "Jobs",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
JobType = table.Column<int>(type: "INTEGER", nullable: false),
JobOptions = table.Column<string>(type: "TEXT", nullable: true),
OwnerId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Jobs", x => x.Id);
table.ForeignKey(
name: "FK_Jobs_Guests_OwnerId",
column: x => x.OwnerId,
principalTable: "Guests",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});

migrationBuilder.CreateTable(
name: "ProxyGroups",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true),
OwnerId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ProxyGroups", x => x.Id);
table.ForeignKey(
name: "FK_ProxyGroups_Guests_OwnerId",
column: x => x.OwnerId,
principalTable: "Guests",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});

migrationBuilder.CreateTable(
name: "Wordlists",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true),
FileName = table.Column<string>(type: "TEXT", nullable: true),
Purpose = table.Column<string>(type: "TEXT", nullable: true),
Total = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<string>(type: "TEXT", nullable: true),
OwnerId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Wordlists", x => x.Id);
table.ForeignKey(
name: "FK_Wordlists_Guests_OwnerId",
column: x => x.OwnerId,
principalTable: "Guests",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});

migrationBuilder.CreateTable(
name: "Proxies",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Host = table.Column<string>(type: "TEXT", nullable: true),
Port = table.Column<int>(type: "INTEGER", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Username = table.Column<string>(type: "TEXT", nullable: true),
Password = table.Column<string>(type: "TEXT", nullable: true),
Country = table.Column<string>(type: "TEXT", nullable: true),
Status = table.Column<int>(type: "INTEGER", nullable: false),
Ping = table.Column<int>(type: "INTEGER", nullable: false),
LastChecked = table.Column<DateTime>(type: "TEXT", nullable: false),
GroupId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Proxies", x => x.Id);
table.ForeignKey(
name: "FK_Proxies_ProxyGroups_GroupId",
column: x => x.GroupId,
principalTable: "ProxyGroups",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});

migrationBuilder.CreateIndex(
name: "IX_Jobs_OwnerId",
table: "Jobs",
column: "OwnerId");

migrationBuilder.CreateIndex(
name: "IX_Proxies_GroupId",
table: "Proxies",
column: "GroupId");

migrationBuilder.CreateIndex(
name: "IX_ProxyGroups_OwnerId",
table: "ProxyGroups",
column: "OwnerId");

migrationBuilder.CreateIndex(
name: "IX_Wordlists_OwnerId",
table: "Wordlists",
column: "OwnerId");
}

protected override void Down(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropTable(
name: "Hits");

migrationBuilder.DropTable(
name: "Jobs");

migrationBuilder.DropTable(
name: "Proxies");

migrationBuilder.DropTable(
name: "Records");

migrationBuilder.DropTable(
name: "Wordlists");

migrationBuilder.DropTable(
name: "ProxyGroups");

migrationBuilder.DropTable(
name: "Guests");
}
}
File: Migrations/20240220175839_ProxyGroupDeleteRelationships.Designer.cs
■// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OpenBullet2.Core;

#nullable disable

namespace OpenBullet2.Core.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20240220175839_ProxyGroupDeleteRelationships")]
partial class ProxyGroupDeleteRelationships
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.6");

modelBuilder.Entity("OpenBullet2.Core.Entities.GuestEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<DateTime>("AccessExpiration")
.HasColumnType("TEXT");

b.Property<string>("AllowedAddresses")
.HasColumnType("TEXT");

b.Property<string>("PasswordHash")
.HasColumnType("TEXT");

b.Property<string>("Username")
.HasColumnType("TEXT");

b.HasKey("Id");

b.ToTable("Guests");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.HitEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("CapturedData")
.HasColumnType("TEXT");
b.Property<string>("ConfigCategory")
.HasColumnType("TEXT");

b.Property<string>("ConfigId")
.HasColumnType("TEXT");

b.Property<string>("ConfigName")
.HasColumnType("TEXT");

b.Property<string>("Data")
.HasColumnType("TEXT");

b.Property<DateTime>("Date")
.HasColumnType("TEXT");

b.Property<int>("OwnerId")
.HasColumnType("INTEGER");

b.Property<string>("Proxy")
.HasColumnType("TEXT");

b.Property<string>("Type")
.HasColumnType("TEXT");

b.Property<int>("WordlistId")
.HasColumnType("INTEGER");

b.Property<string>("WordlistName")
.HasColumnType("TEXT");

b.HasKey("Id");

b.ToTable("Hits");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.JobEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");

b.Property<string>("JobOptions")
.HasColumnType("TEXT");

b.Property<int>("JobType")
.HasColumnType("INTEGER");

b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.HasKey("Id");
b.HasIndex("OwnerId");

b.ToTable("Jobs");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("Country")
.HasColumnType("TEXT");

b.Property<int?>("GroupId")
.HasColumnType("INTEGER");

b.Property<string>("Host")
.HasColumnType("TEXT");

b.Property<DateTime>("LastChecked")
.HasColumnType("TEXT");

b.Property<string>("Password")
.HasColumnType("TEXT");

b.Property<int>("Ping")
.HasColumnType("INTEGER");

b.Property<int>("Port")
.HasColumnType("INTEGER");

b.Property<int>("Status")
.HasColumnType("INTEGER");

b.Property<int>("Type")
.HasColumnType("INTEGER");

b.Property<string>("Username")
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("GroupId");

b.ToTable("Proxies");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyGroupEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("Name")
.HasColumnType("TEXT");

b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.HasKey("Id");

b.HasIndex("OwnerId");

b.ToTable("ProxyGroups");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.RecordEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<int>("Checkpoint")
.HasColumnType("INTEGER");

b.Property<string>("ConfigId")
.HasColumnType("TEXT");

b.Property<int>("WordlistId")
.HasColumnType("INTEGER");

b.HasKey("Id");

b.ToTable("Records");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.WordlistEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("FileName")
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasColumnType("TEXT");

b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.Property<string>("Purpose")
.HasColumnType("TEXT");

b.Property<int>("Total")
.HasColumnType("INTEGER");

b.Property<string>("Type")
.HasColumnType("TEXT");
b.HasKey("Id");

b.HasIndex("OwnerId");

b.ToTable("Wordlists");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.JobEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany()
.HasForeignKey("OwnerId");

b.Navigation("Owner");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.ProxyGroupEntity", "Group")
.WithMany("Proxies")
.HasForeignKey("GroupId")
.OnDelete(DeleteBehavior.Cascade);

b.Navigation("Group");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyGroupEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany("ProxyGroups")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.SetNull);

b.Navigation("Owner");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.WordlistEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany()
.HasForeignKey("OwnerId");

b.Navigation("Owner");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.GuestEntity", b =>
{
b.Navigation("ProxyGroups");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyGroupEntity", b =>
{
b.Navigation("Proxies");
});
#pragma warning restore 612, 618
}
}
}
File: Migrations/20240220175839_ProxyGroupDeleteRelationships.cs
■using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace OpenBullet2.Core.Migrations
{
public partial class ProxyGroupDeleteRelationships : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Proxies_ProxyGroups_GroupId",
table: "Proxies");

migrationBuilder.DropForeignKey(
name: "FK_ProxyGroups_Guests_OwnerId",
table: "ProxyGroups");

migrationBuilder.AddForeignKey(
name: "FK_Proxies_ProxyGroups_GroupId",
table: "Proxies",
column: "GroupId",
principalTable: "ProxyGroups",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);

migrationBuilder.AddForeignKey(
name: "FK_ProxyGroups_Guests_OwnerId",
table: "ProxyGroups",
column: "OwnerId",
principalTable: "Guests",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}

protected override void Down(MigrationBuilder migrationBuilder)


{
migrationBuilder.DropForeignKey(
name: "FK_Proxies_ProxyGroups_GroupId",
table: "Proxies");

migrationBuilder.DropForeignKey(
name: "FK_ProxyGroups_Guests_OwnerId",
table: "ProxyGroups");

migrationBuilder.AddForeignKey(
name: "FK_Proxies_ProxyGroups_GroupId",
table: "Proxies",
column: "GroupId",
principalTable: "ProxyGroups",
principalColumn: "Id");

migrationBuilder.AddForeignKey(
name: "FK_ProxyGroups_Guests_OwnerId",
table: "ProxyGroups",
column: "OwnerId",
principalTable: "Guests",
principalColumn: "Id");
}
}
}
File: Migrations/ApplicationDbContextModelSnapshot.cs
■// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OpenBullet2.Core;

#nullable disable

namespace OpenBullet2.Core.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.6");

modelBuilder.Entity("OpenBullet2.Core.Entities.GuestEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<DateTime>("AccessExpiration")
.HasColumnType("TEXT");

b.Property<string>("AllowedAddresses")
.HasColumnType("TEXT");

b.Property<string>("PasswordHash")
.HasColumnType("TEXT");

b.Property<string>("Username")
.HasColumnType("TEXT");

b.HasKey("Id");

b.ToTable("Guests");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.HitEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("CapturedData")
.HasColumnType("TEXT");

b.Property<string>("ConfigCategory")
.HasColumnType("TEXT");
b.Property<string>("ConfigId")
.HasColumnType("TEXT");

b.Property<string>("ConfigName")
.HasColumnType("TEXT");

b.Property<string>("Data")
.HasColumnType("TEXT");

b.Property<DateTime>("Date")
.HasColumnType("TEXT");

b.Property<int>("OwnerId")
.HasColumnType("INTEGER");

b.Property<string>("Proxy")
.HasColumnType("TEXT");

b.Property<string>("Type")
.HasColumnType("TEXT");

b.Property<int>("WordlistId")
.HasColumnType("INTEGER");

b.Property<string>("WordlistName")
.HasColumnType("TEXT");

b.HasKey("Id");

b.ToTable("Hits");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.JobEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");

b.Property<string>("JobOptions")
.HasColumnType("TEXT");

b.Property<int>("JobType")
.HasColumnType("INTEGER");

b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.HasKey("Id");

b.HasIndex("OwnerId");
b.ToTable("Jobs");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("Country")
.HasColumnType("TEXT");

b.Property<int?>("GroupId")
.HasColumnType("INTEGER");

b.Property<string>("Host")
.HasColumnType("TEXT");

b.Property<DateTime>("LastChecked")
.HasColumnType("TEXT");

b.Property<string>("Password")
.HasColumnType("TEXT");

b.Property<int>("Ping")
.HasColumnType("INTEGER");

b.Property<int>("Port")
.HasColumnType("INTEGER");

b.Property<int>("Status")
.HasColumnType("INTEGER");

b.Property<int>("Type")
.HasColumnType("INTEGER");

b.Property<string>("Username")
.HasColumnType("TEXT");

b.HasKey("Id");

b.HasIndex("GroupId");

b.ToTable("Proxies");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyGroupEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.HasKey("Id");

b.HasIndex("OwnerId");

b.ToTable("ProxyGroups");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.RecordEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<int>("Checkpoint")
.HasColumnType("INTEGER");

b.Property<string>("ConfigId")
.HasColumnType("TEXT");

b.Property<int>("WordlistId")
.HasColumnType("INTEGER");

b.HasKey("Id");

b.ToTable("Records");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.WordlistEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");

b.Property<string>("FileName")
.HasColumnType("TEXT");

b.Property<string>("Name")
.HasColumnType("TEXT");

b.Property<int?>("OwnerId")
.HasColumnType("INTEGER");

b.Property<string>("Purpose")
.HasColumnType("TEXT");

b.Property<int>("Total")
.HasColumnType("INTEGER");

b.Property<string>("Type")
.HasColumnType("TEXT");

b.HasKey("Id");
b.HasIndex("OwnerId");

b.ToTable("Wordlists");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.JobEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany()
.HasForeignKey("OwnerId");

b.Navigation("Owner");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.ProxyGroupEntity", "Group")
.WithMany("Proxies")
.HasForeignKey("GroupId")
.OnDelete(DeleteBehavior.Cascade);

b.Navigation("Group");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyGroupEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany("ProxyGroups")
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.SetNull);

b.Navigation("Owner");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.WordlistEntity", b =>
{
b.HasOne("OpenBullet2.Core.Entities.GuestEntity", "Owner")
.WithMany()
.HasForeignKey("OwnerId");

b.Navigation("Owner");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.GuestEntity", b =>
{
b.Navigation("ProxyGroups");
});

modelBuilder.Entity("OpenBullet2.Core.Entities.ProxyGroupEntity", b =>
{
b.Navigation("Proxies");
});
#pragma warning restore 612, 618
}
}
}
File: Models/Data/CombinationsDataPoolOptions.cs
■using RuriLib.Models.Data.DataPools;

namespace OpenBullet2.Core.Models.Data;

/// <summary>
/// Options for a <see cref="CombinationsDataPool"/>.
/// </summary>
public class CombinationsDataPoolOptions : DataPoolOptions
{
/// <summary>
/// The possible characters that can be in a combination, one after the other without separators.
/// </summary>
public string CharSet { get; set; } = "0123456789";

/// <summary>
/// The length of the combinations to generate.
/// </summary>
public int Length { get; set; } = 4;

/// <summary>
/// The Wordlist Type.
/// </summary>
public string WordlistType { get; set; } = "Default";
}

File: Models/Data/DataPoolOptions.cs
■using RuriLib.Models.Data;

namespace OpenBullet2.Core.Models.Data;

/// <summary>
/// Base class for options of a <see cref="DataPool"/>.
/// </summary>
public abstract class DataPoolOptions
{

File: Models/Data/FileDataPoolOptions.cs
■using RuriLib.Models.Data.DataPools;
using System.Runtime.InteropServices;

namespace OpenBullet2.Core.Models.Data;

/// <summary>
/// Options for a <see cref="FileDataPool"/>.
/// </summary>
public class FileDataPoolOptions : DataPoolOptions
{
private string fileName = null;

/// <summary>
/// The path to the file on disk.
/// </summary>
public string FileName
{
get => fileName;
set
{
// Double quotes in file names are not allowed in Windows, but they are included
// at the start and end of the file path if you copy/paste it from some programs,
// so we need to remove them, otherwise it will not find the file.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Remove the double quotes from the file
fileName = value.Replace("\"", "");
}
else
{
fileName = value;
}
}
}

/// <summary>
/// The Wordlist Type.
/// </summary>
public string WordlistType { get; set; } = "Default";
}
File: Models/Data/InfiniteDataPoolOptions.cs
■using RuriLib.Models.Data.DataPools;

namespace OpenBullet2.Core.Models.Data;

/// <summary>
/// Options for an <see cref="InfiniteDataPool"/>.
/// </summary>
public class InfiniteDataPoolOptions : DataPoolOptions
{
/// <summary>
/// The Wordlist Type.
/// </summary>
public string WordlistType { get; set; } = "Default";
}

File: Models/Data/RangeDataPoolOptions.cs
■using RuriLib.Models.Data.DataPools;

namespace OpenBullet2.Core.Models.Data;

/// <summary>
/// Options for a <see cref="RangeDataPool"/>.
/// </summary>
public class RangeDataPoolOptions : DataPoolOptions
{
/// <summary>
/// The start of the range.
/// </summary>
public long Start { get; set; } = 0;

/// <summary>
/// The length of the range.
/// </summary>
public int Amount { get; set; } = 100;

/// <summary>
/// The entity of the interval between elements.
/// </summary>
public int Step { get; set; } = 1;

/// <summary>
/// Whether to pad numbers with zeroes basing on the number
/// of digits of the biggest number to generate.
/// </summary>
public bool Pad { get; set; } = false;

/// <summary>
/// The Wordlist Type.
/// </summary>
public string WordlistType { get; set; } = "Default";
}
File: Models/Data/WordlistDataPoolOptions.cs
■using RuriLib.Models.Data.DataPools;

namespace OpenBullet2.Core.Models.Data;

/// <summary>
/// Options for a <see cref="WordlistDataPool"/>.
/// </summary>
public class WordlistDataPoolOptions : DataPoolOptions
{
/// <summary>
/// The ID of the Wordlist in the repository.
/// </summary>
public int WordlistId { get; set; } = -1;
}

File: Models/Data/WordlistFactory.cs
■using OpenBullet2.Core.Entities;
using RuriLib.Exceptions;
using RuriLib.Models.Data;
using RuriLib.Services;
using System.Linq;

namespace OpenBullet2.Core.Models.Data;

/// <summary>
/// A factory that creates a <see cref="Wordlist"/> from a <see cref="WordlistEntity"/>.
/// </summary>
public class WordlistFactory
{
private readonly RuriLibSettingsService ruriLibSettings;

public WordlistFactory(RuriLibSettingsService ruriLibSettings)


{
this.ruriLibSettings = ruriLibSettings;
}

/// <summary>
/// Creates a <see cref="Wordlist"/> from a <see cref="WordlistEntity"/>.
/// </summary>
public Wordlist FromEntity(WordlistEntity entity)
{
var wordlistType = ruriLibSettings.Environment.WordlistTypes
.FirstOrDefault(w => w.Name == entity.Type);

if (wordlistType == null)
{
throw new InvalidWordlistTypeException(entity.Type);
}

var wordlist = new Wordlist(entity.Name, entity.FileName, wordlistType, entity.Purpose, false)


{
Id = entity.Id,
Total = entity.Total
};

return wordlist;
}
}
File: Models/Hits/CustomWebhookHitOutputOptions.cs
■using RuriLib.Models.Hits.HitOutputs;

namespace OpenBullet2.Core.Models.Hits;

/// <summary>
/// Options for a <see cref="CustomWebhookHitOutput"/>.
/// </summary>
public class CustomWebhookHitOutputOptions : HitOutputOptions
{
/// <summary>
/// The URL of the remote webhook.
/// </summary>
public string Url { get; set; } = "http://mycustomwebhook.com";

/// <summary>
/// The username to send inside the body of the data, to identify who
/// sent the data to the webhook.
/// </summary>
public string User { get; set; } = "Anonymous";

/// <summary>
/// Whether to only send proper hits (SUCCESS status) to the webhook.
/// </summary>
public bool OnlyHits { get; set; } = true;
}

File: Models/Hits/DatabaseHitOutput.cs
■using OpenBullet2.Core.Services;
using RuriLib.Models.Hits;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Models.Hits;

/// <summary>
/// A hit output that stores hits to a database.
/// </summary>
public class DatabaseHitOutput : IHitOutput
{
private readonly HitStorageService hitStorage;

public DatabaseHitOutput(HitStorageService hitStorage)


{
this.hitStorage = hitStorage;
}

/// <inheritdoc/>
public Task Store(Hit hit)
=> hitStorage.StoreAsync(hit);
}
File: Models/Hits/DatabaseHitOutputOptions.cs
■namespace OpenBullet2.Core.Models.Hits;

/// <summary>
/// Options for a <see cref="DatabaseHitOutput"/>.
/// </summary>
public class DatabaseHitOutputOptions : HitOutputOptions
{

File: Models/Hits/DiscordWebhookHitOutputOptions.cs
■using RuriLib.Models.Hits.HitOutputs;

namespace OpenBullet2.Core.Models.Hits;

/// <summary>
/// Options for a <see cref="DiscordWebhookHitOutput"/>.
/// </summary>
public class DiscordWebhookHitOutputOptions : HitOutputOptions
{
/// <summary>
/// The URL of the webhook.
/// </summary>
public string Webhook { get; set; } = string.Empty;

/// <summary>
/// The username to use when sending the message.
/// </summary>
public string Username { get; set; } = string.Empty;

/// <summary>
/// The URL of the avatar picture to use when sending the message.
/// </summary>
public string AvatarUrl { get; set; } = string.Empty;

/// <summary>
/// Whether to only send proper hits (SUCCESS status) to the webhook.
/// </summary>
public bool OnlyHits { get; set; } = true;
}

File: Models/Hits/FileSystemHitOutputOptions.cs
■using RuriLib.Models.Hits.HitOutputs;
namespace OpenBullet2.Core.Models.Hits;

/// <summary>
/// Options for a <see cref="FileSystemHitOutput"/>.
/// </summary>
public class FileSystemHitOutputOptions : HitOutputOptions
{
/// <summary>
/// The parent directory inside which the text files will be created.
/// </summary>
public string BaseDir { get; set; } = "Hits";
}
File: Models/Hits/HitOutputFactory.cs
■using OpenBullet2.Core.Services;
using RuriLib.Models.Hits;
using RuriLib.Models.Hits.HitOutputs;
using System;

namespace OpenBullet2.Core.Models.Hits;

/// <summary>
/// A factory that creates an <see cref="IHitOutput"/> from <see cref="HitOutputOptions"/>.
/// </summary>
public class HitOutputFactory
{
private readonly HitStorageService hitStorage;

public HitOutputFactory(HitStorageService hitStorage)


{
this.hitStorage = hitStorage;
}

/// <summary>
/// Creates an <see cref="IHitOutput"/> from <see cref="HitOutputOptions"/>.
/// </summary>
public IHitOutput FromOptions(HitOutputOptions options)
{
IHitOutput output = options switch
{
DatabaseHitOutputOptions _ => new DatabaseHitOutput(hitStorage),
FileSystemHitOutputOptions x => new FileSystemHitOutput(x.BaseDir),
DiscordWebhookHitOutputOptions x => new DiscordWebhookHitOutput(x.Webhook, x.Username, x.AvatarUrl),
TelegramBotHitOutputOptions x => new TelegramBotHitOutput(x.Token, x.ChatId),
CustomWebhookHitOutputOptions x => new CustomWebhookHitOutput(x.Url, x.User),
_ => throw new NotImplementedException()
};

return output;
}
}

File: Models/Hits/HitOutputOptions.cs
■using RuriLib.Models.Hits;

namespace OpenBullet2.Core.Models.Hits;

/// <summary>
/// Base class for options of an <see cref="IHitOutput"/>.
/// </summary>
public abstract class HitOutputOptions
{

}
File: Models/Hits/TelegramBotHitOutputOptions.cs
■using RuriLib.Models.Hits.HitOutputs;

namespace OpenBullet2.Core.Models.Hits;

/// <summary>
/// Options for a <see cref="TelegramBotHitOutput"/>.
/// </summary>
public class TelegramBotHitOutputOptions : HitOutputOptions
{
/// <summary>
/// The authentication token.
/// </summary>
public string Token { get; set; } = string.Empty;

/// <summary>
/// The ID of the telegram chat.
/// </summary>
public long ChatId { get; set; } = 0;

/// <summary>
/// Whether to only send proper hits (SUCCESS status) to the webhook.
/// </summary>
public bool OnlyHits { get; set; } = true;
}

File: Models/Jobs/JobOptions.cs
■using RuriLib.Models.Jobs;
using RuriLib.Models.Jobs.StartConditions;

namespace OpenBullet2.Core.Models.Jobs;

/// <summary>
/// Base class for options of a <see cref="Job"/>.
/// </summary>
public abstract class JobOptions
{
/// <summary>
/// The condition that needs to be verified in order to start the job.
/// </summary>
public StartCondition StartCondition { get; set; } = new RelativeTimeStartCondition();

/// <summary>
/// The name of the job.
/// </summary>
public string Name { get; set; } = string.Empty;
}
File: Models/Jobs/JobOptionsFactory.cs
■using OpenBullet2.Core.Models.Hits;
using OpenBullet2.Core.Models.Proxies;
using RuriLib.Helpers;
using RuriLib.Models.Jobs.StartConditions;
using System;
using System.Collections.Generic;

namespace OpenBullet2.Core.Models.Jobs;

/// <summary>
/// A factory that creates a <see cref="JobOptions"/> object with default values.
/// </summary>
public class JobOptionsFactory
{
/// <summary>
/// Creates a new <see cref="JobOptions"/> object according to the provided <paramref name="type"/>.
/// </summary>
public static JobOptions CreateNew(JobType type)
{
JobOptions options = type switch
{
JobType.MultiRun => MakeMultiRun(),
JobType.ProxyCheck => MakeProxyCheck(),
_ => throw new NotImplementedException()
};

options.StartCondition = new RelativeTimeStartCondition();


return options;
}

private static MultiRunJobOptions MakeMultiRun() => new()


{
HitOutputs = new List<HitOutputOptions> { new DatabaseHitOutputOptions() },
ProxySources = new List<ProxySourceOptions> { new GroupProxySourceOptions() { GroupId = -1 } }
};

private static ProxyCheckJobOptions MakeProxyCheck() => new ProxyCheckJobOptions


{
CheckOutput = new DatabaseProxyCheckOutputOptions()
};

public static JobOptions CloneExistant(JobOptions options) => options switch


{
MultiRunJobOptions x => CloneMultiRun(x),
ProxyCheckJobOptions x => Cloner.Clone(x),
_ => throw new NotImplementedException()
};

private static MultiRunJobOptions CloneMultiRun(MultiRunJobOptions options)


{
var newOptions = Cloner.Clone(options);
newOptions.Skip = 0;
return newOptions;
}
}
File: Models/Jobs/JobOptionsWrapper.cs
■namespace OpenBullet2.Core.Models.Jobs;

/// <summary>
/// A wrapper around <see cref="JobOptions"/> for json serialization
/// when saving it to the database.
/// </summary>
public class JobOptionsWrapper
{
public JobOptions Options { get; set; }
}

File: Models/Jobs/JobType.cs
■namespace OpenBullet2.Core.Models.Jobs;

/// <summary>
/// The available job types.
/// </summary>
public enum JobType
{
/// <summary>
/// Used to run a config using multiple bots.
/// </summary>
MultiRun,

/// <summary>
/// Used to check proxies.
/// </summary>
ProxyCheck,

Spider,
Ripper,
PuppeteerUnitTest
}

File: Models/Jobs/MultiRunJobOptions.cs
■using OpenBullet2.Core.Models.Data;
using OpenBullet2.Core.Models.Hits;
using OpenBullet2.Core.Models.Proxies;
using RuriLib.Models.Jobs;
using RuriLib.Models.Proxies;
using System.Collections.Generic;

namespace OpenBullet2.Core.Models.Jobs;

/// <summary>
/// Options for a <see cref="MultiRunJob"/>.
/// </summary>
public class MultiRunJobOptions : JobOptions
{
/// <summary>
/// The ID of the config to use.
/// </summary>
public string ConfigId { get; set; }

/// <summary>
/// The amount of bots that will process the data lines concurrently.
/// </summary>
public int Bots { get; set; } = 1;

/// <summary>
/// The amount of data lines to skip from the start of the data pool.
/// </summary>
public int Skip { get; set; } = 0;

/// <summary>
/// The proxy mode.
/// </summary>
public JobProxyMode ProxyMode { get; set; } = JobProxyMode.Default;

/// <summary>
/// Whether to shuffle the proxies in the pool before starting the job.
/// </summary>
public bool ShuffleProxies { get; set; } = true;

/// <summary>
/// The behaviour that should be applied when no more valid proxies are present in the pool.
/// </summary>
public NoValidProxyBehaviour NoValidProxyBehaviour { get; set; } = NoValidProxyBehaviour.Reloa

/// <summary>
/// How long should proxies be banned for. ONLY use this when <see cref="NoValidProxyBehaviour
/// is set to <see cref="NoValidProxyBehaviour.Unban"/>.
/// </summary>
public int ProxyBanTimeSeconds { get; set; } = 0;

/// <summary>
/// Whether to mark the data lines that are currently being processed as To Check when the job
/// is aborted, in order to know which items weren't properly checked.
/// </summary>
public bool MarkAsToCheckOnAbort { get; set; } = false;

/// <summary>
/// Whether to never ban proxies in any case. Use this for rotating proxy services.
/// </summary>
public bool NeverBanProxies { get; set; } = false;

/// <summary>
/// Whether to allow multiple bots to use the same proxy. Use this for rotating proxy services.
/// </summary>
public bool ConcurrentProxyMode { get; set; } = false;
/// <summary>
/// The amount of seconds that the pool will wait before reloading all proxies from the sources (perio
/// Set it to 0 to disable this behaviour and only allow the pool to reload proxies when all are banned
/// to the value of <see cref="NoValidProxyBehaviour"/>.
/// </summary>
public int PeriodicReloadIntervalSeconds { get; set; } = 0;

/// <summary>
/// The options for the data pool that provides data lines to the job.
/// </summary>
public DataPoolOptions DataPool { get; set; } = new WordlistDataPoolOptions();

/// <summary>
/// The options for the proxy sources that will be used to fill the proxy pool whenever it requests a rel
/// </summary>
public List<ProxySourceOptions> ProxySources { get; set; } = new List<ProxySourceOptions>();

/// <summary>
/// The options for the outputs where hits will be stored.
/// </summary>
public List<HitOutputOptions> HitOutputs { get; set; } = new List<HitOutputOptions>();
}
File: Models/Jobs/ProxyCheckJobOptions.cs
■using OpenBullet2.Core.Models.Proxies;
using OpenBullet2.Core.Models.Settings;
using RuriLib.Models.Jobs;

namespace OpenBullet2.Core.Models.Jobs;

/// <summary>
/// Options for a <see cref="ProxyCheckJob"/>.
/// </summary>
public class ProxyCheckJobOptions : JobOptions
{
/// <summary>
/// The amount of bots that will check the proxies concurrently.
/// </summary>
public int Bots { get; set; } = 1;

/// <summary>
/// The ID of the proxy group to check.
/// </summary>
public int GroupId { get; set; } = -1;

/// <summary>
/// Whether to only check the proxies that were never been tested.
/// </summary>
public bool CheckOnlyUntested { get; set; } = true;

/// <summary>
/// The target site against which proxies should be checked.
/// </summary>
public ProxyCheckTarget Target { get; set; } = null;

/// <summary>
/// The maximum timeout that a valid proxy should have, in milliseconds.
/// </summary>
public int TimeoutMilliseconds { get; set; } = 10000;

/// <summary>
/// The options for the output of a proxy check.
/// </summary>
public ProxyCheckOutputOptions CheckOutput { get; set; }
}

File: Models/Proxies/DBIPProxyGeolocationProvider.cs
■using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using MaxMind.GeoIP2;
using RuriLib.Models.Proxies;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// A provider that uses the free database from https://www.maxmind.com/ to geolocate proxies by IP.
/// </summary>
public class DBIPProxyGeolocationProvider : IProxyGeolocationProvider, IDisposable
{
private readonly DatabaseReader reader;

public DBIPProxyGeolocationProvider(string dbFile)


{
reader = new DatabaseReader(dbFile);
}

/// <inheritdoc/>
public async Task<string> GeolocateAsync(string host)
{
if (!IPAddress.TryParse(host, out var _))
{
var addresses = await Dns.GetHostAddressesAsync(host);

if (addresses.Length > 0)
{
host = addresses.First().MapToIPv4().ToString();
}
}

return reader.Country(host).Country.Name;
}

public void Dispose()


{
reader.Dispose();
GC.SuppressFinalize(this);
}
}
File: Models/Proxies/DatabaseProxyCheckOutput.cs
■using OpenBullet2.Core.Repositories;
using RuriLib.Models.Proxies;
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// A proxy check output that writes proxies to an <see cref="IProxyRepository"/>.
/// </summary>
public class DatabaseProxyCheckOutput : IProxyCheckOutput, IDisposable
{
private readonly IServiceScope _scope;
private readonly IProxyRepository _proxyRepo;
private readonly SemaphoreSlim _semaphore;

public DatabaseProxyCheckOutput(IServiceScopeFactory scopeFactory)


{
_scope = scopeFactory.CreateScope();
_proxyRepo = _scope.ServiceProvider.GetRequiredService<IProxyRepository>();
_semaphore = new SemaphoreSlim(1, 1);
}

/// <inheritdoc/>
public async Task StoreAsync(Proxy proxy)
{
try
{
var entity = await _proxyRepo.GetAsync(proxy.Id);
entity.Country = proxy.Country;
entity.LastChecked = proxy.LastChecked;
entity.Ping = proxy.Ping;
entity.Status = proxy.WorkingStatus;

// Only allow updating one proxy at a time (multiple threads should


// not use the same DbContext at the same time).
await _semaphore.WaitAsync();

try
{
await _proxyRepo.UpdateAsync(entity);
}
finally
{
_semaphore.Release();
}
}
catch (Exception ex)
{
/*
* If we are here it means a few possible things
* - we deleted the job but the parallelizer was still running
* - the original proxy was deleted (e.g. from the proxy tab)
* - the scope was disposed for some reason
*
* In any case we don't want to save anything to the database.
*/

// TODO: Turn this into a log message using a logger


Console.WriteLine($"Error while saving proxy {proxy.Id} to the database: {ex.Message}");
}
}

public void Dispose()


{
_semaphore?.Dispose();
_scope?.Dispose();
GC.SuppressFinalize(this);
}
}
File: Models/Proxies/DatabaseProxyCheckOutputOptions.cs
■namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// Options for a <see cref="DatabaseProxyCheckOutput"/>.
/// </summary>
public class DatabaseProxyCheckOutputOptions : ProxyCheckOutputOptions
{

File: Models/Proxies/FileProxySourceOptions.cs
■using RuriLib.Models.Proxies;
using RuriLib.Models.Proxies.ProxySources;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// Options for a <see cref="FileProxySource"/>
/// </summary>
public class FileProxySourceOptions : ProxySourceOptions
{
/// <summary>
/// The path to the file where proxies are stored in a UTF-8 text format, one per line,
/// in a format that is supported by OB2.
/// </summary>
public string FileName { get; set; } = string.Empty;

/// <summary>
/// The default proxy type when not specified by the format of the proxy.
/// </summary>
public ProxyType DefaultType { get; set; } = ProxyType.Http;
}

File: Models/Proxies/GroupProxySourceOptions.cs
■using OpenBullet2.Core.Models.Proxies.Sources;
using OpenBullet2.Core.Repositories;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// Options for a <see cref="GroupProxySource"/>
/// </summary>
public class GroupProxySourceOptions : ProxySourceOptions
{
/// <summary>
/// The ID of the proxy group, as stored in the <see cref="IProxyGroupRepository"/>.
/// </summary>
public int GroupId { get; set; } = -1;
}
File: Models/Proxies/ProxyCheckOutputFactory.cs
■using RuriLib.Models.Proxies;
using System;
using Microsoft.Extensions.DependencyInjection;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// Factory that creates a <see cref="IProxyCheckOutput"/> from the <see cref="ProxyCheckOutputOptions"/>.
/// </summary>
public class ProxyCheckOutputFactory
{
private readonly IServiceScopeFactory _scopeFactory;

/// <summary></summary>
public ProxyCheckOutputFactory(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}

/// <summary>
/// Creates a <see cref="IProxyCheckOutput"/> from the <see cref="ProxyCheckOutputOptions"/>.
/// </summary>
public IProxyCheckOutput FromOptions(ProxyCheckOutputOptions options)
{
IProxyCheckOutput output = options switch
{
DatabaseProxyCheckOutputOptions _ => new DatabaseProxyCheckOutput(_scopeFactory),
_ => throw new NotImplementedException()
};

return output;
}
}

File: Models/Proxies/ProxyCheckOutputOptions.cs
■using RuriLib.Models.Proxies;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// Base class for options of an <see cref="IProxyCheckOutput"/>.
/// </summary>
public abstract class ProxyCheckOutputOptions
{

}
File: Models/Proxies/ProxyFactory.cs
■using OpenBullet2.Core.Entities;
using RuriLib.Models.Proxies;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// Factory that creates a <see cref="Proxy"/> from a <see cref="ProxyEntity"/>.
/// </summary>
public class ProxyFactory
{
/// <summary>
/// Creates a <see cref="Proxy"/> from a <see cref="ProxyEntity"/>.
/// </summary>
public static Proxy FromEntity(ProxyEntity entity)
=> new(entity.Host, entity.Port, entity.Type, entity.Username, entity.Password)
{
Id = entity.Id,
Country = entity.Country,
WorkingStatus = entity.Status,
LastChecked = entity.LastChecked,
Ping = entity.Ping
};
}

File: Models/Proxies/ProxySourceOptions.cs
■using RuriLib.Models.Proxies;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// Base class for the options of a <see cref="ProxySource"/>
/// </summary>
public abstract class ProxySourceOptions
{

File: Models/Proxies/RemoteProxySourceOptions.cs
■using RuriLib.Models.Proxies;
using RuriLib.Models.Proxies.ProxySources;

namespace OpenBullet2.Core.Models.Proxies;

/// <summary>
/// Options for a <see cref="RemoteProxySource"/>
/// </summary>
public class RemoteProxySourceOptions : ProxySourceOptions
{
/// <summary>
/// The URL to query in order to retrieve the proxies.
/// The API should return a text-based response with one proxy per line, in a format supported by OB
/// </summary>
public string Url { get; set; } = string.Empty;

/// <summary>
/// The default proxy type when not specified by the format of the proxy.
/// </summary>
public ProxyType DefaultType { get; set; } = ProxyType.Http;
}
File: Models/Proxies/Sources/GroupProxySource.cs
■using OpenBullet2.Core.Repositories;
using OpenBullet2.Core.Services;
using RuriLib.Models.Proxies;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Models.Proxies.Sources;

/// <summary>
/// A proxy source that gets proxies from a group of a <see cref="IProxyGroupRepository"/>.
/// </summary>
public class GroupProxySource : ProxySource, IDisposable
{
private readonly ProxyReloadService reloadService;

/// <summary>
/// The ID of the group in the <see cref="IProxyGroupRepository"/>.
/// </summary>
public int GroupId { get; set; }

public GroupProxySource(int groupId, ProxyReloadService reloadService)


{
GroupId = groupId;
this.reloadService = reloadService;
}

/// <inheritdoc/>
public async override Task<IEnumerable<Proxy>> GetAllAsync(CancellationToken cancellationToken = default)
=> await reloadService.ReloadAsync(GroupId, UserId, cancellationToken).ConfigureAwait(false);

public override void Dispose()


{
base.Dispose();
GC.SuppressFinalize(this);
}
}

File: Models/Settings/CustomizationSettings.cs
■namespace OpenBullet2.Core.Models.Settings;

/// <summary>
/// Settings related to the appearance of the OpenBullet2 GUI.
/// </summary>
public class CustomizationSettings
{
/// <summary>
/// The theme to use. Themes are included in separate files and identified
/// by their name. Web UI only.
/// </summary>
public string Theme { get; set; } = "Default";

/// <summary>
/// The theme to use for the Monaco editor. Web UI only.
/// </summary>
public string MonacoTheme { get; set; } = "vs-dark";

/// <summary>
/// Whether to wrap words at viewport width.
/// </summary>
public bool WordWrap { get; set; } = false;

/// <summary>
/// The main background color. Native UI only.
/// </summary>
public string BackgroundMain { get; set; } = "#222";

/// <summary>
/// The background color for inputs. Native UI only.
/// </summary>
public string BackgroundInput { get; set; } = "#282828";

/// <summary>
/// The secondary background color. Native UI only.
/// </summary>
public string BackgroundSecondary { get; set; } = "#111";

/// <summary>
/// The main foreground color. Native UI only.
/// </summary>
public string ForegroundMain { get; set; } = "#DCDCDC";

/// <summary>
/// The foreground color for inputs. Native UI only.
/// </summary>
public string ForegroundInput { get; set; } = "#DCDCDC";

/// <summary>
/// The foreground color for hits. Native UI only.
/// </summary>
public string ForegroundGood { get; set; } = "#ADFF2F";

/// <summary>
/// The foreground color for fails. Native UI only.
/// </summary>
public string ForegroundBad { get; set; } = "#FF6347";

/// <summary>
/// The foreground color for custom hits. Native UI only.
/// </summary>
public string ForegroundCustom { get; set; } = "#FF8C00";

/// <summary>
/// The foreground color for retries. Native UI only.
/// </summary>
public string ForegroundRetry { get; set; } = "#FFFF00";

/// <summary>
/// The foreground color for bans. Native UI only.
/// </summary>
public string ForegroundBanned { get; set; } = "#DDA0DD";

/// <summary>
/// The foreground color for hits to check. Native UI only.
/// </summary>
public string ForegroundToCheck { get; set; } = "#7FFFD4";

/// <summary>
/// The foreground color for selected menu items. Native UI only.
/// </summary>
public string ForegroundMenuSelected { get; set; } = "#1E90FF";

/// <summary>
/// The color of success buttons. Native UI only.
/// </summary>
public string SuccessButton { get; set; } = "#2f5738";

/// <summary>
/// The color of primary buttons. Native UI only.
/// </summary>
public string PrimaryButton { get; set; } = "#3b3a63";

/// <summary>
/// The color of warning buttons. Native UI only.
/// </summary>
public string WarningButton { get; set; } = "#7a552a";

/// <summary>
/// The color of danger buttons. Native UI only.
/// </summary>
public string DangerButton { get; set; } = "#693838";

/// <summary>
/// The foreground color of buttons. Native UI only.
/// </summary>
public string ForegroundButton { get; set; } = "#DCDCDC";

/// <summary>
/// The background color of buttons. Native UI only.
/// </summary>
public string BackgroundButton { get; set; } = "#282828";

/// <summary>
/// The path to the background image. Native UI only.
/// </summary>
public string BackgroundImagePath { get; set; } = "";

/// <summary>
/// The opacity of the background image (from 0 to 100). Native UI only.
/// </summary>
public double BackgroundOpacity { get; set; } = 100;

/// <summary>
/// Whether to play a sound when a hit is found.
/// </summary>
public bool PlaySoundOnHit { get; set; } = false;
}
File: Models/Settings/GeneralSettings.cs
■using System.Collections.Generic;

namespace OpenBullet2.Core.Models.Settings;

/// <summary>
/// The available sections in which every part of a config can be edited.
/// </summary>
public enum ConfigSection
{
Metadata,
Readme,
Stacker,
LoliCode,
Settings,
CSharpCode,
LoliScript
}

/// <summary>
/// The level of detail when displaying information about a job.
/// </summary>
public enum JobDisplayMode
{
Standard = 0,
Detailed = 1
}

/// <summary>
/// A target to be used as proxy check.
/// </summary>
public class ProxyCheckTarget
{
/// <summary>
/// The URL of the website that the proxy will send a GET query to.
/// </summary>
public string Url { get; set; }

/// <summary>
/// A keyword that must be present in the HTTP response body in order
/// to mark the proxy as working. Case sensitive.
/// </summary>
public string SuccessKey { get; set; }

public ProxyCheckTarget(string url = "https://google.com", string successKey = "title>Google")


{
Url = url;
SuccessKey = successKey;
}

public override string ToString() => $"{Url} | {SuccessKey}";


}

/// <summary>
/// A custom LoliCode snippet for editor autocompletion.
/// </summary>
public class CustomSnippet
{
/// <summary>
/// The name of the snippet which will need to be typed (at least partially) to get the suggestion.
/// </summary>
public string Name { get; set; }

/// <summary>
/// The body of the snippet which will be inserted by the editor.
/// </summary>
public string Body { get; set; } = "The body of your snippet";

/// <summary>
/// The description of what the snippet does.
/// </summary>
public string Description { get; set; }
}

/// <summary>
/// General settings of OpenBullet 2.
/// </summary>
public class GeneralSettings
{
/// <summary>
/// Which page to navigate to on config load.
/// </summary>
public ConfigSection ConfigSectionOnLoad { get; set; } = ConfigSection.Stacker;

/// <summary>
/// Whether to automatically set the recommended amount of bots specified by a config
/// when selecting a config in a job.
/// </summary>
public bool AutoSetRecommendedBots { get; set; } = true;

/// <summary>
/// Whether to output a warning upon quitting or loading a new config when
/// the previous one was edited but not saved.
/// </summary>
public bool WarnConfigNotSaved { get; set; } = true;

/// <summary>
/// The default author to use when creating new configs.
/// </summary>
public string DefaultAuthor { get; set; } = "Anonymous";

/// <summary>
/// Whether to display the job log in the interface.
/// </summary>
public bool EnableJobLogging { get; set; } = false;

/// <summary>
/// The maximum amount of log entries that are saved in memory for each job.
/// </summary>
public int LogBufferSize { get; set; } = 30;

/// <summary>
/// Whether to ignore the wordlist name when removing duplicate hits (so that similar hits
/// obtained using different wordlists are treated as duplicate).
/// </summary>
public bool IgnoreWordlistNameOnHitsDedupe { get; set; } = false;

/// <summary>
/// The available targets that can be used to check proxies.
/// </summary>
public List<ProxyCheckTarget> ProxyCheckTargets { get; set; }

/// <summary>
/// The default display mode for job information.
/// </summary>
public JobDisplayMode DefaultJobDisplayMode { get; set; } = JobDisplayMode.Standard;

/// <summary>
/// The refresh interval for periodically displaying a job's progress and information
/// (in milliseconds).
/// </summary>
public int JobUpdateInterval { get; set; } = 1000;

/// <summary>
/// The refresh interval for periodically displaying all jobs' progress and information
/// in the job manager page (in milliseconds).
/// </summary>
public int JobManagerUpdateInterval { get; set; } = 1000;

/// <summary>
/// Whether to group captured variables together in the variables log of the debugger.
/// </summary>
public bool GroupCapturesInDebugger { get; set; } = false;

/// <summary>
/// The localization culture for the UI.
/// </summary>
public string Culture { get; set; } = "en";

/// <summary>
/// Custom user-defined snippets for editor autocompletion.
/// </summary>
public List<CustomSnippet> CustomSnippets { get; set; } = new();
}
File: Models/Settings/OpenBulletSettings.cs
■namespace OpenBullet2.Core.Models.Settings;

/// <summary>
/// Settings for the OpenBullet 2 application.
/// </summary>
public class OpenBulletSettings
{
/// <summary>
/// General settings.
/// </summary>
public GeneralSettings GeneralSettings { get; set; } = new();

/// <summary>
/// Settings related to remote repositories.
/// </summary>
public RemoteSettings RemoteSettings { get; set; } = new();

/// <summary>
/// Settings related to security.
/// </summary>
public SecuritySettings SecuritySettings { get; set; } = new();

/// <summary>
/// Settings related to the appearance of the UI.
/// </summary>
public CustomizationSettings CustomizationSettings { get; set; } = new();
}

File: Models/Settings/RemoteSettings.cs
■using System.Collections.Generic;

namespace OpenBullet2.Core.Models.Settings;

/// <summary>
/// A remote endpoint that hosts configs.
/// </summary>
public class RemoteConfigsEndpoint
{
/// <summary>
/// The URL of the endpoint.
/// </summary>
public string Url { get; set; } = "http://x.x.x.x:5000/api/shared/configs/ENDPOINT_NAME";

/// <summary>
/// The API key to use to access the endpoint.
/// </summary>
public string ApiKey { get; set; } = "MY_API_KEY";
}

/// <summary>
/// Settings related to remote endpoints.
/// </summary>
public class RemoteSettings
{
/// <summary>
/// Remote endpoints from which configs will be fetched by the config manager
/// upon reload.
/// </summary>
public List<RemoteConfigsEndpoint> ConfigsEndpoints { get; set; } = new();
}
File: Models/Settings/SecuritySettings.cs
■using System.Security.Cryptography;

namespace OpenBullet2.Core.Models.Settings;

/// <summary>
/// Settings related to security.
/// </summary>
public class SecuritySettings
{
/// <summary>
/// Whether to allow OpenBullet2 (mainly blocks and file system viewer) to access
/// the whole system or only the UserData folder and its subfolders.
/// </summary>
public bool AllowSystemWideFileAccess { get; set; } = false;

/// <summary>
/// Whether to require admin login when accessing the UI. Use this when exposing
/// an OpenBullet 2 instance on the unprotected internet.
/// </summary>
public bool RequireAdminLogin { get; set; } = false;

/// <summary>
/// The username for the admin user.
/// </summary>
public string AdminUsername { get; set; } = "admin";

/// <summary>
/// The bcrypt hash of the admin user's password.
/// </summary>
public string AdminPasswordHash { get; set; }

/// <summary>
/// The API key that the admin can use to authenticate to the API.
/// If empty, the admin will not be able to use the API.
/// </summary>
public string AdminApiKey { get; set; } = string.Empty;

/// <summary>
/// The JWT key that this application will use when issuing authentication tokens.
/// For security reasons this should be randomly generated via the <see cref="GenerateJwtKey"/> method.
/// </summary>
public byte[] JwtKey { get; set; }

/// <summary>
/// The number of hours that the admin session will last before requiring another login.
/// </summary>
public int AdminSessionLifetimeHours { get; set; } = 24;

/// <summary>
/// The number of hours that the guest session will last before requiring another login.
/// </summary>
public int GuestSessionLifetimeHours { get; set; } = 24;
/// <summary>
/// Whether to use HTTPS redirection when the application is accessed via HTTP.
/// </summary>
public bool HttpsRedirect { get; set; } = false;

/// <summary>
/// Generates a random JWT key to use in order to sign JWT tokens issued by the application
/// to both the admin and guests.
/// </summary>
public SecuritySettings GenerateJwtKey()
{
JwtKey = RandomNumberGenerator.GetBytes(64);
return this;
}

/// <summary>
/// Sets a new admin password.
/// </summary>
public SecuritySettings SetupAdminPassword(string password)
{
AdminPasswordHash = BCrypt.Net.BCrypt.HashPassword(password);
return this;
}
}
File: Models/Sharing/Endpoint.cs
■using System.Collections.Generic;

namespace OpenBullet2.Core.Models.Sharing;

/// <summary>
/// A sharing endpoint that will be used to share configs with other
/// OpenBullet 2 instances.
/// </summary>
public class Endpoint
{
/// <summary>
/// The route for the endpoint.
/// </summary>
public string Route { get; set; } = "configs";

/// <summary>
/// The API keys that are allowed to access the endpoint. When requesting configs
/// from this endpoint, users will send their API key inside the HTTP request.
/// </summary>
public List<string> ApiKeys { get; set; } = new();

/// <summary>
/// The IDs of the configs that will be delivered by the server to the clients.
/// </summary>
public List<string> ConfigIds { get; set; } = new();
}

File: Repositories/DbGuestRepository.cs
■using OpenBullet2.Core.Entities;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores guests to a database.
/// </summary>
public class DbGuestRepository : DbRepository<GuestEntity>, IGuestRepository
{
public DbGuestRepository(ApplicationDbContext context)
: base(context)
{

}
}
File: Repositories/DbHitRepository.cs
■using Microsoft.EntityFrameworkCore;
using OpenBullet2.Core.Entities;
using OpenBullet2.Core.Extensions;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores hits to a database.
/// </summary>
public class DbHitRepository : DbRepository<HitEntity>, IHitRepository
{
public DbHitRepository(ApplicationDbContext context)
: base(context)
{

/// <inheritdoc/>
public async Task PurgeAsync() => await context.Database
.ExecuteSqlRawAsync($"DELETE FROM {nameof(ApplicationDbContext.Hits)}");

/// <inheritdoc/>
public async Task<long> CountAsync() => await context.Hits.CountAsync();

public async override Task UpdateAsync(HitEntity entity, CancellationToken cancellationToken = default)


{
context.DetachLocal<HitEntity>(entity.Id);
context.Entry(entity).State = EntityState.Modified;
await base.UpdateAsync(entity, cancellationToken).ConfigureAwait(false);
}

public async override Task UpdateAsync(IEnumerable<HitEntity> entities, CancellationToken cancellationToken = defau


{
foreach (var entity in entities)
{
context.DetachLocal<HitEntity>(entity.Id);
context.Entry(entity).State = EntityState.Modified;
}

await base.UpdateAsync(entities, cancellationToken).ConfigureAwait(false);


}
}
File: Repositories/DbJobRepository.cs
■using Microsoft.EntityFrameworkCore;
using OpenBullet2.Core.Entities;
using OpenBullet2.Core.Extensions;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores jobs to a database.
/// </summary>
public class DbJobRepository : DbRepository<JobEntity>, IJobRepository
{
public DbJobRepository(ApplicationDbContext context)
: base(context)
{

public override async Task<JobEntity> GetAsync(int id, CancellationToken cancellationToken = default)


{
var entity = await context.Jobs
.Include(j => j.Owner)
.FirstOrDefaultAsync(j => j.Id == id, cancellationToken);
await context.Entry(entity).ReloadAsync(cancellationToken);
return entity;
}

public async override Task UpdateAsync(JobEntity entity, CancellationToken cancellationToken = default)


{
context.DetachLocal<JobEntity>(entity.Id);
context.Entry(entity).State = EntityState.Modified;
await base.UpdateAsync(entity, cancellationToken).ConfigureAwait(false);
}

public async override Task UpdateAsync(IEnumerable<JobEntity> entities, CancellationToken cancellationToken = defa


{
foreach (var entity in entities)
{
context.DetachLocal<JobEntity>(entity.Id);
context.Entry(entity).State = EntityState.Modified;
}

await base.UpdateAsync(entities, cancellationToken).ConfigureAwait(false);


}

/// <inheritdoc/>
public void Purge() => context.Database.ExecuteSqlRaw($"DELETE FROM {nameof(ApplicationDbContext.Jobs)}");
}
File: Repositories/DbProxyGroupRepository.cs
■using Microsoft.EntityFrameworkCore;
using OpenBullet2.Core.Entities;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores proxy groups to a database.
/// </summary>
public class DbProxyGroupRepository : DbRepository<ProxyGroupEntity>, IProxyGroupRepository
{
public DbProxyGroupRepository(ApplicationDbContext context)
: base(context)
{

/// <inheritdoc/>
public async override Task<ProxyGroupEntity> GetAsync(int id, CancellationToken cancellationToken = default)
=> await GetAll().Include(w => w.Owner)
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken)
.ConfigureAwait(false);
}

File: Repositories/DbProxyRepository.cs
■using Microsoft.EntityFrameworkCore;
using OpenBullet2.Core.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores proxies to a database.
/// </summary>
public class DbProxyRepository : DbRepository<ProxyEntity>, IProxyRepository
{
public DbProxyRepository(ApplicationDbContext context)
: base(context)
{

public async override Task UpdateAsync(ProxyEntity entity, CancellationToken cancellationToken = default)


{
context.Entry(entity).State = EntityState.Modified;
await base.UpdateAsync(entity, cancellationToken).ConfigureAwait(false);
}

public async override Task UpdateAsync(IEnumerable<ProxyEntity> entities, CancellationToken ca


{
foreach (var entity in entities)
{
context.Entry(entity).State = EntityState.Modified;
}

await base.UpdateAsync(entities, cancellationToken).ConfigureAwait(false);


}

/// <inheritdoc/>
public async Task<int> RemoveDuplicatesAsync(int groupId)
{
var proxies = await GetAll()
.Where(p => p.Group.Id == groupId)
.ToListAsync();

var duplicates = proxies


.GroupBy(p => new { p.Type, p.Host, p.Port, p.Username, p.Password })
.SelectMany(g => g.Skip(1))
.ToList();

await DeleteAsync(duplicates);

return duplicates.Count;
}
}
File: Repositories/DbRecordRepository.cs
■using OpenBullet2.Core.Entities;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores records to a database.
/// </summary>
public class DbRecordRepository : DbRepository<RecordEntity>, IRecordRepository
{
public DbRecordRepository(ApplicationDbContext context)
: base(context)
{

}
}

File: Repositories/DbRepository.cs
■using Microsoft.EntityFrameworkCore;
using OpenBullet2.Core.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores data to a database.
/// </summary>
/// <typeparam name="T">The type of data to store</typeparam>
public class DbRepository<T> : IRepository<T> where T : Entity
{
protected readonly ApplicationDbContext context;
private readonly SemaphoreSlim _semaphore = new(1, 1);

public DbRepository(ApplicationDbContext context)


{
this.context = context;
}

/// <inheritdoc/>
public async virtual Task AddAsync(T entity, CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

try
{
context.Add(entity);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}

/// <inheritdoc/>
public async virtual Task AddAsync(IEnumerable<T> entities, CancellationToken cancellationToken
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

try
{
context.AddRange(entities);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}

/// <inheritdoc/>
public async virtual Task DeleteAsync(T entity, CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

try
{
context.Remove(entity);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}

/// <inheritdoc/>
public async virtual Task DeleteAsync(IEnumerable<T> entities, CancellationToken cancellationTok
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

try
{
context.RemoveRange(entities);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}
/// <inheritdoc/>
public async virtual Task<T> GetAsync(int id, CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

try
{
return await GetAll()
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}

/// <inheritdoc/>
public virtual IQueryable<T> GetAll()
=> context.Set<T>();

/// <inheritdoc/>
public async virtual Task UpdateAsync(T entity, CancellationToken cancellationToken = default)
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

try
{
context.Update(entity);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}

/// <inheritdoc/>
public async virtual Task UpdateAsync(IEnumerable<T> entities, CancellationToken cancellationTo
{
await _semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);

try
{
context.UpdateRange(entities);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_semaphore.Release();
}
}

/// <inheritdoc/>
public void Attach<TEntity>(TEntity entity) where TEntity : Entity => context.Attach(entity);
}
File: Repositories/DiskConfigRepository.cs
■using RuriLib.Models.Configs;
using RuriLib.Helpers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System;
using RuriLib.Helpers.Transpilers;
using RuriLib.Services;
using RuriLib.Legacy.Configs;
using System.Text;
using OpenBullet2.Core.Exceptions;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores configs on disk.
/// </summary>
public class DiskConfigRepository : IConfigRepository
{
private readonly RuriLibSettingsService _rlSettings;

private string BaseFolder { get; init; }

public DiskConfigRepository(RuriLibSettingsService rlSettings, string baseFolder)


{
_rlSettings = rlSettings;
BaseFolder = baseFolder;
Directory.CreateDirectory(baseFolder);
}

/// <inheritdoc/>
public async Task<IEnumerable<Config>> GetAllAsync()
{
// Try to convert legacy configs automatically before loading
foreach (var file in Directory.GetFiles(BaseFolder).Where(file => file.EndsWith(".loli")))
{
try
{
var id = Path.GetFileNameWithoutExtension(file);
var converted = ConfigConverter.Convert(File.ReadAllText(file), id);
await SaveAsync(converted);
File.Delete(file);
Console.WriteLine($"Converted legacy .loli config ({file}) to the new .opk format");
}
catch
{
Console.WriteLine($"Could not convert legacy .loli config ({file}) to the new .opk format");
}
}

var tasks = Directory.GetFiles(BaseFolder).Where(file => file.EndsWith(".opk"))


.Select(async file =>
{
try
{
return await GetAsync(Path.GetFileNameWithoutExtension(file));
}
catch (Exception ex)
{
Console.WriteLine($"Could not unpack {file} properly: {ex.Message}");
return null;
}
});

var results = await Task.WhenAll(tasks);


return results.Where(r => r != null);
}

/// <inheritdoc/>
public async Task<Config> GetAsync(string id)
{
var file = GetFileName(id);

if (File.Exists(file))
{
using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read);

var config = await ConfigPacker.UnpackAsync(fileStream);


config.Id = id;
return config;
}

throw new FileNotFoundException();


}

/// <inheritdoc/>
public async Task<byte[]> GetBytesAsync(string id)
{
var file = GetFileName(id);

if (File.Exists(file))
{
using FileStream fileStream = new(file, FileMode.Open, FileAccess.Read);
using var ms = new MemoryStream();
await fileStream.CopyToAsync(ms);

return ms.ToArray();
}

throw new FileNotFoundException();


}

/// <inheritdoc/>
public async Task<Config> CreateAsync(string id = null)
{
var config = new Config { Id = id ?? Guid.NewGuid().ToString() };
config.Settings.DataSettings.AllowedWordlistTypes = [
_rlSettings.Environment.WordlistTypes.First().Name
];

await SaveAsync(config);
return config;
}

/// <inheritdoc/>
public async Task UploadAsync(Stream stream, string fileName)
{
var extension = Path.GetExtension(fileName);

// If it's a .opk config


if (extension == ".opk")
{
var config = await ConfigPacker.UnpackAsync(stream);
await File.WriteAllBytesAsync(GetFileName(config), await ConfigPacker.PackAsync(config));
}
// Otherwise it's a .loli config
else if (extension == ".loli")
{
using var ms = new MemoryStream();
stream.CopyTo(ms);
ms.Seek(0, SeekOrigin.Begin);
var content = Encoding.UTF8.GetString(ms.ToArray());
var id = Path.GetFileNameWithoutExtension(fileName);
var converted = ConfigConverter.Convert(content, id);
await SaveAsync(converted);
}
else
{
throw new UnsupportedFileTypeException($"Unsupported file type: {extension}");
}
}

/// <inheritdoc/>
public async Task SaveAsync(Config config)
{
// Update the last modified date
config.Metadata.LastModified = DateTime.Now;

// If it's possible to retrieve the block descriptors, get required plugins


if (config.Mode is ConfigMode.Stack or ConfigMode.LoliCode)
{
try
{
var stack = config.Mode is ConfigMode.Stack
? config.Stack
: Loli2StackTranspiler.Transpile(config.LoliCodeScript);

// Write the required plugins in the config's metadata


config.Metadata.Plugins = stack.Select(b => b.Descriptor.AssemblyFullName)
.Where(n => n != null && !n.Contains("RuriLib")).ToList();
}
catch
{
// Don't do anything, it's not the end of the world if we don't write some metadata ^_^
}
}

await File.WriteAllBytesAsync(GetFileName(config), await ConfigPacker.PackAsync(config));


}

/// <inheritdoc/>
public void Delete(Config config)
{
var file = GetFileName(config);

if (File.Exists(file))
File.Delete(file);
}

private string GetFileName(Config config)


=> GetFileName(config.Id);

private string GetFileName(string id)


=> Path.Combine(BaseFolder, $"{id}.opk").Replace('\\', '/');
}
File: Repositories/HybridWordlistRepository.cs
■using Microsoft.EntityFrameworkCore;
using OpenBullet2.Core.Entities;
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores wordlists to the disk and the database. Files are stored on disk while
/// metadata is stored in a database.
/// </summary>
public class HybridWordlistRepository : IWordlistRepository
{
private readonly string baseFolder;
private readonly ApplicationDbContext context;

public HybridWordlistRepository(ApplicationDbContext context, string baseFolder)


{
this.context = context;
this.baseFolder = baseFolder;
Directory.CreateDirectory(baseFolder);
}

/// <inheritdoc/>
public async Task AddAsync(WordlistEntity entity, CancellationToken cancellationToken = default)
{
// Save it to the DB
context.Add(entity);
await context.SaveChangesAsync(cancellationToken);
}

/// <inheritdoc/>
public async Task AddAsync(WordlistEntity entity, MemoryStream stream,
CancellationToken cancellationToken = default)
{
// Generate a unique filename
var path = Path.Combine(baseFolder, $"{Guid.NewGuid()}.txt");
entity.FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? path.Replace('/', '\\')
: path.Replace('\\', '/');

// Create the file on disk


await File.WriteAllBytesAsync(entity.FileName, stream.ToArray(),
cancellationToken);

// Count the amount of lines


entity.Total = File.ReadLines(entity.FileName).Count();

await AddAsync(entity);
}

/// <inheritdoc/>
public IQueryable<WordlistEntity> GetAll()
=> context.Wordlists;

/// <inheritdoc/>
public async Task<WordlistEntity> GetAsync(
int id, CancellationToken cancellationToken = default)
=> await GetAll().Include(w => w.Owner)
.FirstOrDefaultAsync(e => e.Id == id, cancellationToken: cancellationToken)
.ConfigureAwait(false);

/// <inheritdoc/>
public async Task UpdateAsync(WordlistEntity entity, CancellationToken cancellationToken = defau
{
context.Entry(entity).State = EntityState.Modified;
context.Update(entity);
await context.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public async Task DeleteAsync(WordlistEntity entity, bool deleteFile = false,
CancellationToken cancellationToken = default)
{
if (deleteFile && File.Exists(entity.FileName))
File.Delete(entity.FileName);

context.Remove(entity);
await context.SaveChangesAsync(cancellationToken);
}

/// <inheritdoc/>
public void Purge() => _ = context.Database.ExecuteSqlRaw($"DELETE FROM {nameof(Application

/// <inheritdoc/>
public void Dispose()
{
GC.SuppressFinalize(this);
context?.Dispose();
}
}
File: Repositories/IConfigRepository.cs
■using RuriLib.Models.Configs;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores configs.
/// </summary>
public interface IConfigRepository
{
/// <summary>
/// Creates a new config with a given <paramref name="id"/>.
/// If <paramref name="id"/> is null, a random one will be generated.
/// </summary>
Task<Config> CreateAsync(string id = null);

/// <summary>
/// Deletes a config from the repository.
/// </summary>
void Delete(Config config);

/// <summary>
/// Retrieves and unpacks a config by ID.
/// </summary>
Task<Config> GetAsync(string id);

/// <summary>
/// Retrieves and unpacks all configs from the repository.
/// </summary>
/// <returns></returns>
Task<IEnumerable<Config>> GetAllAsync();

/// <summary>
/// Retrieves the raw bytes of the OPK config from the repository.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Task<byte[]> GetBytesAsync(string id);

/// <summary>
/// Packs and saves a config to the repository.
/// </summary>
Task SaveAsync(Config config);

/// <summary>
/// Saves a packed config (as a raw bytes stream) to the repository.
/// </summary>
Task UploadAsync(Stream stream, string fileName);
}
File: Repositories/IGuestRepository.cs
■using OpenBullet2.Core.Entities;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores guests.
/// </summary>
public interface IGuestRepository : IRepository<GuestEntity>
{
}

File: Repositories/IHitRepository.cs
■using OpenBullet2.Core.Entities;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores hits.
/// </summary>
public interface IHitRepository : IRepository<HitEntity>
{
/// <summary>
/// Deletes all hits from the repository.
/// </summary>
Task PurgeAsync();

/// <summary>
/// Count the number of hits.
/// </summary>
Task<long> CountAsync();
}

File: Repositories/IJobRepository.cs
■using OpenBullet2.Core.Entities;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores jobs.
/// </summary>
public interface IJobRepository : IRepository<JobEntity>
{
/// <summary>
/// Deletes all jobs from the repository.
/// </summary>
void Purge();
}
File: Repositories/IProxyGroupRepository.cs
■using OpenBullet2.Core.Entities;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores proxy groups.
/// </summary>
public interface IProxyGroupRepository : IRepository<ProxyGroupEntity>
{
}

File: Repositories/IProxyRepository.cs
■using OpenBullet2.Core.Entities;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores proxies.
/// </summary>
public interface IProxyRepository : IRepository<ProxyEntity>
{
/// <summary>
/// Removes duplicate proxies that belong to the group with a given <paramref name="groupId"/> from the Proxies table.
/// Duplication is checked on type, host, port, username and password.
/// Returns the number of removed entries.
/// </summary>
Task<int> RemoveDuplicatesAsync(int groupId);
}

File: Repositories/IRecordRepository.cs
■using OpenBullet2.Core.Entities;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores records.
/// </summary>
public interface IRecordRepository : IRepository<RecordEntity>
{
}

File: Repositories/IRepository.cs
■using OpenBullet2.Core.Entities;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores data.
/// </summary>
/// <typeparam name="T">The type of data to store</typeparam>
public interface IRepository<T> where T : Entity
{
// ------
// CREATE
// ------

/// <summary>
/// Adds an <paramref name="entity"/> to the repository.
/// </summary>
Task AddAsync(T entity, CancellationToken cancellationToken = default);

/// <summary>
/// Adds multiple <paramref name="entities"/> to the repository.
/// </summary>
Task AddAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);

// ----
// READ
// ----

/// <summary>
/// Gets an entity by <paramref name="id"/>. Returns null if not found.
/// </summary>
Task<T> GetAsync(int id, CancellationToken cancellationToken = default);

/// <summary>
/// Gets an <see cref="IQueryable{T}"/> of all entities in the repository for further filtering.
/// </summary>
IQueryable<T> GetAll();

// ------
// UPDATE
// ------

/// <summary>
/// Updates an <paramref name="entity"/> in the repository.
/// </summary>
Task UpdateAsync(T entity, CancellationToken cancellationToken = default);

/// <summary>
/// Updates multiple <paramref name="entities"/> in the repository.
/// </summary>
Task UpdateAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);
// ------
// DELETE
// ------

/// <summary>
/// Deletes an <paramref name="entity"/> from the repository.
/// </summary>
Task DeleteAsync(T entity, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes multiple <paramref name="entities"/> from the repository.
/// </summary>
Task DeleteAsync(IEnumerable<T> entities, CancellationToken cancellationToken = default);

/// <summary>
/// Attaches to a given entity so that EF doesn't try to create a new one
/// in a one to many relationship.
/// </summary>
public void Attach<TEntity>(TEntity entity) where TEntity : Entity;
}
File: Repositories/IWordlistRepository.cs
■using OpenBullet2.Core.Entities;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Repositories;

/// <summary>
/// Stores wordlists.
/// </summary>
public interface IWordlistRepository : IDisposable
{
/// <summary>
/// Adds an <paramref name="entity"/> to the repository.
/// </summary>
Task AddAsync(WordlistEntity entity, CancellationToken cancellationToken = default);

/// <summary>
/// Adds an <paramref name="entity"/> to the repository and creates the file as well
/// by reading it from a raw <paramref name="stream"/>.
/// </summary>
Task AddAsync(WordlistEntity entity, MemoryStream stream, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes an <paramref name="entity"/> from the repository.
/// </summary>
/// <param name="deleteFile">Whether to delete the file as well</param>
Task DeleteAsync(WordlistEntity entity, bool deleteFile = false, CancellationToken cancellationToken = default);

/// <summary>
/// Gets an entity from the repository by <paramref name="id"/>.
/// </summary>
Task<WordlistEntity> GetAsync(int id, CancellationToken cancellationToken = default);

/// <summary>
/// Gets an <see cref="IQueryable"/> of all entities for further filtering.
/// </summary>
/// <returns></returns>
IQueryable<WordlistEntity> GetAll();

/// <summary>
/// Updates an <paramref name="entity"/> in the repository.
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
Task UpdateAsync(WordlistEntity entity, CancellationToken cancellationToken = default);

/// <summary>
/// Deletes all wordlists from the repository.
/// </summary>
void Purge();
}
File: Services/ConfigService.cs
■using Microsoft.Scripting.Utils;
using OpenBullet2.Core.Models.Settings;
using OpenBullet2.Core.Repositories;
using RuriLib.Models.Configs;
using System;
using System.Linq;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using System.IO.Compression;
using RuriLib.Helpers;
using System.IO;
using RuriLib.Functions.Conversion;

namespace OpenBullet2.Core.Services;

// TODO: The config service should also be in charge of calling methods of the IConfigRepository
/// <summary>
/// Manages the list of available configs.
/// </summary>
public class ConfigService
{
/// <summary>
/// The list of available configs.
/// </summary>
public List<Config> Configs { get; set; } = new();

/// <summary>
/// Called when a new config is selected.
/// </summary>
public event EventHandler<Config> OnConfigSelected;

/// <summary>
/// Called when all configs from configured remote endpoints are loaded.
/// </summary>
public event EventHandler OnRemotesLoaded;

private Config selectedConfig = null;


private readonly IConfigRepository configRepo;
private readonly OpenBulletSettingsService openBulletSettingsService;

/// <summary>
/// The currently selected config.
/// </summary>
public Config SelectedConfig
{
get => selectedConfig;
set
{
selectedConfig = value;
OnConfigSelected?.Invoke(this, selectedConfig);
}
}
public ConfigService(IConfigRepository configRepo, OpenBulletSettingsService openBulletSettingsS
{
this.configRepo = configRepo;
this.openBulletSettingsService = openBulletSettingsService;
}

/// <summary>
/// Reloads all configs from the <see cref="IConfigRepository"/> and remote endpoints.
/// </summary>
public async Task ReloadConfigsAsync()
{
// Load from the main repository
Configs = (await configRepo.GetAllAsync()).ToList();
SelectedConfig = null;

// Load from remotes (fire and forget)


LoadFromRemotes();
}

private async void LoadFromRemotes()


{
List<Config> remoteConfigs = new();

var func = new Func<RemoteConfigsEndpoint, Task>(async endpoint =>


{
try
{
// Get the file
using HttpClient client = new();
client.DefaultRequestHeaders.Add("Api-Key", endpoint.ApiKey);
using var response = await client.GetAsync(endpoint.Url);

if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
throw new UnauthorizedAccessException();
}

if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
throw new FileNotFoundException();
}

var fileStream = await response.Content.ReadAsStreamAsync();

// Unpack the archive in memory


using ZipArchive archive = new(fileStream, ZipArchiveMode.Read);
foreach (var entry in archive.Entries)
{
if (!entry.Name.EndsWith(".opk"))
{
continue;
}

try
{
using var entryStream = entry.Open();
var config = await ConfigPacker.UnpackAsync(entryStream);

// Calculate the hash of the metadata of the remote config to use as id.
// This is done to have a consistent id through successive pulls of configs
// from remotes, so that jobs can reference the id and retrieve the correct one
config.Id = HexConverter.ToHexString(config.Metadata.GetUniqueHash());
config.IsRemote = true;

// If a config with the same hash is not already present (e.g. same exact config
// from another source) add it to the list
if (!remoteConfigs.Any(c => c.Id == config.Id))
{
remoteConfigs.Add(config);
}
}
catch
{

}
}

}
catch (Exception ex)
{
Console.WriteLine($"[{endpoint.Url}] Failed to pull configs from endpoint: {ex.Message}");
}
});

var tasks = openBulletSettingsService.Settings.RemoteSettings.ConfigsEndpoints


.Select(endpoint => func.Invoke(endpoint));

await Task.WhenAll(tasks).ConfigureAwait(false);

lock (Configs)
{
Configs.AddRange(remoteConfigs);
}

OnRemotesLoaded?.Invoke(this, EventArgs.Empty);
}
}
File: Services/DataPoolFactoryService.cs
■using OpenBullet2.Core.Models.Data;
using OpenBullet2.Core.Repositories;
using RuriLib.Models.Data;
using RuriLib.Models.Data.DataPools;
using RuriLib.Services;
using System;
using System.IO;
using System.Threading.Tasks;
using OpenBullet2.Core.Exceptions;

namespace OpenBullet2.Core.Services;

/// <summary>
/// Factory that creates a <see cref="DataPool"/> from <see cref="DataPoolOptions"/>.
/// </summary>
public class DataPoolFactoryService
{
private readonly IWordlistRepository _wordlistRepo;
private readonly RuriLibSettingsService _ruriLibSettings;

public DataPoolFactoryService(IWordlistRepository wordlistRepo, RuriLibSettingsService ruriLibSettings)


{
_wordlistRepo = wordlistRepo;
_ruriLibSettings = ruriLibSettings;
}

/// <summary>
/// Creates a <see cref="DataPool"/> from <see cref="DataPoolOptions"/>.
/// </summary>
public async Task<DataPool> FromOptionsAsync(DataPoolOptions options)
{
try
{
return options switch
{
InfiniteDataPoolOptions x => new InfiniteDataPool(x.WordlistType),
CombinationsDataPoolOptions x => new CombinationsDataPool(x.CharSet, x.Length, x.WordlistType),
RangeDataPoolOptions x => new RangeDataPool(x.Start, x.Amount, x.Step, x.Pad, x.WordlistType),
FileDataPoolOptions x => new FileDataPool(x.FileName, x.WordlistType),
WordlistDataPoolOptions x => await MakeWordlistDataPoolAsync(x),
_ => throw new NotImplementedException()
};
}
catch (Exception ex)
{
Console.WriteLine($"Exception while loading data pool. {ex.Message}");
return new InfiniteDataPool();
}
}

private async Task<DataPool> MakeWordlistDataPoolAsync(WordlistDataPoolOptions options)


{
var entity = await _wordlistRepo.GetAsync(options.WordlistId);
// If the entity was deleted
if (entity == null)
{
throw new EntityNotFoundException($"Wordlist entity not found: {options.WordlistId}");
}

if (!File.Exists(entity.FileName))
{
throw new EntityNotFoundException($"Wordlist file not found: {entity.FileName}");
}

var factory = new WordlistFactory(_ruriLibSettings);


return new WordlistDataPool(factory.FromEntity(entity));
}
}
File: Services/HitStorageService.cs
■using Microsoft.Extensions.DependencyInjection;
using OpenBullet2.Core.Entities;
using OpenBullet2.Core.Repositories;
using RuriLib.Models.Data.DataPools;
using RuriLib.Models.Hits;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Services;

/// <summary>
/// Stores hits to an <see cref="IHitRepository"/> in a thread-safe manner.
/// </summary>
public class HitStorageService : IDisposable
{
private readonly SemaphoreSlim _semaphore;
private readonly IServiceScopeFactory _scopeFactory;

public HitStorageService(IServiceScopeFactory scopeFactory)


{
_semaphore = new SemaphoreSlim(1, 1);
_scopeFactory = scopeFactory;
}

/// <summary>
/// Stores a hit in a thread-safe manner.
/// </summary>
public async Task StoreAsync(Hit hit)
{
using var scope = _scopeFactory.CreateScope();

// TODO: If this is too slow for a huge amount of hits, since


// we are basically creating a new context each time, we might
// explore using raw queries to insert data which would be way faster.
var hitRepo = scope.ServiceProvider.GetRequiredService<IHitRepository>();

var entity = new HitEntity


{
CapturedData = hit.CapturedDataString,
Data = hit.DataString,
Date = hit.Date,
Proxy = hit.ProxyString,
Type = hit.Type,
ConfigId = hit.Config.Id,
ConfigName = hit.Config.Metadata.Name,
ConfigCategory = hit.Config.Metadata.Category,
OwnerId = hit.OwnerId
};

switch (hit.DataPool)
{
case WordlistDataPool wordlistDataPool:
entity.WordlistId = wordlistDataPool.Wordlist.Id;
entity.WordlistName = wordlistDataPool.Wordlist.Name;
break;

// The following are not actual wordlists but it can help identify which pool was used
case FileDataPool fileDataPool:
entity.WordlistId = fileDataPool.POOL_CODE;
entity.WordlistName = fileDataPool.FileName;
break;

case RangeDataPool rangeDataPool:


entity.WordlistId = rangeDataPool.POOL_CODE;
entity.WordlistName = $"{rangeDataPool.Start}|{rangeDataPool.Amount}|{rangeDataPool.Pa
break;

case CombinationsDataPool combationsDataPool:


entity.WordlistId = combationsDataPool.POOL_CODE;
entity.WordlistName = $"{combationsDataPool.Length}|{combationsDataPool.CharSet}";
break;

case InfiniteDataPool infiniteDataPool:


entity.WordlistId = infiniteDataPool.POOL_CODE;
break;
}

// Only allow saving one hit at a time (multiple threads should


// not use the same DbContext at the same time).
await _semaphore.WaitAsync();

try
{
await hitRepo.AddAsync(entity);
}
finally
{
_semaphore.Release();
}
}

public void Dispose()


{
_semaphore?.Dispose();
GC.SuppressFinalize(this);
}
}
File: Services/IntoliRandomUAProvider.cs
■using Newtonsoft.Json.Linq;
using RuriLib.Providers.UserAgents;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using OpenBullet2.Core.Exceptions;

namespace OpenBullet2.Core.Services;

/// <summary>
/// Random UA provider that uses the User-Agents collected by intoli.com
/// </summary>
public class IntoliRandomUAProvider : IRandomUAProvider
{
private readonly Dictionary<UAPlatform, UserAgent[]> distributions = new Dictionary<UAPlatform, UserAgent[]>();
private readonly Random rand;

/// <inheritdoc/>
public int Total => distributions[UAPlatform.All].Length;

public IntoliRandomUAProvider(string jsonFile)


{
var json = File.ReadAllText(jsonFile);
var array = JArray.Parse(json);

var agents = new List<UserAgent>();


foreach (var elem in array)
{
agents.Add(new UserAgent(elem.Value<string>("userAgent"),
ConvertPlatform(elem.Value<string>("platform")), elem.Value<double>("weight"), 0));
}

rand = new Random();

if (agents.Count == 0)
{
throw new MissingUserAgentsException("No valid user agents found in user-agents.json");
}

foreach (var platform in (UAPlatform[])Enum.GetValues(typeof(UAPlatform)))


{
distributions[platform] = ComputeDistribution(agents, platform);
}
}

/// <inheritdoc/>
public string Generate() => Generate(UAPlatform.All);

/// <inheritdoc/>
public string Generate(UAPlatform platform)
{
// Take the correct precomputed cumulative distribution
var distribution = distributions[platform];

// Take the maximum value of the cumulative function


var max = distribution.Last().cumulative;

// Generate a random double up to the previously computed maximum


var random = rand.NextDouble() * max;

// Return the first user agent with cumulative greater or equal to the random one
return distribution.First(u => u.cumulative >= random).userAgentString;
}

private static UserAgent[] ComputeDistribution(IEnumerable<UserAgent> agents, UAPlatform platfo


{
var valid = agents.Where(a => BelongsToPlatform(a.platform, platform));

var distribution = new List<UserAgent>();


double cumulative = 0;
foreach (var elem in valid)
{
cumulative += elem.weight;
distribution.Add(new UserAgent(elem.userAgentString, elem.platform, elem.weight, cumulative
}

return distribution.ToArray();
}

private static UAPlatform ConvertPlatform(string platform) => platform switch


{
"iPad" => UAPlatform.iPad,
"iPhone" => UAPlatform.iPhone,
"Linux aarch64" => UAPlatform.Android,
"Linux armv71" => UAPlatform.Android,
"Linux armv81" => UAPlatform.Android,
"Linux x86_64" => UAPlatform.Linux,
"MacIntel" => UAPlatform.Mac,
"Win32" => UAPlatform.Windows,
"Win64" => UAPlatform.Windows,
"Windows" => UAPlatform.Windows,
_ => UAPlatform.Windows
};

private static bool BelongsToPlatform(UAPlatform current, UAPlatform required) => required switch
{
UAPlatform.All => true,
UAPlatform.Desktop => current == UAPlatform.Linux || current == UAPlatform.Mac || current == U
UAPlatform.Mobile => current == UAPlatform.iPhone || current == UAPlatform.iPad || current == U
_ => current == required
};
}
File: Services/JobFactoryService.cs
■using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OpenBullet2.Core.Models.Hits;
using OpenBullet2.Core.Models.Jobs;
using OpenBullet2.Core.Models.Proxies;
using RuriLib.Logging;
using RuriLib.Models.Bots;
using RuriLib.Models.Jobs;
using RuriLib.Models.Proxies;
using RuriLib.Providers.RandomNumbers;
using RuriLib.Providers.UserAgents;
using RuriLib.Services;
using System;
using System.Linq;

namespace OpenBullet2.Core.Services;

/// <summary>
/// Factory that creates a <see cref="Job"/> from <see cref="JobOptions"/>.
/// </summary>
public class JobFactoryService
{
private readonly ConfigService _configService;
private readonly RuriLibSettingsService _settingsService;
private readonly HitStorageService _hitStorage;
private readonly IServiceScopeFactory _scopeFactory;
private readonly ProxyCheckOutputFactory _proxyCheckOutputFactory;
private readonly ProxyReloadService _proxyReloadService;
private readonly IRandomUAProvider _randomUaProvider;
private readonly IRNGProvider _rngProvider;
private readonly IJobLogger _logger;
private readonly PluginRepository _pluginRepo;

/// <summary>
/// The maximum amount of bots that a job can use.
/// </summary>
public int BotLimit { get; init; } = 200;

public JobFactoryService(ConfigService configService, RuriLibSettingsService settingsService, PluginRepository pluginR


HitStorageService hitStorage, IServiceScopeFactory scopeFactory, ProxyCheckOutputFactory proxyCheckOutputFac
ProxyReloadService proxyReloadService, IRandomUAProvider randomUaProvider, IRNGProvider rngProvider, IJobL
IConfiguration config)
{
_configService = configService;
_settingsService = settingsService;
_pluginRepo = pluginRepo;
_hitStorage = hitStorage;
_scopeFactory = scopeFactory;
_proxyCheckOutputFactory = proxyCheckOutputFactory;
_proxyReloadService = proxyReloadService;
_randomUaProvider = randomUaProvider;
_rngProvider = rngProvider;
_logger = logger;
var botLimit = config.GetSection("Resources")["BotLimit"];

if (botLimit is not null)


{
BotLimit = int.Parse(botLimit);
}
}

/// <summary>
/// Creates a <see cref="Job"/> with the provided <paramref name="id"/> and <paramref name="own
/// from <see cref="JobOptions"/>.
/// </summary>
/// <param name="id">The ID of the newly created job, must be unique</param>
/// <param name="ownerId">The ID of the user who owns the job. 0 for admin</param>
/// <param name="options">The options to create the job from</param>
public Job FromOptions(int id, int ownerId, JobOptions options)
{
Job job = options switch
{
MultiRunJobOptions x => MakeMultiRunJob(x),
ProxyCheckJobOptions x => MakeProxyCheckJob(x),
_ => throw new NotImplementedException()
};

job.Id = id;
job.OwnerId = ownerId;
return job;
}

private MultiRunJob MakeMultiRunJob(MultiRunJobOptions options)


{
using var scope = _scopeFactory.CreateScope();
var proxySourceFactory = scope.ServiceProvider.GetRequiredService<ProxySourceFactoryServi
var dataPoolFactory = scope.ServiceProvider.GetRequiredService<DataPoolFactoryService>();

var hitOutputsFactory = new HitOutputFactory(_hitStorage);

var job = new MultiRunJob(_settingsService, _pluginRepo, _logger)


{
Config = _configService.Configs.FirstOrDefault(c => c.Id == options.ConfigId),
CreationTime = DateTime.Now,
ProxyMode = options.ProxyMode,
ShuffleProxies = options.ShuffleProxies,
NoValidProxyBehaviour = options.NoValidProxyBehaviour,
NeverBanProxies = options.NeverBanProxies,
MarkAsToCheckOnAbort = options.MarkAsToCheckOnAbort,
ProxyBanTime = TimeSpan.FromSeconds(options.ProxyBanTimeSeconds),
ConcurrentProxyMode = options.ConcurrentProxyMode,
PeriodicReloadInterval = TimeSpan.FromSeconds(options.PeriodicReloadIntervalSeconds),
StartCondition = options.StartCondition,
Name = options.Name,
Bots = options.Bots,
BotLimit = BotLimit,
CurrentBotDatas = new BotData[BotLimit],
Skip = options.Skip,
HitOutputs = options.HitOutputs.Select(o => hitOutputsFactory.FromOptions(o)).ToList(),
ProxySources = options.ProxySources.Select(s => proxySourceFactory.FromOptions(s).Resul
Providers = new(_settingsService)
{
RandomUA = _settingsService.RuriLibSettings.GeneralSettings.UseCustomUserAgentsList
? new DefaultRandomUAProvider(_settingsService)
: _randomUaProvider,
RNG = _rngProvider
},
DataPool = dataPoolFactory.FromOptionsAsync(options.DataPool).Result
};

return job;
}

private ProxyCheckJob MakeProxyCheckJob(ProxyCheckJobOptions options)


{
var job = new ProxyCheckJob(_settingsService, _pluginRepo, _logger)
{
StartCondition = options.StartCondition,
Bots = options.Bots,
Name = options.Name,
BotLimit = BotLimit,
CheckOnlyUntested = options.CheckOnlyUntested,
Url = options.Target.Url,
SuccessKey = options.Target.SuccessKey,
Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds),
GeoProvider = new DBIPProxyGeolocationProvider("dbip-country-lite.mmdb")
};

job.Proxies = _proxyReloadService.ReloadAsync(options.GroupId, job.OwnerId).Result;

// Update the stats


var proxies = options.CheckOnlyUntested
? job.Proxies.Where(p => p.WorkingStatus == ProxyWorkingStatus.Untested)
: job.Proxies;

var proxiesList = proxies.ToList();


job.Total = proxiesList.Count;
job.Tested = proxiesList.Count(p => p.WorkingStatus != ProxyWorkingStatus.Untested);
job.Working = proxiesList.Count(p => p.WorkingStatus == ProxyWorkingStatus.Working);
job.NotWorking = proxiesList.Count(p => p.WorkingStatus == ProxyWorkingStatus.NotWorking);
job.ProxyOutput = _proxyCheckOutputFactory.FromOptions(new DatabaseProxyCheckOutputOp

return job;
}
}
File: Services/JobManagerService.cs
■using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using OpenBullet2.Core.Entities;
using OpenBullet2.Core.Models.Data;
using OpenBullet2.Core.Models.Jobs;
using OpenBullet2.Core.Repositories;
using RuriLib.Models.Data.DataPools;
using RuriLib.Models.Jobs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Services;

/// <summary>
/// Manages multiple jobs.
/// </summary>
public class JobManagerService : IDisposable
{
/// <summary>
/// The list of all created jobs.
/// </summary>
public IEnumerable<Job> Jobs => _jobs;
private readonly List<Job> _jobs = new();

private readonly SemaphoreSlim _jobSemaphore = new(1, 1);


private readonly SemaphoreSlim _recordSemaphore = new(1, 1);
private readonly IServiceScopeFactory _scopeFactory;

public JobManagerService(IServiceScopeFactory scopeFactory, JobFactoryService jobFactory)


{
using var scope = scopeFactory.CreateScope();
var jobRepo = scope.ServiceProvider.GetRequiredService<IJobRepository>();

// Restore jobs from the database


var entities = jobRepo.GetAll().Include(j => j.Owner).ToList();
var jsonSettings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto };

foreach (var entity in entities)


{
// Convert old namespaces to support old databases
if (entity.JobOptions.Contains("OpenBullet2.Models") || entity.JobOptions.Contains(", OpenBullet2\""))
{
entity.JobOptions = entity.JobOptions
.Replace("OpenBullet2.Models", "OpenBullet2.Core.Models")
.Replace(", OpenBullet2\"", ", OpenBullet2.Core\"");

jobRepo.UpdateAsync(entity).Wait();
}
var options = JsonConvert.DeserializeObject<JobOptionsWrapper>(entity.JobOptions, jsonSet
var job = jobFactory.FromOptions(entity.Id, entity.Owner == null ? 0 : entity.Owner.Id, options);
AddJob(job);
}

_scopeFactory = scopeFactory;
}

public void AddJob(Job job)


{
_jobs.Add(job);

if (job is MultiRunJob mrj)


{
mrj.OnCompleted += SaveRecord;
mrj.OnTimerTick += SaveRecord;
mrj.OnCompleted += SaveMultiRunJobOptionsAsync;
mrj.OnTimerTick += SaveMultiRunJobOptionsAsync;
mrj.OnBotsChanged += SaveMultiRunJobOptionsAsync;
}
}

public void RemoveJob(Job job)


{
_jobs.Remove(job);

if (job is MultiRunJob mrj)


{
try
{
mrj.OnCompleted -= SaveRecord;
mrj.OnTimerTick -= SaveRecord;
mrj.OnCompleted -= SaveMultiRunJobOptionsAsync;
mrj.OnTimerTick -= SaveMultiRunJobOptionsAsync;
mrj.OnBotsChanged -= SaveMultiRunJobOptionsAsync;
}
catch
{

}
}
}

public void Clear()


{
UnbindAllEvents();
_jobs.Clear();
}

// Saves the record for a MultiRunJob in the IRecordRepository. Thread safe.


private async void SaveRecord(object sender, EventArgs e)
{
using var scope = _scopeFactory.CreateScope();
var recordRepo = scope.ServiceProvider.GetRequiredService<IRecordRepository>();
if (sender is not MultiRunJob job || job.DataPool is not WordlistDataPool pool)
{
return;
}

await _recordSemaphore.WaitAsync();

try
{
var record = await recordRepo.GetAll()
.FirstOrDefaultAsync(r => r.ConfigId == job.Config.Id && r.WordlistId == pool.Wordlist.Id);

var checkpoint = job.Status == JobStatus.Idle


? job.Skip
: job.Skip + job.DataTested;

if (record == null)
{
await recordRepo.AddAsync(new RecordEntity
{
ConfigId = job.Config.Id,
WordlistId = pool.Wordlist.Id,
Checkpoint = checkpoint
});
}
else
{
record.Checkpoint = checkpoint;
await recordRepo.UpdateAsync(record);
}
}
catch
{

}
finally
{
_recordSemaphore.Release();
}
}

private async void SaveMultiRunJobOptionsAsync(object sender, EventArgs e)


{
if (sender is not MultiRunJob job)
{
return;
}

await SaveMultiRunJobOptionsAsync(job);
}

// Saves the options for a MultiRunJob in the IJobRepository. Thread safe.


public async Task SaveMultiRunJobOptionsAsync(MultiRunJob job)
{
using var scope = _scopeFactory.CreateScope();
var jobRepo = scope.ServiceProvider.GetRequiredService<IJobRepository>();

await _jobSemaphore.WaitAsync();

try
{
var entity = await jobRepo.GetAsync(job.Id);

if (entity == null || entity.JobOptions == null)


{
Console.WriteLine("Skipped job options save because Job (or JobOptions) was null");
return;
}

// Deserialize and unwrap the job options


var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto };
var wrapper = JsonConvert.DeserializeObject<JobOptionsWrapper>(entity.JobOptions, setting
var options = (MultiRunJobOptions)wrapper.Options;

// Check if it's valid


if (string.IsNullOrEmpty(options.ConfigId))
{
Console.WriteLine("Skipped job options save because ConfigId was null");
return;
}

if (options.DataPool is WordlistDataPoolOptions x && x.WordlistId == -1)


{
Console.WriteLine("Skipped job options save because WordlistId was -1");
return;
}

// Update the skip (if not idle, also add the currently tested ones) and the bots
options.Skip = job.Status == JobStatus.Idle
? job.Skip
: job.Skip + job.DataTested;

options.Bots = job.Bots;

// Wrap and serialize again


var newWrapper = new JobOptionsWrapper { Options = options };
entity.JobOptions = JsonConvert.SerializeObject(newWrapper, settings);

// Update the job


await jobRepo.UpdateAsync(entity);
}
catch
{

}
finally
{
_jobSemaphore.Release();
}
}
private void UnbindAllEvents()
{
foreach (var job in _jobs)
{
if (job is MultiRunJob mrj)
{
try
{
mrj.OnCompleted -= SaveRecord;
mrj.OnTimerTick -= SaveRecord;
mrj.OnCompleted -= SaveMultiRunJobOptionsAsync;
mrj.OnTimerTick -= SaveMultiRunJobOptionsAsync;
mrj.OnBotsChanged -= SaveMultiRunJobOptionsAsync;
}
catch
{

}
}
}
}

public void Dispose() => UnbindAllEvents();


}
File: Services/JobMonitorService.cs
■using Newtonsoft.Json;
using RuriLib.Functions.Crypto;
using RuriLib.Models.Jobs.Monitor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;

namespace OpenBullet2.Core.Services;

/// <summary>
/// Monitors jobs, checks defined triggers every second and executes the corresponding actions.
/// </summary>
public class JobMonitorService : IDisposable
{
/// <summary>
/// The list of triggered actions that can be executed by the job monitor.
/// </summary>
public List<TriggeredAction> TriggeredActions { get; set; } = new List<TriggeredAction>();

private readonly Timer timer;


private readonly Timer saveTimer;
private readonly JobManagerService jobManager;
private readonly string fileName;
private readonly JsonSerializerSettings jsonSettings = new JsonSerializerSettings
{
TypeNameHandling = TypeNameHandling.Auto,
Formatting = Formatting.Indented
};
private byte[] lastSavedHash = Array.Empty<byte>();

public JobMonitorService(JobManagerService jobManager,


string fileName = "UserData/triggeredActions.json", bool autoSave = true)
{
this.jobManager = jobManager;
this.fileName = fileName;
RestoreTriggeredActions();

timer = new Timer(new TimerCallback(_ => CheckAndExecute()), null, 1000, 1000);

if (autoSave)
{
saveTimer = new Timer(new TimerCallback(_ => SaveStateIfChanged()), null, 5000, 5000);
}
}

private void CheckAndExecute()


{
for (var i = 0; i < TriggeredActions.Count; i++)
{
var action = TriggeredActions[i];
if (action.IsActive && !action.IsExecuting && (action.IsRepeatable || action.Executions == 0))
{
action.CheckAndExecute(jobManager.Jobs).ConfigureAwait(false);
}
}
}

private void RestoreTriggeredActions()


{
if (!File.Exists(fileName))
{
return;
}

try
{
var json = File.ReadAllText(fileName);
TriggeredActions = JsonConvert.DeserializeObject<TriggeredAction[]>(json, jsonSettings).ToL
}
catch
{
Console.WriteLine("Failed to deserialize triggered actions from json, recreating them");
}
}

public void SaveStateIfChanged()


{
var json = JsonConvert.SerializeObject(TriggeredActions.ToArray(), jsonSettings);
var hash = Crypto.MD5(Encoding.UTF8.GetBytes(json));

if (hash != lastSavedHash)
{
try
{
File.WriteAllText(fileName, json);
lastSavedHash = hash;
}
catch
{
// File probably in use
}
}
}

public void Dispose()


{
timer?.Dispose();
saveTimer?.Dispose();
GC.SuppressFinalize(this);
}
}
File: Services/OpenBulletSettingsService.cs
■using Newtonsoft.Json;
using OpenBullet2.Core.Models.Settings;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Services;

/// <summary>
/// Provides interaction with settings of the OpenBullet 2 application.
/// </summary>
public class OpenBulletSettingsService
{
private string BaseFolder { get; }
private readonly JsonSerializerSettings jsonSettings;

/// <summary>
/// The path of the file where settings are saved.
/// </summary>
public string FileName => Path.Combine(BaseFolder, "OpenBulletSettings.json");

/// <summary>
/// The actual settings. After modifying them, call the <see cref="SaveAsync"/> method to persist them.
/// </summary>
public OpenBulletSettings Settings { get; private set; }

public OpenBulletSettingsService(string baseFolder)


{
BaseFolder = baseFolder;
Directory.CreateDirectory(baseFolder);

jsonSettings = new JsonSerializerSettings


{
Formatting = Formatting.Indented,
TypeNameHandling = TypeNameHandling.Auto
};

if (File.Exists(FileName))
{
Settings = JsonConvert.DeserializeObject<OpenBulletSettings>(File.ReadAllText(FileName), jsonSettings);
}
else
{
Recreate();
SaveAsync().Wait();
}
}

/// <summary>
/// Saves the <see cref="Settings"/> to disk.
/// </summary>
public async Task SaveAsync() => await File.WriteAllTextAsync(FileName, JsonConvert.SerializeObject(Settings, jsonS
/// <summary>
/// Restores the default <see cref="Settings"/> (does not save to disk).
/// </summary>
public void Recreate() => Settings = new OpenBulletSettings
{
GeneralSettings = new GeneralSettings { ProxyCheckTargets = new List<ProxyCheckTarget> { n
RemoteSettings = new RemoteSettings(),
SecuritySettings = new SecuritySettings().GenerateJwtKey().SetupAdminPassword("admin"),
CustomizationSettings = new CustomizationSettings()
};
}
File: Services/ProxyReloadService.cs
■using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using OpenBullet2.Core.Entities;
using OpenBullet2.Core.Models.Proxies;
using OpenBullet2.Core.Repositories;
using RuriLib.Models.Proxies;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Services;

/// <summary>
/// A reload service that will reload proxies from an <see cref="IProxyGroupRepository"/>.
/// </summary>
public class ProxyReloadService : IDisposable
{
private readonly SemaphoreSlim _semaphore;
private readonly IServiceScopeFactory _scopeFactory;

public ProxyReloadService(IServiceScopeFactory scopeFactory)


{
_semaphore = new SemaphoreSlim(1, 1);
_scopeFactory = scopeFactory;
}

/// <summary>
/// Reloads proxies from a group with a given <paramref name="groupId"/> of a user with a given
/// <paramref name="userId"/>.
/// </summary>
public async Task<IEnumerable<Proxy>> ReloadAsync(int groupId, int userId, CancellationToken cancellationToken = d
{
using var scope = _scopeFactory.CreateScope();
var proxyGroupsRepo = scope.ServiceProvider.GetRequiredService<IProxyGroupRepository>();
var proxyRepo = scope.ServiceProvider.GetRequiredService<IProxyRepository>();

List<ProxyEntity> entities;

// Only allow reloading one group at a time (multiple threads should


// not use the same DbContext at the same time).
await _semaphore.WaitAsync(cancellationToken);

try
{
// If the groupId is -1 reload all proxies
if (groupId == -1)
{
entities = userId == 0
? await proxyRepo.GetAll().ToListAsync(cancellationToken).ConfigureAwait(false)
: await proxyRepo.GetAll().Include(p => p.Group).ThenInclude(g => g.Owner)
.Where(p => p.Group.Owner.Id == userId).ToListAsync(cancellationToken).ConfigureAwait(false);
}
else
{
var group = await proxyGroupsRepo.GetAsync(groupId, cancellationToken).ConfigureAwait(
entities = await proxyRepo.GetAll()
.Where(p => p.Group.Id == groupId)
.ToListAsync(cancellationToken).ConfigureAwait(false);
}
}
finally
{
_semaphore.Release();
}

var proxyFactory = new ProxyFactory();


return entities.Select(e => ProxyFactory.FromEntity(e));
}

public void Dispose()


{
_semaphore?.Dispose();
GC.SuppressFinalize(this);
}
}
File: Services/ProxySourceFactoryService.cs
■using OpenBullet2.Core.Models.Proxies;
using OpenBullet2.Core.Models.Proxies.Sources;
using RuriLib.Models.Proxies;
using RuriLib.Models.Proxies.ProxySources;
using System;
using System.Threading.Tasks;

namespace OpenBullet2.Core.Services;

/// <summary>
/// Factory that creates a <see cref="ProxySource"/> from a <see cref="ProxySourceOptions"/> object.
/// </summary>
public class ProxySourceFactoryService
{
private readonly ProxyReloadService _reloadService;

public ProxySourceFactoryService(ProxyReloadService reloadService)


{
_reloadService = reloadService;
}

/// <summary>
/// Creates a <see cref="ProxySource"/> from a <see cref="ProxySourceOptions"/> object.
/// </summary>
public Task<ProxySource> FromOptions(ProxySourceOptions options)
{
ProxySource source = options switch
{
RemoteProxySourceOptions x => new RemoteProxySource(x.Url) { DefaultType = x.DefaultType },
FileProxySourceOptions x => new FileProxySource(x.FileName) { DefaultType = x.DefaultType },
GroupProxySourceOptions x => new GroupProxySource(x.GroupId, _reloadService),
_ => throw new NotImplementedException()
};

return Task.FromResult(source);
}
}

You might also like