Add migrations, add private streams

This commit is contained in:
Laura Hausmann 2022-02-04 04:48:04 +01:00
parent cade2df6a5
commit 59b3dc6f68
Signed by: zotan
GPG Key ID: D044E84C5BE01605
12 changed files with 333 additions and 184 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ obj/
/packages/
/data/
app.db
app.db.bak

View File

@ -2,26 +2,26 @@
using Microsoft.AspNetCore.Mvc;
using RTMPDash.DataModels;
namespace RTMPDash.Controllers {
[ApiController, Route("/api/authenticate")]
public class RtmpAuthController : ControllerBase {
[HttpGet]
public string Get() {
var db = new AppDb.DbConn();
if (!db.Users.Any(p => p.StreamKey == Request.Query["name"])) {
Response.StatusCode = 403;
return "unauthorized";
}
namespace RTMPDash.Controllers;
var user = db.Users.FirstOrDefault(p => p.StreamKey == Request.Query["name"]);
Response.Headers.Add("x-rtmp-user", user!.Username);
if (user.AllowRestream && !string.IsNullOrWhiteSpace(user.RestreamTargets))
Response.Headers.Add("x-rtmp-target", user.RestreamTargets);
Response.StatusCode = 302;
return "authorized as " + user!.Username;
[ApiController, Route("/api/authenticate")]
public class RtmpAuthController : ControllerBase {
[HttpGet]
public string Get() {
var db = new AppDb.DbConn();
if (!db.Users.Any(p => p.StreamKey == Request.Query["name"])) {
Response.StatusCode = 403;
return "unauthorized";
}
var user = db.Users.FirstOrDefault(p => p.StreamKey == Request.Query["name"]);
Response.Headers.Add("x-rtmp-user", user!.IsPrivate ? user!.PrivateAccessKey : user!.Username);
if (user.AllowRestream && !string.IsNullOrWhiteSpace(user.RestreamTargets))
Response.Headers.Add("x-rtmp-target", user.RestreamTargets);
Response.StatusCode = 302;
return "authorized as " + user!.Username;
}
}

View File

@ -5,35 +5,32 @@ using LinqToDB.Configuration;
using LinqToDB.Data;
using RTMPDash.DataModels.Tables;
namespace RTMPDash.DataModels {
public class AppDb {
public class ConnectionStringSettings : IConnectionStringSettings {
public string ConnectionString { get; set; }
public string Name { get; set; }
public string ProviderName { get; set; }
public bool IsGlobal => false;
}
namespace RTMPDash.DataModels;
public class Settings : ILinqToDBSettings {
public IEnumerable<IDataProviderSettings> DataProviders => Enumerable.Empty<IDataProviderSettings>();
public class AppDb {
public class ConnectionStringSettings : IConnectionStringSettings {
public string ConnectionString { get; set; }
public string Name { get; set; }
public string ProviderName { get; set; }
public bool IsGlobal => false;
}
public string DefaultConfiguration => "SQLite";
public string DefaultDataProvider => "SQLite";
public class Settings : ILinqToDBSettings {
public IEnumerable<IDataProviderSettings> DataProviders => Enumerable.Empty<IDataProviderSettings>();
public IEnumerable<IConnectionStringSettings> ConnectionStrings {
get {
yield return new ConnectionStringSettings {
Name = "db", ProviderName = "SQLite", ConnectionString = @"Data Source=app.db;"
};
}
}
}
public string DefaultConfiguration => "SQLite";
public string DefaultDataProvider => "SQLite";
public class DbConn : DataConnection {
public DbConn() : base("db") { }
public ITable<User> Users => GetTable<User>();
public ITable<Invite> Invites => GetTable<Invite>();
public IEnumerable<IConnectionStringSettings> ConnectionStrings {
get { yield return new ConnectionStringSettings { Name = "db", ProviderName = "SQLite", ConnectionString = @"Data Source=app.db;" }; }
}
}
public class DbConn : DataConnection {
public DbConn() : base("db") { }
public ITable<User> Users => GetTable<User>();
public ITable<Invite> Invites => GetTable<Invite>();
public ITable<DbInfo> DbInfo => GetTable<DbInfo>();
}
}

View File

@ -0,0 +1,9 @@
using LinqToDB.Mapping;
namespace RTMPDash.DataModels.Tables;
[Table(Name = "DbInfo")]
public class DbInfo {
[Column(Name = "ID"), PrimaryKey, Identity, NotNull] public int Id { get; set; }
[Column(Name = "DbVer"), NotNull] public int DbVer { get; set; }
}

View File

@ -1,18 +1,20 @@
using LinqToDB.Mapping;
namespace RTMPDash.DataModels.Tables {
[Table(Name = "Users")]
public class User {
[Column(Name = "Username"), PrimaryKey] public string Username { get; set; }
[Column(Name = "Password")] public string Password { get; set; }
[Column(Name = "IsAdmin")] public bool IsAdmin { get; set; }
[Column(Name = "AllowRestream")] public bool AllowRestream { get; set; }
[Column(Name = "StreamKey")] public string StreamKey { get; set; }
[Column(Name = "PronounSubject")] public string PronounSubject { get; set; }
[Column(Name = "PronounPossessive")] public string PronounPossessive { get; set; }
[Column(Name = "ChatUrl")] public string ChatUrl { get; set; }
[Column(Name = "AnnouncementUrl")] public string AnnouncementUrl { get; set; }
[Column(Name = "RestreamTargets")] public string RestreamTargets { get; set; }
[Column(Name = "RestreamUrls")] public string RestreamUrls { get; set; }
}
namespace RTMPDash.DataModels.Tables;
[Table(Name = "Users")]
public class User {
[Column(Name = "Username"), PrimaryKey] public string Username { get; set; }
[Column(Name = "Password")] public string Password { get; set; }
[Column(Name = "IsAdmin")] public bool IsAdmin { get; set; }
[Column(Name = "AllowRestream")] public bool AllowRestream { get; set; }
[Column(Name = "StreamKey")] public string StreamKey { get; set; }
[Column(Name = "PronounSubject")] public string PronounSubject { get; set; }
[Column(Name = "PronounPossessive")] public string PronounPossessive { get; set; }
[Column(Name = "ChatUrl")] public string ChatUrl { get; set; }
[Column(Name = "AnnouncementUrl")] public string AnnouncementUrl { get; set; }
[Column(Name = "RestreamTargets")] public string RestreamTargets { get; set; }
[Column(Name = "RestreamUrls")] public string RestreamUrls { get; set; }
[Column(Name = "IsPrivate")] public bool IsPrivate { get; set; }
[Column(Name = "PrivateAccessKey")] public string PrivateAccessKey { get; set; }
}

75
Migrations.cs Normal file
View File

@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Linq;
using LinqToDB;
using LinqToDB.Data;
using RTMPDash.DataModels;
using RTMPDash.DataModels.Tables;
namespace RTMPDash;
/**
* TODO: Initial migrations should create all tables & an admin user and print a randomly generated password for the admin user
* *
*/
public static class Migrations {
public const int DbVer = 1;
private static readonly List<Migration> _migrations = new() {
new Migration(1, "ALTER TABLE Users ADD IsPrivate INTEGER DEFAULT 0 NOT NULL"), new Migration(1, "ALTER TABLE Users ADD PrivateAccessKey TEXT")
};
public static void RunMigrations() {
using var db = new AppDb.DbConn();
if (db.DataProvider.GetSchemaProvider().GetSchema(db).Tables.All(t => t.TableName != "DbInfo")) {
db.CreateTable<DbInfo>();
db.InsertWithIdentity(new DbInfo { DbVer = 0 });
}
var dbinfo = db.DbInfo.First();
var ccolor = Console.ForegroundColor;
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine($"Database version: {db.DbInfo.ToList().First().DbVer}");
var migrationsToRun = _migrations.FindAll(p => p.IntroducedWithDbVer > db.DbInfo.First().DbVer);
if (migrationsToRun.Count == 0) {
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("No migrations to run.");
}
else {
migrationsToRun.ForEach(RunMigration);
dbinfo.DbVer = DbVer;
db.Update(dbinfo);
Console.ForegroundColor = ConsoleColor.DarkGreen;
Console.WriteLine($"Database version is now: {DbVer}");
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("Finished running migrations.");
}
Console.ForegroundColor = ccolor;
}
private static void RunMigration(Migration mig) => mig.Run();
private class Migration {
private readonly string _sql;
public readonly int IntroducedWithDbVer;
public Migration(int introducedWithDbVer, string sql) {
IntroducedWithDbVer = introducedWithDbVer;
_sql = sql;
}
public void Run() {
using var db = new AppDb.DbConn();
Console.ForegroundColor = ConsoleColor.DarkCyan;
Console.Write("Running migration: ");
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine(_sql);
db.Execute(_sql);
}
}
}

View File

@ -24,11 +24,29 @@ else {
<p>Thanks for using @Program.SiteName. If you have any issues, please contact me on @Html.Raw(Program.ContactInfo)</p>
<hr/>
<p>Please subscribe to the <a href="@Program.ServiceAnnouncementUrl" target="_blank">Service Announcements Channel</a> to get informed about maintenance and other important things.</p>
<hr>
<p class="mb-0">When using OBS, the stream key and leading slash before it is to be left out in the RTMP url.</p>
<hr/>
<p class="mb-0">For low-latancy streams, please set your keyframe interval to 1-2 seconds. Otherwise, automatic or something in the range of 4-8 seconds is fine.</p>
</div>
if (StreamUtils.ListLiveUsers().Contains(user.Username) && user.IsPrivate) {
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Warning!</h4>
<p class="mb-0">
You set your stream to private, but have not restarted your stream since.
<br/>
While this setting is applied immediately, the old player URL will remain accessible until you stop and restart your stream.
</p>
</div>
}
else if (StreamUtils.ListLiveUsers().Contains(user.PrivateAccessKey) && !user.IsPrivate) {
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Warning!</h4>
<p class="mb-0">
You set your stream to public, but have not restarted your stream since.
<br/>
While this setting is applied immediately, the public player will not work until you restart your stream.
</p>
</div>
}
<div class="input-group mb-3">
<div class="input-group-prepend">
<span class="input-group-text" style="width:23ch">Profile URL</span>
@ -42,10 +60,21 @@ else {
<div class="input-group-prepend">
<span class="input-group-text" style="width:23ch">Player URL</span>
</div>
<input type="text" class="form-control" id="input-playerurl" value="@Program.PlayerDomain/@user.Username" disabled>
<div class="input-group-append">
<button class="btn btn-outline-secondary" role="button" id="button-copy-playerurl" onclick="copyToClipboard(document.getElementById('input-playerurl').value);">Copy</button>
</div>
@if (user.IsPrivate) {
<input type="text" class="form-control" id="input-playerurl" value="@Program.PlayerDomain/@user.PrivateAccessKey" disabled>
<div class="input-group-append">
<button onclick="ajax_and_reload('private_toggle')" class="btn btn-outline-info" role="button" id="button-toggle-private">Private</button>
<button class="btn btn-outline-secondary" role="button" id="button-copy-playerurl" onclick="copyToClipboard(document.getElementById('input-playerurl').value);">Copy</button>
</div>
}
else {
<input type="text" class="form-control" id="input-playerurl" value="@Program.PlayerDomain/@user.Username" disabled>
<div class="input-group-append">
<button onclick="ajax_and_reload('private_toggle')" class="btn btn-outline-success" role="button" id="button-toggle-private">Public</button>
<button class="btn btn-outline-secondary" role="button" id="button-copy-playerurl" onclick="copyToClipboard(document.getElementById('input-playerurl').value);">Copy</button>
</div>
}
</div>
<div class="input-group mb-3">
<div class="input-group-prepend">
@ -61,9 +90,10 @@ else {
<div class="input-group-prepend">
<span class="input-group-text" style="width:23ch">Stream URL</span>
</div>
<input type="text" class="form-control" id="input-streamurl" value="@Program.IngressDomain/ingress/@user.StreamKey" disabled>
<input type="text" class="form-control" id="input-streamurl" value="@Program.IngressDomain/ingress" disabled>
<div class="input-group-append">
@if (StreamUtils.IsLive(user.Username, stats)) {
@* ReSharper disable once ConvertIfStatementToSwitchStatement *@
@if (!user.IsPrivate && StreamUtils.IsLive(user.Username, stats)) {
var uptime = TimeSpan.FromMilliseconds(StreamUtils.GetClientTime(user.Username, stats)).StripMilliseconds();
if (user.AllowRestream && !string.IsNullOrWhiteSpace(user.RestreamTargets)) {
if (StreamUtils.GetClientTime(user.Username, stats) > 5000) {
@ -84,6 +114,27 @@ else {
}
<button class="btn btn-dark" role="button" disabled>@uptime.ToString("c")</button>
}
else if (user.IsPrivate && StreamUtils.IsLive(user.PrivateAccessKey, stats)) {
var uptime = TimeSpan.FromMilliseconds(StreamUtils.GetClientTime(user.PrivateAccessKey, stats)).StripMilliseconds();
if (user.AllowRestream && !string.IsNullOrWhiteSpace(user.RestreamTargets)) {
if (StreamUtils.GetClientTime(user.Username, stats) > 5000) {
var restreams = StreamUtils.CountLiveRestreams(user.PrivateAccessKey, stats);
if (restreams > 0) {
<button class="btn btn-success" role="button" style="width:20ch" disabled>Live & restreaming</button>
}
else {
<button class="btn btn-warning" role="button" style="width:22ch" disabled>Live & restream down</button>
}
}
else {
<button class="btn btn-dark" role="button" style="width:13ch" disabled>Starting...</button>
}
}
else {
<button class="btn btn-success" role="button" style="width:13ch" disabled>Live</button>
}
<button class="btn btn-dark" role="button" disabled>@uptime.ToString("c")</button>
}
else {
<button class="btn btn-danger" role="button" style="width:13ch" disabled>No data</button>
}

View File

@ -5,80 +5,84 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RTMPDash.DataModels;
namespace RTMPDash.Pages {
public class DashboardModel : PageModel {
public void OnGet() { }
namespace RTMPDash.Pages;
public void OnPost() {
if (!Request.HasFormContentType
|| string.IsNullOrWhiteSpace(Request.Form["action"])
|| string.IsNullOrWhiteSpace(HttpContext.Session.GetString("authenticatedUser")))
return;
public class DashboardModel : PageModel {
public void OnGet() { }
using var db = new AppDb.DbConn();
var user = db.Users.FirstOrDefault(p => p.Username == HttpContext.Session.GetString("authenticatedUser"));
public void OnPost() {
if (!Request.HasFormContentType || string.IsNullOrWhiteSpace(Request.Form["action"]) || string.IsNullOrWhiteSpace(HttpContext.Session.GetString("authenticatedUser")))
return;
if (Request.Form["action"] == "password_change") {
var newPass = Request.Form["pass"];
user!.Password = newPass.ToString().Sha256();
db.Update(user);
Response.Redirect("/Logout");
}
using var db = new AppDb.DbConn();
var user = db.Users.FirstOrDefault(p => p.Username == HttpContext.Session.GetString("authenticatedUser"));
if (Request.Form["action"] == "chaturl_set") {
user!.ChatUrl = Request.Form["value"];
if (Request.Form["action"] == "password_change") {
var newPass = Request.Form["pass"];
user!.Password = newPass.ToString().Sha256();
db.Update(user);
Response.Redirect("/Logout");
}
if (Request.Form["action"] == "chaturl_set") {
user!.ChatUrl = Request.Form["value"];
db.Update(user);
Response.Redirect("/Dashboard");
}
if (Request.Form["action"] == "announceurl_set") {
user!.AnnouncementUrl = Request.Form["value"];
db.Update(user);
Response.Redirect("/Dashboard");
}
if (user!.AllowRestream) {
if (Request.Form["action"] == "restream_urls_set") {
user!.RestreamUrls = Request.Form["value"];
db.Update(user);
Response.Redirect("/Dashboard");
}
if (Request.Form["action"] == "announceurl_set") {
user!.AnnouncementUrl = Request.Form["value"];
if (Request.Form["action"] == "restream_targets_set") {
var newtgts = Request.Form["value"].ToString();
if (newtgts.Contains("localhost") || newtgts.Contains("127.0.0.1") || newtgts.Contains(user.StreamKey))
return;
user!.RestreamTargets = newtgts;
db.Update(user);
Response.Redirect("/Dashboard");
}
}
if (user!.AllowRestream) {
if (Request.Form["action"] == "restream_urls_set") {
user!.RestreamUrls = Request.Form["value"];
db.Update(user);
Response.Redirect("/Dashboard");
}
if (Request.Form["action"] == "pronoun_subj_set") {
var target = string.IsNullOrWhiteSpace(Request.Form["value"]) ? "they" : Request.Form["value"].ToString();
user!.PronounSubject = target.ToLowerInvariant();
db.Update(user);
Response.Redirect("/Dashboard");
}
if (Request.Form["action"] == "restream_targets_set") {
var newtgts = Request.Form["value"].ToString();
if (newtgts.Contains("localhost")
|| newtgts.Contains("127.0.0.1")
|| newtgts.Contains(user.StreamKey))
return;
if (Request.Form["action"] == "pronoun_poss_set") {
var target = string.IsNullOrWhiteSpace(Request.Form["value"]) ? "their" : Request.Form["value"].ToString();
user!.PronounPossessive = target.ToLowerInvariant();
db.Update(user);
Response.Redirect("/Dashboard");
}
user!.RestreamTargets = newtgts;
db.Update(user);
Response.Redirect("/Dashboard");
}
if (Request.Form["action"] == "streamkey_reset") {
user!.StreamKey = Guid.NewGuid().ToString();
db.Update(user);
}
if (Request.Form["action"] == "private_toggle") {
if (user.IsPrivate) {
user!.IsPrivate = false;
}
else {
user!.PrivateAccessKey = Guid.NewGuid().ToString();
user!.IsPrivate = true;
}
if (Request.Form["action"] == "pronoun_subj_set") {
var target = string.IsNullOrWhiteSpace(Request.Form["value"])
? "they"
: Request.Form["value"].ToString();
user!.PronounSubject = target.ToLowerInvariant();
db.Update(user);
Response.Redirect("/Dashboard");
}
if (Request.Form["action"] == "pronoun_poss_set") {
var target = string.IsNullOrWhiteSpace(Request.Form["value"])
? "their"
: Request.Form["value"].ToString();
user!.PronounPossessive = target.ToLowerInvariant();
db.Update(user);
Response.Redirect("/Dashboard");
}
if (Request.Form["action"] == "streamkey_reset") {
user!.StreamKey = Guid.NewGuid().ToString();
db.Update(user);
}
db.Update(user);
}
}
}

View File

@ -1,13 +1,17 @@
@page
@using RTMPDash.DataModels
@model IndexModel
@{
ViewData["Title"] = "Home";
var liveUsers = StreamUtils.ListLiveUsers();
var db = new AppDb.DbConn();
var allStreams = StreamUtils.ListLiveUsers();
var allUsers = db.Users.Where(p => !p.IsPrivate).Select(p => p.Username);
var liveUsers = allStreams.Intersect(allUsers);
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
@if (liveUsers.Count > 0) {
@if (liveUsers.Any()) {
<p>The following users are currently live:</p>
<div class="btn-group btn-group" role="group">
@foreach (var user in liveUsers) {

View File

@ -6,40 +6,46 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
using RTMPDash.DataModels;
using RTMPDash.DataModels.Tables;
namespace RTMPDash.Pages {
public class RegisterModel : PageModel {
public void OnPost() {
if (!Request.HasFormContentType
|| string.IsNullOrWhiteSpace(Request.Form["user"])
|| string.IsNullOrWhiteSpace(Request.Form["pass"])
|| string.IsNullOrWhiteSpace(Request.Form["code"]))
return;
namespace RTMPDash.Pages;
using var db = new AppDb.DbConn();
if (!db.Invites.Any(p => p.Code == Request.Form["code"]))
return;
public class RegisterModel : PageModel {
public void OnPost() {
if (!Request.HasFormContentType
|| string.IsNullOrWhiteSpace(Request.Form["user"])
|| string.IsNullOrWhiteSpace(Request.Form["pass"])
|| string.IsNullOrWhiteSpace(Request.Form["code"]))
return;
var user = db.Users.FirstOrDefault(p => p.Username == Request.Form["user"].ToString());
if (user != null) {
//user already exists
Response.Redirect("/Register?e=user_exists");
return;
}
using var db = new AppDb.DbConn();
if (!db.Invites.Any(p => p.Code == Request.Form["code"]))
return;
user = new User {
Username = Request.Form["user"].ToString(),
Password = Request.Form["pass"].ToString().Sha256(),
StreamKey = Guid.NewGuid().ToString(),
PronounSubject = "they",
PronounPossessive = "their",
AllowRestream = true
};
db.Insert(user);
db.Delete(db.Invites.First(p => p.Code == Request.Form["code"]));
HttpContext.Session.SetString("authenticatedUser", user.Username);
var user = db.Users.FirstOrDefault(p => p.Username == Request.Form["user"].ToString());
if (user != null) {
//user already exists
Response.Redirect("/Register?e=user_exists");
return;
}
if (db.Users.Any(p => p.StreamKey == Request.Form["user"] || p.PrivateAccessKey == Request.Form["user"])) {
//user invalid
Response.Redirect("/Register?e=user_invalid");
return;
}
user = new User {
Username = Request.Form["user"].ToString(),
Password = Request.Form["pass"].ToString().Sha256(),
StreamKey = Guid.NewGuid().ToString(),
PronounSubject = "they",
PronounPossessive = "their",
AllowRestream = true
};
db.Insert(user);
db.Delete(db.Invites.First(p => p.Code == Request.Form["code"]));
HttpContext.Session.SetString("authenticatedUser", user.Username);
}
}

View File

@ -10,7 +10,7 @@
}
var user = db.Users.First(p => p.Username == Model.User);
var stats = StreamUtils.GetStatsObject();
var live = StreamUtils.IsLive(user.Username, stats);
var live = StreamUtils.IsLive(user.Username, stats) && !user.IsPrivate;
Stream stream = null;
if (live) {
stream = stats.Server.Applications.First(p => p.Name == "ingress").MethodLive.Streams.FirstOrDefault(p => p.Name == user.Username);

View File

@ -5,34 +5,34 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using RTMPDash.DataModels;
namespace RTMPDash {
public class Program {
public const string SiteName = "chaos.stream";
public const string IngressDomain = "rtmp://chaos.stream";
public const string RootDomain = "https://chaos.stream";
public const string PlayerDomain = "https://live.on.chaos.stream";
public const string FragmentDomain = "https://cdn.chaos.stream";
public const string StatsDomain = "https://stats.chaos.stream";
public const string PrivacyEmail = "chaosstream-privacy@zotan.email";
public const string CopyrightEmail = "chaosstream-copyright@zotan.email";
public const string AbuseEmail = "chaosstream-abuse@zotan.email";
public const string ServiceAnnouncementUrl = "https://t.me/chaosstream";
public const string ServiceStatusUrl = "https://status.chaos.stream";
namespace RTMPDash;
public const string ContactInfo =
"<a href=\"https://t.me/zotan\" target=\"_blank\">Telegram</a>, <a href=\"https://threema.id/S59S9U8J\" target=\"_blank\">Threema</a>, or via <a href=\"mailto:chaosstream-contact@zotan.email\" target=\"_blank\">email</a>.";
public class Program {
public const string SiteName = "chaos.stream";
public const string IngressDomain = "rtmp://chaos.stream";
public const string RootDomain = "https://chaos.stream";
public const string PlayerDomain = "https://live.on.chaos.stream";
public const string FragmentDomain = "https://cdn.chaos.stream";
public const string StatsDomain = "https://stats.chaos.stream";
public const string PrivacyEmail = "chaosstream-privacy@zotan.email";
public const string CopyrightEmail = "chaosstream-copyright@zotan.email";
public const string AbuseEmail = "chaosstream-abuse@zotan.email";
public const string ServiceAnnouncementUrl = "https://t.me/chaosstream";
public const string ServiceStatusUrl = "https://status.chaos.stream";
public static void Main(string[] args) {
DataConnection.DefaultSettings = new AppDb.Settings();
ThreadPool.SetMinThreads(100, 100);
CreateHostBuilder(args).Build().Run();
}
public const string ContactInfo =
"<a href=\"https://t.me/zotan\" target=\"_blank\">Telegram</a>, <a href=\"https://threema.id/S59S9U8J\" target=\"_blank\">Threema</a>, or via <a href=\"mailto:chaosstream-contact@zotan.email\" target=\"_blank\">email</a>.";
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
public static void Main(string[] args) {
DataConnection.DefaultSettings = new AppDb.Settings();
ThreadPool.SetMinThreads(100, 100);
Migrations.RunMigrations();
CreateHostBuilder(args).Build().Run();
}
public static class TimeExtensions {
public static TimeSpan StripMilliseconds(this TimeSpan time) => new(time.Days, time.Hours, time.Minutes, time.Seconds);
}
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); });
}
public static class TimeExtensions {
public static TimeSpan StripMilliseconds(this TimeSpan time) => new(time.Days, time.Hours, time.Minutes, time.Seconds);
}