using System.Text.RegularExpressions; using SpacetimeDB; public static partial class Module { [Table(Name = "User", Public = true)] public partial class User { [PrimaryKey] public Identity Identity; public string? Name; public string? Color; public bool Online; } [Table(Name = "Message", Public = true)] public partial class Message { public Identity Sender; public Timestamp Sent; public string Text = ""; } [Reducer] public static void SetName(ReducerContext ctx, string name) { name = ValidateName(name); var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { user.Name = name; ctx.Db.User.Identity.Update(user); } } /// Takes a name and checks if it's acceptable as a user's name. private static string ValidateName(string name) { if (string.IsNullOrEmpty(name)) { throw new Exception("Names must not be empty"); } return name; } [Reducer] public static void SetColor(ReducerContext ctx, string color) { color = ValidateColor(color); var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is null) { return; } user.Color = color; ctx.Db.User.Identity.Update(user); } private static string ValidateColor(string color) { var regex = new Regex("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$"); if (regex.IsMatch(color)) { throw new Exception("Invalid color code"); } return color; } [Reducer] public static void SendMessage(ReducerContext ctx, string text) { text = ValidateMessage(text); Log.Info(text); ctx.Db.Message.Insert( new Message { Sender = ctx.Sender, Text = text, Sent = ctx.Timestamp, } ); } /// Takes a message's text and checks if it's acceptable to send. private static string ValidateMessage(string text) { if (string.IsNullOrEmpty(text)) { throw new ArgumentException("Messages must not be empty"); } return text; } [Reducer(ReducerKind.ClientConnected)] public static void ClientConnected(ReducerContext ctx) { Log.Info($"Connect {ctx.Sender}"); var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { // If this is a returning user, i.e., we already have a `User` with this `Identity`, // set `Online: true`, but leave `Name` and `Identity` unchanged. user.Online = true; ctx.Db.User.Identity.Update(user); } else { // If this is a new user, create a `User` object for the `Identity`, // which is online, but hasn't set a name. ctx.Db.User.Insert( new User { Name = null, Identity = ctx.Sender, Online = true, } ); } } [Reducer(ReducerKind.ClientDisconnected)] public static void ClientDisconnected(ReducerContext ctx) { var user = ctx.Db.User.Identity.Find(ctx.Sender); if (user is not null) { // This user should exist, so set `Online: false`. user.Online = false; ctx.Db.User.Identity.Update(user); } else { // User does not exist, log warning Log.Warn("Warning: No user found for disconnected client."); } } }