Kostiantyn's Blog

Blazor Secret Santa or How to side project for fun and profit

Santa
Secret Santa.
Photo by Kim Jin Cheol on Unsplash

I struggle with side projects a lot. Usually, I am either too exhausted from my day job or can't find a thing I am passionate about. However, one small side project ticked all the boxes in my brain.

My family and close friends practice playing a so-called 'Secret Santa' game every New Years. You write your names somewhere, shuffle them, and assign everyone a person with one constraint - the target can not be yourself. Previously, one person was responsible for arranging the shuffle, and they knew all the pair assignments for the event. This year, I tried to use my skills and help my family by creating a small software product that takes the burden of organising the shuffle.

Well, I thought, why wouldn't I try some technologies along the way, and use some new hot and shiny stuff. Here I am going to show how I implemented a .NET 5 Blazor Secret Santa.

The full app is available in the kostya9/SecretSanta repository. How did I build it?

Here are the requirements I decided on:

  • The app must create a Secret Santa event.
  • The app must give links to the person you ought to prepare a present for.
  • The app must secure the links in a way that other people cannot access your target.
  • The app must use a well-known social platform for authorizing viewing of the links.

Table of Contents

  1. Telegram Auth via JS Interop
  2. Secret Santa Event
  3. Sharing the event with friends
  4. Telegram Bot
  5. Ending
  6. Picture credits

Telegram Auth via JS Interop

Telegram Login screenshot
Telegram Login

Here, in Ukraine, a lot of people use Telegram for communication. Everyone who may play the game this year has an account. That's the perfect auth system for my project!

Here are the steps we are going to do.

  1. Get the functioning Telegram login button from docs.
  2. Implement interop between .NET and JS.
  3. Hook up interop to the Telegram login button.

The button is generated by script from Telegram docs:

html
<script id="login" suppress-error="BL9992" src="https://telegram.org/js/telegram-widget.js?14" data-telegram-login="secret_santa_presents_bot" data-size="large" data-onauth="onTelegramAuth(user)"></script>

I need to get this data into my Blazor Server app. For this, let's use JS interop. We will pass the user we received to our c# code via references(refs) that we should store. Firstly, let's setup a global function that manages refs and puts them into DOM.

js
window.refs = {};
function setupDotnetRef(dotnetRef, id) {
    window.refs[id] = dotnetRef;
}

Then, we need to pass our specific ref for handling login through the JSRuntime

c#
@inject IJSRuntime Js
...
@code {
...
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    _telegramLoginInterop = new TelegramLoginInterop(Login);
    await Js.InvokeVoidAsync("setupDotnetRef", DotNetObjectReference.Create(_telegramLoginInterop), _jsUpdateLoginRefId);

    await base.OnAfterRenderAsync(firstRender);
}
...
}

The TelegramLoginInterop is a simple class that passes data around.

c#
public class TelegramLoginInterop
{
    private readonly Func<JsonElement, Task> _action;

    public TelegramLoginInterop(Func<JsonElement, Task> action)
    {
        _action = action;
    }
    
    [JSInvokable("LoginWithTelegram")]
    public async Task LoginWithTelegram(JsonDocument receivedTelegramInfo)
    {
        await _action.Invoke(receivedTelegramInfo.RootElement);
    }
}

Now that we have our refs, let's hook up our telegram callback function

js
window.onTelegramAuth = (user) => {
    var ref = window.refs.updateTelegramLogin
    ref.invokeMethodAsync('LoginWithTelegram', user);
...
}

By invoking the ref method LoginWithTelegram, we pass the user we received from Telegram into a c# method that invokes the login initialization logic

c#
private async Task Login(JsonElement receivedTelegramInfo)
{
    var isValid = BotWrapper.IsValidPayload(receivedTelegramInfo);
    if (!isValid)
    {
        await Js.InvokeVoidAsync("alert", "Telegram payload is not valid");
        return;
    }

    var id = receivedTelegramInfo.GetProperty("id").GetInt64();
    var login = receivedTelegramInfo.GetProperty("username").GetString();
    await LoadEvents(login);
    Auth.UpdateLogin(new TelegramId(id), new TelegramLogin(login));
}

Security for my family app is not of the utmost importance, but we still need to validate the telegram payload in the BotWrapper.IsValidPayload method. For that, we hash all the fields of the payloads with the Telegram bot secret key and compare it with the hash field of the payload. .NET provides SHA256 implementation in the BCL, so verification was pretty straightforward.

That's the gist of using Telegram auth with Blazor Server. Feel free to check out the whole component here.

Secret Santa Event

Let's design our event.

  • The Event should contain a unique identifier that allows us to find the event we need.
  • The Event has a name.
  • An Event is created from Members and a name.
  • These Members should take part in that event.
  • These Members are uniquely identified by a telegram login.
  • Each Member has a name (or nickname).
  • Each Member has an opponent (to who they should give a gift) that is also uniquely identified by a telegram login.

From this, we have our model:

c#
public class SecretSantaEvent
{
    IEnumerable<SecretSantaMember> TelegramUsers { get; }
    
    string Uid { get; }
    
    string Name { get; }
    
    SecretSantaMember? GetOpponentFor(string? username) => throw null;
    
    static SecretSantaEvent Create(string name, IList<SecretSantaMember> members) => throw null;
}

record SecretSantaMember(string Name, string TelegramLogin);

Now we can create a page for event construction.

New Event page screenshot
New Event page

Sharing the event with friends

Our requirements state that we should be able to easily share the event with friends and they will see their opponent. For that, we use the Uid of the event and Telegram Auth.

Firstly, we generate a link for the event and show it on the event page.

Santa link sharing screenshot
Santa link sharing

Then, based on the telegram username we can easily determine who is the opponent based on the value the return value of SecretSantaEvent.GetOpponentFor method.

Santa Opponent screenshot
Santa Opponent

All we need to do is post this one link to the chat with all participants, and everyone will see their opponent only when they click on it.

Telegram Bot

Sometimes things do not go smoothly. Some of my family members could not open the page on some browser, and I wanted to fix that as soon as possible. The easiest way for me to make that person happy was to provide them with another way to get opponent information. I could use this by adding a chatbot, utilising the same model that I use for my Blazor Server app.

For that, I added a small method that consumed commands via text and presented the response information via text.

c#
private void OnNewTelegramMessage(object sender, MessageEventArgs args)
{
    var chatId = new ChatId(args.Message.Chat.Id);
    if (args.Message.Text == "/start")
    {
        _bot.SendTextMessageAsync(chatId,
            "Write /whoamisantafor to find who to buy presents for!").GetAwaiter().GetResult();
    }

    if (args.Message.Text == "/whoamisantafor")
    {
        var events = _persistence.GetEventsFor(args.Message.From.Username).GetAwaiter().GetResult();

        var sb = new StringBuilder();

        foreach (var santaEvent in events)
        {
            var opponent = santaEvent.GetOpponentFor(args.Message.From.Username);
            sb.AppendLine(
                $"For event '{santaEvent.Name}', buy a present for {opponent.Name} (@{opponent.TelegramLogin})!");
        }

        if (events.Length == 0)
        {
            sb.AppendLine("Sorry, I do not know any events for you.");
        }
        
        _bot.SendTextMessageAsync(chatId, sb.ToString()).GetAwaiter().GetResult();
    }
}
Telegram santa screenshot
Telegram santa

That solved the problem in an hour, and everybody remained happy.

Ending

What I got was a fully functioning Secret Santa app which was built in a weekend. Our family and friends used this app for our little game, leaving everybody happy. One thing that I learned from this experience is that helping people having fun makes a great way to utilise our professional skills.

Picture credits