mirror of
https://github.com/Jetsparrow/jetherald.git
synced 2026-01-20 23:56:08 +03:00
rewrite working with db.
This commit is contained in:
parent
4cdeb8a7c7
commit
31c6ced153
@ -21,7 +21,8 @@ public class ListCommand : IChatCommand
|
||||
|
||||
var msg = update.Message;
|
||||
var chatid = msg.Chat.Id;
|
||||
var topics = await Db.GetTopicsForSub(NamespacedId.Telegram(chatid));
|
||||
using var ctx = await Db.GetContext();
|
||||
var topics = await ctx.GetTopicsForSub(NamespacedId.Telegram(chatid));
|
||||
|
||||
return topics.Any()
|
||||
? "Topics:\n" + string.Join("\n", topics)
|
||||
|
||||
@ -25,7 +25,8 @@ public class SubscribeCommand : IChatCommand
|
||||
var chat = NamespacedId.Telegram(args.Message.Chat.Id);
|
||||
var token = cmd.Parameters[0];
|
||||
|
||||
var topic = await Db.GetTopicForSub(token, chat);
|
||||
using var ctx = await Db.GetContext();
|
||||
var topic = await ctx.GetTopicForSub(token, chat);
|
||||
|
||||
if (topic == null)
|
||||
return "topic not found";
|
||||
@ -35,7 +36,8 @@ public class SubscribeCommand : IChatCommand
|
||||
return "token mismatch";
|
||||
else
|
||||
{
|
||||
await Db.CreateSubscription(topic.TopicId, chat);
|
||||
await ctx.CreateSubscription(topic.TopicId, chat);
|
||||
ctx.Commit();
|
||||
return $"subscribed to {topic.Name}";
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,9 @@ public class UnsubscribeCommand : IChatCommand
|
||||
var chat = NamespacedId.Telegram(msg.Chat.Id);
|
||||
|
||||
var topicName = cmd.Parameters[0];
|
||||
int affected = await Db.RemoveSubscription(topicName, chat);
|
||||
using var ctx = await Db.GetContext();
|
||||
int affected = await ctx.RemoveSubscription(topicName, chat);
|
||||
ctx.Commit();
|
||||
if (affected >= 1)
|
||||
return $"unsubscribed from {topicName}";
|
||||
else
|
||||
|
||||
@ -67,7 +67,8 @@ public class HeartbeatController : ControllerBase
|
||||
{
|
||||
var heart = args.Heart ?? "General";
|
||||
|
||||
var t = await Db.GetTopic(args.Topic);
|
||||
var ctx = await Db.GetContext();
|
||||
var t = await ctx.GetTopic(args.Topic);
|
||||
if (t == null)
|
||||
return new NotFoundResult();
|
||||
else if (!t.WriteToken.Equals(args.WriteToken, StringComparison.Ordinal))
|
||||
@ -76,8 +77,8 @@ public class HeartbeatController : ControllerBase
|
||||
if (Timeouts.IsTimedOut(t.TopicId))
|
||||
return StatusCode(StatusCodes.Status429TooManyRequests);
|
||||
|
||||
var wasBeating = await Db.ReportHeartbeat(t.TopicId, heart, args.ExpiryTimeout);
|
||||
|
||||
var wasBeating = await ctx.ReportHeartbeat(t.TopicId, heart, args.ExpiryTimeout);
|
||||
ctx.Commit();
|
||||
if (wasBeating == 0)
|
||||
await Herald.BroadcastMessageRaw(t.TopicId, $"!{t.Description}!:\nHeart \"{heart}\" has started beating at {DateTime.UtcNow:O}");
|
||||
|
||||
|
||||
@ -52,7 +52,12 @@ public class ReportController : ControllerBase
|
||||
|
||||
private async Task<IActionResult> DoReport(ReportArgs args)
|
||||
{
|
||||
var t = await Db.GetTopic(args.Topic);
|
||||
Contracts.Topic t;
|
||||
using (var ctx = await Db.GetContext())
|
||||
{
|
||||
t = await ctx.GetTopic(args.Topic);
|
||||
}
|
||||
|
||||
if (t == null)
|
||||
return new NotFoundResult();
|
||||
else if (!t.WriteToken.Equals(args.WriteToken, StringComparison.OrdinalIgnoreCase))
|
||||
|
||||
@ -31,9 +31,10 @@ public class AdminToolsController : Controller
|
||||
[HttpGet, Route("ui/admintools/invites")]
|
||||
public async Task<IActionResult> ViewInvites()
|
||||
{
|
||||
var invites = await Db.GetInvites();
|
||||
var plans = await Db.GetPlans();
|
||||
var roles = await Db.GetRoles();
|
||||
using var ctx = await Db.GetContext();
|
||||
var invites = await ctx.GetInvites();
|
||||
var plans = await ctx.GetPlans();
|
||||
var roles = await ctx.GetRoles();
|
||||
return View(new ViewInvitesModel
|
||||
{
|
||||
Invites = invites.ToArray(),
|
||||
@ -52,7 +53,9 @@ public class AdminToolsController : Controller
|
||||
[HttpPost, Route("ui/admintools/invites/create")]
|
||||
public async Task<IActionResult> CreateInvite(CreateInviteRequest req)
|
||||
{
|
||||
await Db.CreateUserInvite(req.PlanId, req.RoleId, TokenHelper.GetToken(AuthCfg.InviteCodeLength));
|
||||
using var ctx = await Db.GetContext();
|
||||
await ctx.CreateUserInvite(req.PlanId, req.RoleId, TokenHelper.GetToken(AuthCfg.InviteCodeLength));
|
||||
ctx.Commit();
|
||||
return RedirectToAction(nameof(ViewInvites));
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,9 +21,10 @@ public class DashboardController : Controller
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var login = HttpContext.User.GetUserLogin();
|
||||
var user = await Db.GetUser(login);
|
||||
var topics = await Db.GetTopicsForUser(user.UserId);
|
||||
var hearts = await Db.GetHeartsForUser(user.UserId);
|
||||
using var ctx = await Db.GetContext();
|
||||
var user = await ctx.GetUser(login);
|
||||
var topics = await ctx.GetTopicsForUser(user.UserId);
|
||||
var hearts = await ctx.GetHeartsForUser(user.UserId);
|
||||
var vm = new DashboardViewModel
|
||||
{
|
||||
Topics = topics.ToArray(),
|
||||
|
||||
@ -51,7 +51,8 @@ public class LoginController : Controller
|
||||
|
||||
ViewData["RedirectTo"] = PathStringOrDefault(redirect);
|
||||
|
||||
var user = await Db.GetUser(req.Username);
|
||||
using var ctx = await Db.GetContext();
|
||||
var user = await ctx.GetUser(req.Username);
|
||||
if (user == null)
|
||||
{
|
||||
ModelState.AddModelError("", "User not found");
|
||||
|
||||
@ -19,7 +19,8 @@ public class ProfileController : Controller
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var login = HttpContext.User.GetUserLogin();
|
||||
var user = await Db.GetUser(login);
|
||||
using var ctx = await Db.GetContext();
|
||||
var user = await ctx.GetUser(login);
|
||||
|
||||
var vm = new ProfileViewModel
|
||||
{
|
||||
|
||||
@ -69,13 +69,14 @@ public class RegistrationController : Controller
|
||||
|
||||
ViewData["RedirectTo"] = PathStringOrDefault(redirect);
|
||||
|
||||
var oldUser = await Db.GetUser(req.Login);
|
||||
using var ctx = await Db.GetContext();
|
||||
var oldUser = await ctx.GetUser(req.Login);
|
||||
if (oldUser != null)
|
||||
{
|
||||
ModelState.AddModelError("", "User already exists");
|
||||
return View();
|
||||
}
|
||||
var invite = await Db.GetInviteByCode(req.InviteCode);
|
||||
var invite = await ctx.GetInviteByCode(req.InviteCode);
|
||||
if (invite == null || invite.RedeemedBy != default)
|
||||
{
|
||||
ModelState.AddModelError("", "No unredeemed invite with this code found");
|
||||
@ -91,8 +92,9 @@ public class RegistrationController : Controller
|
||||
PasswordSalt = RandomNumberGenerator.GetBytes(64)
|
||||
};
|
||||
user.PasswordHash = AuthUtils.GetHashFor(req.Password, user.PasswordSalt, user.HashType);
|
||||
user = await Db.RegisterUser(user);
|
||||
await Db.RedeemInvite(invite.UserInviteId, user.UserId);
|
||||
user = await ctx.RegisterUser(user);
|
||||
await ctx.RedeemInvite(invite.UserInviteId, user.UserId);
|
||||
ctx.Commit();
|
||||
var userIdentity = AuthUtils.CreateIdentity(user.UserId, user.Login, user.Name, user.Allow);
|
||||
var principal = new ClaimsPrincipal(userIdentity);
|
||||
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
|
||||
|
||||
@ -36,7 +36,9 @@ public class TopicController : Controller
|
||||
if (!ModelState.IsValid)
|
||||
return View();
|
||||
var userId = HttpContext.User.GetUserId();
|
||||
var topic = await Db.CreateTopic(userId, req.Name, req.Description);
|
||||
using var ctx = await Db.GetContext();
|
||||
var topic = await ctx.CreateTopic(userId, req.Name, req.Description);
|
||||
ctx.Commit();
|
||||
if (topic == null)
|
||||
{
|
||||
ModelState.AddModelError("", "Unknown error");
|
||||
@ -50,11 +52,12 @@ public class TopicController : Controller
|
||||
public async Task<IActionResult> ViewTopic(string topicName)
|
||||
{
|
||||
var userId = HttpContext.User.GetUserId();
|
||||
var topic = await Db.GetTopic(topicName);
|
||||
using var ctx = await Db.GetContext();
|
||||
var topic = await ctx.GetTopic(topicName);
|
||||
if (topic == null || topic.CreatorId != userId)
|
||||
return NotFound();
|
||||
|
||||
var hearts = await Db.GetHeartsForTopic(topic.TopicId);
|
||||
var hearts = await ctx.GetHeartsForTopic(topic.TopicId);
|
||||
var vm = new TopicViewModel
|
||||
{
|
||||
Topic = topic,
|
||||
|
||||
@ -12,7 +12,8 @@ public class AnonymousUserMassagerMiddleware : IMiddleware
|
||||
{
|
||||
AnonymousPermissions = new Lazy<Task<string>>(async () =>
|
||||
{
|
||||
var anonymousUser = await db.GetUser("Anonymous");
|
||||
using var ctx = await db.GetContext();
|
||||
var anonymousUser = await ctx.GetUser("Anonymous");
|
||||
return anonymousUser.Allow;
|
||||
});
|
||||
}
|
||||
|
||||
@ -91,12 +91,12 @@ try
|
||||
// preflight checks
|
||||
{
|
||||
var db = app.Services.GetService<Db>();
|
||||
|
||||
var adminUser = await db.GetUser("admin");
|
||||
using var ctx = await db.GetContext();
|
||||
var adminUser = await ctx.GetUser("admin");
|
||||
if (adminUser == null)
|
||||
{
|
||||
var adminRole = (await db.GetRoles()).First(r => r.Name == "admin");
|
||||
var unlimitedPlan = (await db.GetPlans()).First(p => p.Name == "unlimited");
|
||||
var adminRole = (await ctx.GetRoles()).First(r => r.Name == "admin");
|
||||
var unlimitedPlan = (await ctx.GetPlans()).First(p => p.Name == "unlimited");
|
||||
|
||||
var authCfg = app.Services.GetService<IOptions<AuthConfig>>().Value;
|
||||
var password = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48));
|
||||
@ -110,7 +110,8 @@ try
|
||||
PlanId = unlimitedPlan.PlanId
|
||||
};
|
||||
adminUser.PasswordHash = AuthUtils.GetHashFor(password, adminUser.PasswordSalt, adminUser.HashType);
|
||||
var newUser = await db.RegisterUser(adminUser);
|
||||
var newUser = await ctx.RegisterUser(adminUser);
|
||||
ctx.Commit();
|
||||
log.Warn($"Created administrative account {adminUser.Login}:{password}. Be sure to save these credentials somewhere!");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,288 +0,0 @@
|
||||
using MySql.Data.MySqlClient;
|
||||
using Dapper;
|
||||
using JetHerald.Options;
|
||||
using JetHerald.Contracts;
|
||||
|
||||
namespace JetHerald.Services;
|
||||
public class Db
|
||||
{
|
||||
public async Task<IEnumerable<Topic>> GetTopicsForUser(uint userId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<Topic>(
|
||||
" SELECT * FROM topic WHERE CreatorId = @userId",
|
||||
new { userId });
|
||||
}
|
||||
public async Task<IEnumerable<Plan>> GetPlans()
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<Plan>("SELECT * FROM plan");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Role>> GetRoles()
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<Role>("SELECT * FROM role");
|
||||
}
|
||||
public async Task<IEnumerable<UserInvite>> GetInvites()
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<UserInvite>("SELECT * FROM userinvite");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Heart>> GetHeartsForUser(uint userId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<Heart>(
|
||||
" SELECT h.* FROM heart h JOIN topic USING (TopicId) WHERE CreatorId = @userId",
|
||||
new { userId });
|
||||
}
|
||||
|
||||
public async Task CreateUserInvite(uint planId, uint roleId, string inviteCode)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
await c.ExecuteAsync(@"
|
||||
INSERT INTO userinvite
|
||||
( PlanId, RoleId, InviteCode)
|
||||
VALUES
|
||||
(@planId, @roleId, @inviteCode)",
|
||||
new { planId, roleId, inviteCode });
|
||||
}
|
||||
|
||||
public async Task<Topic> GetTopic(string name)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<Topic>(
|
||||
"SELECT * FROM topic WHERE Name = @name",
|
||||
new { name });
|
||||
}
|
||||
|
||||
public async Task<int> DeleteTopic(string name, uint userId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.ExecuteAsync(
|
||||
" DELETE FROM topic WHERE Name = @name AND CreatorId = @userId",
|
||||
new { name, userId });
|
||||
}
|
||||
|
||||
|
||||
public async Task<Topic> GetTopicForSub(string token, NamespacedId sub)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<Topic>(
|
||||
" SELECT t.*, ts.Sub " +
|
||||
" FROM topic t " +
|
||||
" LEFT JOIN topic_sub ts ON t.TopicId = ts.TopicId AND ts.Sub = @sub " +
|
||||
" WHERE ReadToken = @token",
|
||||
new { token, sub });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Heart>> GetHeartsForTopic(uint topicId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<Heart>(
|
||||
" SELECT * FROM heart WHERE TopicId = @topicId",
|
||||
new { topicId });
|
||||
}
|
||||
public async Task<User> GetUser(string login)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<User>(@"
|
||||
SELECT u.*, up.*, ur.*
|
||||
FROM user u
|
||||
JOIN plan up ON u.PlanId = up.PlanId
|
||||
JOIN role ur ON u.RoleId = ur.RoleId
|
||||
WHERE u.Login = @login;",
|
||||
new { login });
|
||||
}
|
||||
|
||||
public async Task<Topic> CreateTopic(uint user, string name, string descr)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
|
||||
await c.OpenAsync();
|
||||
|
||||
await using var tx = await c.BeginTransactionAsync();
|
||||
|
||||
var topicsCount = await c.QuerySingleAsync<int>(
|
||||
" SELECT COUNT(*) " +
|
||||
" FROM user u " +
|
||||
" LEFT JOIN topic t ON t.CreatorId = u.UserId " +
|
||||
" WHERE u.UserId = @user",
|
||||
new { user },
|
||||
transaction: tx
|
||||
);
|
||||
|
||||
var planTopicsCount = await c.QuerySingleAsync<int>(
|
||||
" SELECT p.MaxTopics " +
|
||||
" FROM user u " +
|
||||
" LEFT JOIN plan p ON p.PlanId = u.PlanId " +
|
||||
" WHERE u.UserId = @user",
|
||||
new { user },
|
||||
transaction: tx
|
||||
);
|
||||
|
||||
if (topicsCount >= planTopicsCount) return null;
|
||||
|
||||
var topic = await c.QuerySingleOrDefaultAsync<Topic>(
|
||||
" INSERT INTO topic " +
|
||||
" ( CreatorId, Name, Description, ReadToken, WriteToken) " +
|
||||
" VALUES " +
|
||||
" (@CreatorId, @Name, @Description, @ReadToken, @WriteToken); " +
|
||||
" SELECT * FROM topic WHERE TopicId = LAST_INSERT_ID(); ",
|
||||
new Topic
|
||||
{
|
||||
CreatorId = user,
|
||||
Name = name,
|
||||
Description = descr,
|
||||
ReadToken = TokenHelper.GetToken(),
|
||||
WriteToken = TokenHelper.GetToken()
|
||||
}, transaction: tx);
|
||||
|
||||
await tx.CommitAsync();
|
||||
|
||||
return topic;
|
||||
}
|
||||
|
||||
public async Task<User> RegisterUser(User user)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
uint userId = await c.QuerySingleOrDefaultAsync<uint>(@"
|
||||
INSERT INTO user
|
||||
( Login, Name, PasswordHash, PasswordSalt, HashType, PlanId, RoleId)
|
||||
VALUES
|
||||
(@Login, @Name, @PasswordHash, @PasswordSalt, @HashType, @PlanId, @RoleId);",
|
||||
param:user);
|
||||
return await GetUser(user.Login);
|
||||
}
|
||||
|
||||
public async Task RedeemInvite(uint inviteId, uint userId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
await c.ExecuteAsync(
|
||||
@"UPDATE userinvite SET RedeemedBy = @userId WHERE UserInviteId = @inviteId",
|
||||
new { inviteId, userId });
|
||||
}
|
||||
|
||||
public async Task<UserInvite> GetInviteByCode(string inviteCode)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<UserInvite>(
|
||||
" SELECT * FROM userinvite " +
|
||||
" WHERE InviteCode = @inviteCode " +
|
||||
" AND RedeemedBy IS NULL ",
|
||||
new { inviteCode });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<NamespacedId>> GetSubsForTopic(uint topicId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<NamespacedId>(
|
||||
" SELECT Sub " +
|
||||
" FROM topic_sub " +
|
||||
" WHERE TopicId = @topicid",
|
||||
new { topicId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Topic>> GetTopicsForSub(NamespacedId sub)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<Topic>(
|
||||
" SELECT t.*" +
|
||||
" FROM topic_sub ts" +
|
||||
" JOIN topic t USING (TopicId)" +
|
||||
" WHERE ts.Sub = @sub",
|
||||
new { sub });
|
||||
}
|
||||
|
||||
public async Task CreateSubscription(uint topicId, NamespacedId sub)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
await c.ExecuteAsync(
|
||||
" INSERT INTO topic_sub" +
|
||||
" (TopicId, Sub)" +
|
||||
" VALUES" +
|
||||
" (@topicId, @sub)",
|
||||
new { topicId, sub });
|
||||
}
|
||||
|
||||
public async Task<int> RemoveSubscription(string topicName, NamespacedId sub)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.ExecuteAsync(
|
||||
" DELETE ts " +
|
||||
" FROM topic_sub ts" +
|
||||
" JOIN topic t USING (TopicId) " +
|
||||
" WHERE t.Name = @topicName AND ts.Sub = @sub;",
|
||||
new { topicName, sub });
|
||||
}
|
||||
|
||||
|
||||
public async Task<int> ReportHeartbeat(uint topicId, string heart, int timeoutSeconds)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryFirstAsync<int>(
|
||||
@"CALL report_heartbeat(@topicId, @heart, @timeoutSeconds);",
|
||||
new { topicId, heart, timeoutSeconds });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<HeartEvent>> ProcessHearts()
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QueryAsync<HeartEvent>("CALL process_hearts();");
|
||||
}
|
||||
|
||||
public async Task MarkHeartAttackReported(ulong id)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
await c.ExecuteAsync("UPDATE heartevent SET Status = 'reported' WHERE HeartEventId = @id", new { id });
|
||||
}
|
||||
|
||||
#region authorization
|
||||
|
||||
public async Task RemoveSession(string sessionId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
await c.ExecuteAsync("DELETE FROM usersession WHERE SessionId = @sessionId", new {sessionId});
|
||||
}
|
||||
public async Task<UserSession> GetSession(string sessionId)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
return await c.QuerySingleOrDefaultAsync<UserSession>(
|
||||
"SELECT * FROM usersession WHERE SessionId = @sessionId",
|
||||
new { sessionId });
|
||||
}
|
||||
|
||||
public async Task UpdateSession(string sessionId, byte[] data, DateTime expiryTs)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
await c.ExecuteAsync(@"
|
||||
UPDATE usersession SET
|
||||
SessionData = @data,
|
||||
ExpiryTs = @expiryTs
|
||||
WHERE SessionId = @sessionId;",
|
||||
new { sessionId, data, expiryTs });
|
||||
}
|
||||
|
||||
public async Task<string> CreateSession(string sessionId, byte[] data, DateTime expiryTs)
|
||||
{
|
||||
using var c = GetConnection();
|
||||
await c.ExecuteAsync(@"
|
||||
INSERT INTO usersession
|
||||
(SessionId, SessionData, ExpiryTs)
|
||||
VALUES
|
||||
(@sessionId, @data, @expiryTs);",
|
||||
new { sessionId, data, expiryTs });
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public Db(IOptionsMonitor<ConnectionStrings> cfg)
|
||||
{
|
||||
Config = cfg;
|
||||
}
|
||||
IOptionsMonitor<ConnectionStrings> Config { get; }
|
||||
public MySqlConnection GetConnection() => new(Config.CurrentValue.DefaultConnection);
|
||||
}
|
||||
|
||||
238
JetHerald/Services/DbContext.cs
Normal file
238
JetHerald/Services/DbContext.cs
Normal file
@ -0,0 +1,238 @@
|
||||
using System.Data;
|
||||
using System.Threading;
|
||||
using System.ComponentModel;
|
||||
using MySql.Data.MySqlClient;
|
||||
using Dapper.Transaction;
|
||||
using JetHerald.Options;
|
||||
using JetHerald.Contracts;
|
||||
|
||||
namespace JetHerald.Services;
|
||||
|
||||
public class Db
|
||||
{
|
||||
public Db(IOptionsMonitor<ConnectionStrings> cfg)
|
||||
{
|
||||
Config = cfg;
|
||||
}
|
||||
IOptionsMonitor<ConnectionStrings> Config { get; }
|
||||
MySqlConnection GetConnection() => new(Config.CurrentValue.DefaultConnection);
|
||||
public async Task<DbContext> GetContext(
|
||||
IsolationLevel lvl = IsolationLevel.RepeatableRead,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var conn = GetConnection();
|
||||
if (conn.State != ConnectionState.Open)
|
||||
await conn.OpenAsync();
|
||||
|
||||
var tran = await conn.BeginTransactionAsync(lvl, token);
|
||||
return new DbContext(tran);
|
||||
}
|
||||
}
|
||||
|
||||
public class DbContext : IDisposable
|
||||
{
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public DbContext(IDbTransaction tran)
|
||||
{
|
||||
Tran = tran;
|
||||
Conn = Tran.Connection;
|
||||
}
|
||||
|
||||
IDbConnection Conn;
|
||||
IDbTransaction Tran;
|
||||
|
||||
public void Commit() => Tran.Commit();
|
||||
public void Dispose()
|
||||
{
|
||||
Tran.Dispose();
|
||||
Conn.Dispose();
|
||||
}
|
||||
public Task<IEnumerable<Topic>> GetTopicsForUser(uint userId)
|
||||
=> Tran.QueryAsync<Topic>(
|
||||
" SELECT * FROM topic WHERE CreatorId = @userId",
|
||||
new { userId });
|
||||
public Task<IEnumerable<Plan>> GetPlans()
|
||||
=> Tran.QueryAsync<Plan>("SELECT * FROM plan");
|
||||
public Task<IEnumerable<Role>> GetRoles()
|
||||
=> Tran.QueryAsync<Role>("SELECT * FROM role");
|
||||
public Task<IEnumerable<UserInvite>> GetInvites()
|
||||
=> Tran.QueryAsync<UserInvite>("SELECT * FROM userinvite");
|
||||
|
||||
public Task<IEnumerable<Heart>> GetHeartsForUser(uint userId)
|
||||
=> Tran.QueryAsync<Heart>(
|
||||
" SELECT h.* FROM heart h JOIN topic USING (TopicId) WHERE CreatorId = @userId",
|
||||
new { userId });
|
||||
|
||||
public Task CreateUserInvite(uint planId, uint roleId, string inviteCode)
|
||||
=> Tran.ExecuteAsync(@"
|
||||
INSERT INTO userinvite
|
||||
( PlanId, RoleId, InviteCode)
|
||||
VALUES
|
||||
(@planId, @roleId, @inviteCode)",
|
||||
new { planId, roleId, inviteCode });
|
||||
|
||||
public Task<Topic> GetTopic(string name)
|
||||
=> Tran.QuerySingleOrDefaultAsync<Topic>(
|
||||
"SELECT * FROM topic WHERE Name = @name",
|
||||
new { name });
|
||||
|
||||
public Task<int> DeleteTopic(string name, uint userId)
|
||||
=> Tran.ExecuteAsync(
|
||||
" DELETE FROM topic WHERE Name = @name AND CreatorId = @userId",
|
||||
new { name, userId });
|
||||
|
||||
public Task<Topic> GetTopicForSub(string token, NamespacedId sub)
|
||||
=> Tran.QuerySingleOrDefaultAsync<Topic>(
|
||||
" SELECT t.*, ts.Sub " +
|
||||
" FROM topic t " +
|
||||
" LEFT JOIN topic_sub ts ON t.TopicId = ts.TopicId AND ts.Sub = @sub " +
|
||||
" WHERE ReadToken = @token",
|
||||
new { token, sub });
|
||||
|
||||
public Task<IEnumerable<Heart>> GetHeartsForTopic(uint topicId)
|
||||
=> Tran.QueryAsync<Heart>(
|
||||
" SELECT * FROM heart WHERE TopicId = @topicId",
|
||||
new { topicId });
|
||||
public Task<User> GetUser(string login)
|
||||
=> Tran.QuerySingleOrDefaultAsync<User>(@"
|
||||
SELECT u.*, up.*, ur.*
|
||||
FROM user u
|
||||
JOIN plan up ON u.PlanId = up.PlanId
|
||||
JOIN role ur ON u.RoleId = ur.RoleId
|
||||
WHERE u.Login = @login;",
|
||||
new { login });
|
||||
|
||||
public async Task<Topic> CreateTopic(uint user, string name, string descr)
|
||||
{
|
||||
var topicsCount = await Tran.QuerySingleAsync<int>(
|
||||
" SELECT COUNT(*) " +
|
||||
" FROM user u " +
|
||||
" LEFT JOIN topic t ON t.CreatorId = u.UserId " +
|
||||
" WHERE u.UserId = @user",
|
||||
new { user }
|
||||
);
|
||||
|
||||
var planTopicsCount = await Tran.QuerySingleAsync<int>(
|
||||
" SELECT p.MaxTopics " +
|
||||
" FROM user u " +
|
||||
" LEFT JOIN plan p ON p.PlanId = u.PlanId " +
|
||||
" WHERE u.UserId = @user",
|
||||
new { user }
|
||||
);
|
||||
|
||||
if (topicsCount >= planTopicsCount) return null;
|
||||
|
||||
var topic = await Tran.QuerySingleOrDefaultAsync<Topic>(
|
||||
" INSERT INTO topic " +
|
||||
" ( CreatorId, Name, Description, ReadToken, WriteToken) " +
|
||||
" VALUES " +
|
||||
" (@CreatorId, @Name, @Description, @ReadToken, @WriteToken); " +
|
||||
" SELECT * FROM topic WHERE TopicId = LAST_INSERT_ID(); ",
|
||||
new Topic
|
||||
{
|
||||
CreatorId = user,
|
||||
Name = name,
|
||||
Description = descr,
|
||||
ReadToken = TokenHelper.GetToken(),
|
||||
WriteToken = TokenHelper.GetToken()
|
||||
});
|
||||
return topic;
|
||||
}
|
||||
|
||||
public async Task<User> RegisterUser(User user)
|
||||
{
|
||||
_ = await Tran.QuerySingleOrDefaultAsync<uint>(@"
|
||||
INSERT INTO user
|
||||
( Login, Name, PasswordHash, PasswordSalt, HashType, PlanId, RoleId)
|
||||
VALUES
|
||||
(@Login, @Name, @PasswordHash, @PasswordSalt, @HashType, @PlanId, @RoleId);",
|
||||
param:user);
|
||||
return await GetUser(user.Login);
|
||||
}
|
||||
|
||||
public Task RedeemInvite(uint inviteId, uint userId)
|
||||
=> Tran.ExecuteAsync(
|
||||
@"UPDATE userinvite SET RedeemedBy = @userId WHERE UserInviteId = @inviteId",
|
||||
new { inviteId, userId });
|
||||
|
||||
public Task<UserInvite> GetInviteByCode(string inviteCode)
|
||||
=> Tran.QuerySingleOrDefaultAsync<UserInvite>(
|
||||
" SELECT * FROM userinvite " +
|
||||
" WHERE InviteCode = @inviteCode " +
|
||||
" AND RedeemedBy IS NULL ",
|
||||
new { inviteCode });
|
||||
|
||||
public Task<IEnumerable<NamespacedId>> GetSubsForTopic(uint topicId)
|
||||
=> Tran.QueryAsync<NamespacedId>(
|
||||
" SELECT Sub " +
|
||||
" FROM topic_sub " +
|
||||
" WHERE TopicId = @topicid",
|
||||
new { topicId });
|
||||
|
||||
public Task<IEnumerable<Topic>> GetTopicsForSub(NamespacedId sub)
|
||||
=> Tran.QueryAsync<Topic>(
|
||||
" SELECT t.*" +
|
||||
" FROM topic_sub ts" +
|
||||
" JOIN topic t USING (TopicId)" +
|
||||
" WHERE ts.Sub = @sub",
|
||||
new { sub });
|
||||
|
||||
public Task CreateSubscription(uint topicId, NamespacedId sub)
|
||||
=> Tran.ExecuteAsync(
|
||||
" INSERT INTO topic_sub" +
|
||||
" (TopicId, Sub)" +
|
||||
" VALUES" +
|
||||
" (@topicId, @sub)",
|
||||
new { topicId, sub });
|
||||
|
||||
public Task<int> RemoveSubscription(string topicName, NamespacedId sub)
|
||||
=> Tran.ExecuteAsync(
|
||||
" DELETE ts " +
|
||||
" FROM topic_sub ts" +
|
||||
" JOIN topic t USING (TopicId) " +
|
||||
" WHERE t.Name = @topicName AND ts.Sub = @sub;",
|
||||
new { topicName, sub });
|
||||
|
||||
|
||||
public Task<int> ReportHeartbeat(uint topicId, string heart, int timeoutSeconds)
|
||||
=> Tran.QueryFirstAsync<int>(
|
||||
@"CALL report_heartbeat(@topicId, @heart, @timeoutSeconds);",
|
||||
new { topicId, heart, timeoutSeconds });
|
||||
|
||||
public Task<IEnumerable<HeartEvent>> ProcessHearts()
|
||||
=> Tran.QueryAsync<HeartEvent>("CALL process_hearts();");
|
||||
|
||||
public Task MarkHeartAttackReported(ulong id)
|
||||
=> Tran.ExecuteAsync("UPDATE heartevent SET Status = 'reported' WHERE HeartEventId = @id", new { id });
|
||||
|
||||
#region TicketStore
|
||||
|
||||
public Task RemoveSession(string sessionId)
|
||||
=> Tran.ExecuteAsync("DELETE FROM usersession WHERE SessionId = @sessionId", new { sessionId });
|
||||
public Task<UserSession> GetSession(string sessionId)
|
||||
=> Tran.QuerySingleOrDefaultAsync<UserSession>(
|
||||
"SELECT * FROM usersession WHERE SessionId = @sessionId",
|
||||
new { sessionId });
|
||||
|
||||
public Task UpdateSession(string sessionId, byte[] data, DateTime expiryTs)
|
||||
=> Tran.ExecuteAsync(@"
|
||||
UPDATE usersession SET
|
||||
SessionData = @data,
|
||||
ExpiryTs = @expiryTs
|
||||
WHERE SessionId = @sessionId;",
|
||||
new { sessionId, data, expiryTs });
|
||||
|
||||
public async Task<string> CreateSession(string sessionId, byte[] data, DateTime expiryTs)
|
||||
{
|
||||
await Tran.ExecuteAsync(@"
|
||||
INSERT INTO usersession
|
||||
(SessionId, SessionData, ExpiryTs)
|
||||
VALUES
|
||||
(@sessionId, @data, @expiryTs);",
|
||||
new { sessionId, data, expiryTs });
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -21,7 +21,8 @@ public class DiscordCommands : BaseCommandModule
|
||||
_ = ctx.TriggerTypingAsync();
|
||||
|
||||
var chat = NamespacedId.Discord(ctx.Channel.Id);
|
||||
var topic = await Db.GetTopicForSub(token, chat);
|
||||
using var dbctx = await Db.GetContext();
|
||||
var topic = await dbctx.GetTopicForSub(token, chat);
|
||||
|
||||
if (topic == null)
|
||||
await ctx.RespondAsync("topic not found");
|
||||
@ -31,7 +32,8 @@ public class DiscordCommands : BaseCommandModule
|
||||
await ctx.RespondAsync("token mismatch");
|
||||
else
|
||||
{
|
||||
await Db.CreateSubscription(topic.TopicId, chat);
|
||||
await dbctx.CreateSubscription(topic.TopicId, chat);
|
||||
dbctx.Commit();
|
||||
await ctx.RespondAsync($"subscribed to {topic.Name}");
|
||||
}
|
||||
}
|
||||
@ -46,8 +48,8 @@ public class DiscordCommands : BaseCommandModule
|
||||
)
|
||||
{
|
||||
_ = ctx.TriggerTypingAsync();
|
||||
|
||||
int affected = await Db.RemoveSubscription(name, NamespacedId.Discord(ctx.Channel.Id));
|
||||
using var dbctx = await Db.GetContext();
|
||||
int affected = await dbctx.RemoveSubscription(name, NamespacedId.Discord(ctx.Channel.Id));
|
||||
if (affected >= 1)
|
||||
await ctx.RespondAsync($"unsubscribed from {name}");
|
||||
else
|
||||
|
||||
@ -24,14 +24,15 @@ public class HeartMonitor : BackgroundService
|
||||
await Task.Delay(1000 * 10, token);
|
||||
try
|
||||
{
|
||||
var attacks = await Db.ProcessHearts();
|
||||
using var ctx = await Db.GetContext();
|
||||
var attacks = await ctx.ProcessHearts();
|
||||
foreach (var a in attacks)
|
||||
{
|
||||
await Herald.BroadcastMessageRaw(
|
||||
a.TopicId,
|
||||
$"!{a.Description}!:\nHeart \"{a.Heart}\" stopped beating at {a.CreateTs:O}");
|
||||
|
||||
await Db.MarkHeartAttackReported(a.HeartEventId);
|
||||
await ctx.MarkHeartAttackReported(a.HeartEventId);
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
@ -44,7 +44,9 @@ public partial class JetHeraldBot : IHostedService
|
||||
|
||||
public async Task BroadcastMessageRaw(uint topicId, string formatted)
|
||||
{
|
||||
var chatIds = await Db.GetSubsForTopic(topicId);
|
||||
IEnumerable<NamespacedId> chatIds;
|
||||
using (var ctx = await Db.GetContext())
|
||||
chatIds = await ctx.GetSubsForTopic(topicId);
|
||||
foreach (var c in chatIds)
|
||||
await SendMessageRaw(c, formatted);
|
||||
}
|
||||
|
||||
@ -13,29 +13,40 @@ public class JetHeraldTicketStore : ITicketStore
|
||||
Db = db;
|
||||
Cfg = cfg;
|
||||
}
|
||||
public Task RemoveAsync(string key)
|
||||
=> Db.RemoveSession(key);
|
||||
|
||||
public Task RenewAsync(string key, AuthenticationTicket ticket)
|
||||
=> Db.UpdateSession(
|
||||
public async Task RemoveAsync(string key)
|
||||
{
|
||||
using var ctx = await Db.GetContext();
|
||||
await ctx.RemoveSession(key);
|
||||
ctx.Commit();
|
||||
}
|
||||
public async Task RenewAsync(string key, AuthenticationTicket ticket)
|
||||
{
|
||||
using var ctx = await Db.GetContext();
|
||||
await ctx.UpdateSession(
|
||||
key,
|
||||
TicketSerializer.Default.Serialize(ticket),
|
||||
ticket.Properties.ExpiresUtc.Value.DateTime);
|
||||
ctx.Commit();
|
||||
|
||||
}
|
||||
public async Task<AuthenticationTicket> RetrieveAsync(string key)
|
||||
{
|
||||
var userSession = await Db.GetSession(key);
|
||||
using var ctx = await Db.GetContext();
|
||||
var userSession = await ctx.GetSession(key);
|
||||
return TicketSerializer.Default.Deserialize(userSession.SessionData);
|
||||
}
|
||||
|
||||
public Task<string> StoreAsync(AuthenticationTicket ticket)
|
||||
public async Task<string> StoreAsync(AuthenticationTicket ticket)
|
||||
{
|
||||
var cfg = Cfg.CurrentValue;
|
||||
var bytes = RandomNumberGenerator.GetBytes(cfg.TicketIdLengthBytes);
|
||||
var key = Convert.ToBase64String(bytes);
|
||||
return Db.CreateSession(
|
||||
using var ctx = await Db.GetContext();
|
||||
await ctx.CreateSession(
|
||||
key,
|
||||
TicketSerializer.Default.Serialize(ticket),
|
||||
ticket.Properties.ExpiresUtc.Value.DateTime);
|
||||
ctx.Commit();
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user