Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
Felk committed Jul 30, 2024
1 parent 16a447b commit 60a95a0
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 85 deletions.
3 changes: 2 additions & 1 deletion TPP.Core/Chat/TwitchChat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ public TwitchChat(
chatConfig.RefreshToken,
chatConfig.AppClientId,
chatConfig.AppClientSecret);
_twitchChatSender = new TwitchChatSender(loggerFactory, TwitchApi, chatConfig, useTwitchReplies);
TwitchEventSubChat = new TwitchEventSubChat(loggerFactory, clock, TwitchApi, userRepo,
subscriptionProcessor, overlayConnection, _twitchChatSender,
chatConfig.ChannelId, chatConfig.UserId,
chatConfig.CoStreamInputsEnabled, chatConfig.CoStreamInputsOnlyLive, coStreamChannelsRepo);

TwitchEventSubChat.IncomingMessage += MessageReceived;
_twitchChatSender = new TwitchChatSender(loggerFactory, TwitchApi, chatConfig, useTwitchReplies);
_twitchChatModeChanger = new TwitchChatModeChanger(
loggerFactory.CreateLogger<TwitchChatModeChanger>(), TwitchApi, chatConfig);
_twitchChatExecutor = new TwitchChatExecutor(loggerFactory.CreateLogger<TwitchChatExecutor>(),
Expand Down
164 changes: 157 additions & 7 deletions TPP.Core/Chat/TwitchEventSubChat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using NodaTime;
using TPP.Common;
using TPP.Common.Utils;
using TPP.Core.Overlay;
using TPP.Core.Overlay.Events;
using TPP.Core.Utils;
using TPP.Persistence;
using TPP.Twitch.EventSub;
Expand All @@ -30,6 +32,9 @@ public partial class TwitchEventSubChat : IWithLifecycle, IMessageSource
private readonly TwitchApi _twitchApi;
private readonly IClock _clock;
private readonly IUserRepo _userRepo;
private readonly ISubscriptionProcessor _subscriptionProcessor;
private readonly OverlayConnection _overlayConnection;
private readonly IMessageSender _responseSender;

private readonly bool _coStreamInputsEnabled;
private readonly bool _coStreamInputsOnlyLive;
Expand All @@ -49,6 +54,9 @@ public TwitchEventSubChat(
IClock clock,
TwitchApi twitchApi,
IUserRepo userRepo,
ISubscriptionProcessor subscriptionProcessor,
OverlayConnection overlayConnection,
IMessageSender responseSender,
string channelId,
string userId,
bool coStreamInputsEnabled,
Expand All @@ -59,13 +67,16 @@ public TwitchEventSubChat(
_twitchApi = twitchApi;
_clock = clock;
_userRepo = userRepo;
_subscriptionProcessor = subscriptionProcessor;
_overlayConnection = overlayConnection;
_responseSender = responseSender;
_channelId = channelId;
_userId = userId;
_coStreamInputsEnabled = coStreamInputsEnabled;
_coStreamInputsOnlyLive = coStreamInputsOnlyLive;
_coStreamChannelsRepo = coStreamChannelsRepo;

_client = new EventSubClient(loggerFactory, clock);
_client = new EventSubClient(loggerFactory, clock, url: "ws://127.0.0.1:8080/ws");
_client.RevocationReceived += (_, revocation) =>
_logger.LogError("received revocation for {SubscriptionType}: {Data}",
revocation.Metadata.SubscriptionType,
Expand Down Expand Up @@ -418,21 +429,160 @@ private async Task WhisperReceived(UserWhisperMessage whisperMessage)
IncomingMessage?.Invoke(this, new MessageEventArgs(message));
}

private static SubscriptionTier ParseTier(ChannelSubscribe.Tier tier) =>
tier switch
{
ChannelSubscribe.Tier.Tier1000 => SubscriptionTier.Tier1,
ChannelSubscribe.Tier.Tier2000 => SubscriptionTier.Tier2,
ChannelSubscribe.Tier.Tier3000 => SubscriptionTier.Tier3,
};

private async Task ChannelSubscribeReceived(ChannelSubscribe channelSubscribe)
{
// TODO handle
await Task.CompletedTask;
ChannelSubscribe.Event evt = channelSubscribe.Payload.Event;
User subscriber = await _userRepo.RecordUser(new UserInfo(
Id: evt.UserId,
TwitchDisplayName: evt.UserName,
SimpleName: evt.UserLogin,
UpdatedAt: channelSubscribe.Metadata.MessageTimestamp
));
// If a user subscribes, then cancels, and later subscribes again, I believe we get this message.
// This event does not tell us the cumulative months, so let's just guess using our previous knowledge.
int cumulativeMonths = subscriber.MonthsSubscribed + 1;
SubscriptionInfo subscriptionInfo = new(
subscriber,
cumulativeMonths,
null, // this is a new subscription, hence no streak
ParseTier(evt.Tier),
null, // EventSub does not give us the informational plan name (like "Channel Subscription: $24.99 Sub")
channelSubscribe.Metadata.MessageTimestamp,
null,
[]
);
ISubscriptionProcessor.SubResult subResult = await _subscriptionProcessor
.ProcessSubscription(subscriptionInfo);

string response = BuildSubResponse(subResult, null, false);
await _responseSender.SendWhisper(subscriptionInfo.Subscriber, response);
await _overlayConnection.Send(new NewSubscriber
{
User = subscriptionInfo.Subscriber,
Emotes = subscriptionInfo.Emotes.Select(EmoteInfo.FromOccurence).ToImmutableList(),
SubMessage = subscriptionInfo.Message,
ShareSub = true,
}, CancellationToken.None);
}

private async Task ChannelSubscriptionMessageReceived(ChannelSubscriptionMessage channelSubscriptionMessage)
{
// TODO handle
await Task.CompletedTask;
ChannelSubscriptionMessage.Event evt = channelSubscriptionMessage.Payload.Event;
User subscriber = await _userRepo.RecordUser(new UserInfo(
Id: evt.UserId,
TwitchDisplayName: evt.UserName,
SimpleName: evt.UserLogin,
UpdatedAt: channelSubscriptionMessage.Metadata.MessageTimestamp
));
SubscriptionInfo subscriptionInfo = new(
subscriber,
evt.CumulativeMonths,
evt.StreakMonths,
ParseTier(evt.Tier),
null, // EventSub does not give us the informational plan name (like "Channel Subscription: $24.99 Sub")
channelSubscriptionMessage.Metadata.MessageTimestamp,
evt.Message.Text,
evt.Message.Emotes.Select(e => new EmoteOccurrence(
e.Id, evt.Message.Text.Substring(e.Begin, e.End - e.Begin + 1), e.Begin, e.End))
.ToImmutableList()
);
ISubscriptionProcessor.SubResult subResult = await _subscriptionProcessor.ProcessSubscription(
subscriptionInfo);

string response = BuildSubResponse(subResult, null, false);
await _responseSender.SendWhisper(subscriptionInfo.Subscriber, response);

await _overlayConnection.Send(new NewSubscriber
{
User = subscriptionInfo.Subscriber,
Emotes = subscriptionInfo.Emotes.Select(EmoteInfo.FromOccurence).ToImmutableList(),
SubMessage = subscriptionInfo.Message,
ShareSub = true,
}, CancellationToken.None);
}

private async Task ChannelSubscriptionGiftReceived(ChannelSubscriptionGift channelSubscriptionGift)
{
// TODO handle
await Task.CompletedTask;
ChannelSubscriptionGift.Event evt = channelSubscriptionGift.Payload.Event;
User? gifter = evt.IsAnonymous
? null
: await _userRepo.RecordUser(new UserInfo(
Id: evt.UserId!,
TwitchDisplayName: evt.UserName!,
SimpleName: evt.UserLogin!,
UpdatedAt: channelSubscriptionGift.Metadata.MessageTimestamp
));
SubscriptionGiftInfo subscriptionGiftInfo = new(
gifter,
ParseTier(evt.Tier),
evt.Total,
evt.IsAnonymous
);
ISubscriptionProcessor.SubGiftResult subGiftResult = await _subscriptionProcessor.ProcessSubscriptionGift(
subscriptionGiftInfo);

string subGiftResponse = subGiftResult switch
{
// ISubscriptionProcessor.SubGiftResult.LinkedAccount =>
// $"As you are linked to the account '{e.SubscriptionInfo.Subscriber.Name}' you have gifted to, " +
// "you have not received a token bonus. " +
// "The recipient account still gains the normal benefits however. Thanks for subscribing!",
ISubscriptionProcessor.SubGiftResult.SameMonth { Month: var month } =>
$"We detected that this gift sub may have been a repeated message for month {month}, " +
"and you have already received the appropriate tokens. " +
"If you believe this is in error, please contact a moderator so this can be corrected.",
ISubscriptionProcessor.SubGiftResult.Ok { GifterTokens: var tokens } =>
$"Thank you for your generosity! You received T{tokens} tokens for giving a gift " +
"subscription. The recipient has been notified and awarded their token benefits.",
_ => throw new ArgumentOutOfRangeException(nameof(subGiftResult))
};
if (!subscriptionGiftInfo.IsAnonymous)
await _responseSender.SendWhisper(subscriptionGiftInfo.Gifter!,
subGiftResponse); // don't respond to the "AnAnonymousGifter" user
}

private static string BuildSubResponse(
ISubscriptionProcessor.SubResult subResult, User? gifter, bool isAnonymous)
{
return subResult switch
{
ISubscriptionProcessor.SubResult.Ok ok => BuildOkMessage(ok, gifter, isAnonymous),
ISubscriptionProcessor.SubResult.SameMonth sameMonth =>
$"We detected that you've already announced your resub for month {sameMonth.Month}, " +
"and received the appropriate tokens. " +
"If you believe this is in error, please contact a moderator so this can be corrected.",
_ => throw new ArgumentOutOfRangeException(nameof(subResult)),
};

static string BuildOkMessage(ISubscriptionProcessor.SubResult.Ok ok, User? gifter, bool isAnonymous)
{
string message = "";
if (ok.SubCountCorrected)
message +=
$"We detected that the amount of months subscribed ({ok.CumulativeMonths}) is lower than " +
"our system expected. This happened due to erroneously detected subscriptions in the past. " +
"Your account data has been adjusted accordingly, and you will receive your rewards normally. ";
if (ok.NewLoyaltyLeague > ok.OldLoyaltyLeague)
message += $"You reached Loyalty League {ok.NewLoyaltyLeague}! ";
if (ok.DeltaTokens > 0)
message += $"You gained T{ok.DeltaTokens} tokens! ";
if (gifter != null && isAnonymous)
message += "An anonymous user gifted you a subscription!";
else if (gifter != null && !isAnonymous)
message += $"{gifter.Name} gifted you a subscription!";
else if (ok.CumulativeMonths > 1)
message += "Thank you for resubscribing!";
else
message += "Thank you for subscribing!";
return message;
}
}
}
46 changes: 24 additions & 22 deletions TPP.Core/Subscriptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ public record SubscriptionInfo(

/// Information on a user subscribing through a gifted subscription.
public record SubscriptionGiftInfo(
SubscriptionInfo SubscriptionInfo, User Gifter, int NumGiftedMonths, bool IsAnonymous);
User? Gifter,
SubscriptionTier Tier,
int NumGiftedMonths,
bool IsAnonymous);

public interface ISubscriptionProcessor
{
Expand All @@ -48,6 +51,7 @@ public sealed record Ok(
int NewLoyaltyLeague,
int CumulativeMonths,
bool SubCountCorrected) : SubResult;

public sealed record SameMonth(int Month) : SubResult;
}

Expand All @@ -62,13 +66,13 @@ private SubGiftResult()
/// The gifter successfully received a token reward
public sealed record Ok(int GifterTokens) : SubGiftResult;
/// The gifter is linked to the gift recipient and has not received a token reward
public sealed record LinkedAccount : SubGiftResult;
// public sealed record LinkedAccount : SubGiftResult;
/// The subscription was deemed a duplicate and the gifter has not received a token reward
public sealed record SameMonth(int Month) : SubGiftResult;
}

Task<SubResult> ProcessSubscription(SubscriptionInfo subscriptionInfo);
Task<(SubResult, SubGiftResult)> ProcessSubscriptionGift(SubscriptionGiftInfo subscriptionGiftInfo);
Task<SubGiftResult> ProcessSubscriptionGift(SubscriptionGiftInfo subscriptionGiftInfo);
}

public class SubscriptionProcessor : ISubscriptionProcessor
Expand Down Expand Up @@ -186,31 +190,29 @@ await _tokenBank.PerformTransaction(new Transaction<User>(
SubCountCorrected: subCountCorrected);
}

public async Task<(ISubscriptionProcessor.SubResult, ISubscriptionProcessor.SubGiftResult)> ProcessSubscriptionGift(
public async Task<ISubscriptionProcessor.SubGiftResult> ProcessSubscriptionGift(
SubscriptionGiftInfo subscriptionGiftInfo)
{
SubscriptionInfo subscriptionInfo = subscriptionGiftInfo.SubscriptionInfo;
ISubscriptionProcessor.SubResult subResult =
await ProcessSubscription(subscriptionInfo);
bool isLinkedAccount = await _linkedAccountRepo.AreLinked(
subscriptionGiftInfo.Gifter.Id,
subscriptionInfo.Subscriber.Id);

if (isLinkedAccount)
return (subResult, new ISubscriptionProcessor.SubGiftResult.LinkedAccount());
// bool isLinkedAccount = subscriptionGiftInfo.Gifter != null && await _linkedAccountRepo.AreLinked(
// subscriptionGiftInfo.Gifter.Id,
// subscriptionInfo.Subscriber.Id);

if (subResult is ISubscriptionProcessor.SubResult.SameMonth { Month: var month })
return (subResult, new ISubscriptionProcessor.SubGiftResult.SameMonth(month));
// if (isLinkedAccount)
// return new ISubscriptionProcessor.SubGiftResult.LinkedAccount();

const int tokensPerRank = 10;
int rewardTokens = subscriptionGiftInfo.NumGiftedMonths * subscriptionInfo.Tier.ToRank() * tokensPerRank;
await _tokenBank.PerformTransaction(new Transaction<User>(
subscriptionGiftInfo.Gifter,
rewardTokens,
TransactionType.SubscriptionGift
));
int rewardTokens = subscriptionGiftInfo.NumGiftedMonths * subscriptionGiftInfo.Tier.ToRank() *
tokensPerRank;
if (subscriptionGiftInfo.Gifter != null)
{
await _tokenBank.PerformTransaction(new Transaction<User>(
subscriptionGiftInfo.Gifter,
rewardTokens,
TransactionType.SubscriptionGift
));
}

return (subResult, new ISubscriptionProcessor.SubGiftResult.Ok(rewardTokens));
return new ISubscriptionProcessor.SubGiftResult.Ok(rewardTokens);
}
}
}
57 changes: 2 additions & 55 deletions tests/TPP.Core.Tests/SubscriptionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,8 @@ public async Task handle_sub_gift_and_reward_gift_tokens()
userRepoMock.SetSubscriptionInfo(recipient, Arg.Any<int>(), Arg.Any<SubscriptionTier>(),
Arg.Any<int>(), Arg.Any<Instant>()).Returns(recipient);

SubscriptionInfo subscriptionInfo = new(recipient, 1, 0, SubscriptionTier.Tier3, "Sub Plan Name",
Instant.MinValue, "sub message", ImmutableList<EmoteOccurrence>.Empty);
(ISubscriptionProcessor.SubResult subResult, ISubscriptionProcessor.SubGiftResult subGiftResult) =
await subscriptionProcessor.ProcessSubscriptionGift(
new SubscriptionGiftInfo(subscriptionInfo, gifter, 2, false));
ISubscriptionProcessor.SubGiftResult subGiftResult = await subscriptionProcessor.ProcessSubscriptionGift(
new SubscriptionGiftInfo(gifter, SubscriptionTier.Tier3, 2, false));

const int expectedGiftTokens = 10 * 5 * 2; // 10 per rank. Tier 3 has rank 5 because $25 = 5 * $5, 2 months
Assert.That(subGiftResult, Is.InstanceOf<ISubscriptionProcessor.SubGiftResult.Ok>());
Expand All @@ -202,56 +199,6 @@ await subscriptionProcessor.ProcessSubscriptionGift(
await bankMock.Received(1).PerformTransaction(
new Transaction<User>(gifter, expectedGiftTokens, "subscription gift", expectedGiftData),
CancellationToken.None);

const int expectedSubTokens = 10 + 12 + 14 + 16 + 18; // Tier 3 = 5 ranks with increasing loyalty league
Assert.That(subResult, Is.InstanceOf<ISubscriptionProcessor.SubResult.Ok>());
var okResult = (ISubscriptionProcessor.SubResult.Ok)subResult;
Assert.That(okResult.CumulativeMonths, Is.EqualTo(1));
Assert.That(okResult.DeltaTokens, Is.EqualTo(expectedSubTokens));
Assert.That(okResult.OldLoyaltyLeague, Is.EqualTo(0));
Assert.That(okResult.NewLoyaltyLeague, Is.EqualTo(5));
Assert.That(okResult.SubCountCorrected, Is.False);
IDictionary<string, object?> expectedSubData = new Dictionary<string, object?>
{
["previous_months_subscribed"] = 0,
["new_months_subscribed"] = 1,
["months_difference"] = 1,
["previous_loyalty_tier"] = 0,
["new_loyalty_tier"] = 5,
["loyalty_completions"] = 5,
};
await bankMock.Received(1).PerformTransaction(
new Transaction<User>(recipient, expectedSubTokens, "subscription", expectedSubData),
CancellationToken.None);
}

[Test]
public async Task ignore_duplicate_month_for_sub_gift()
{
User gifter = MockUser("gifter", monthsSubscribed: 2, SubscriptionTier.Prime, loyaltyLeague: 2);
const SubscriptionTier tier = SubscriptionTier.Tier3;
User recipient = MockUser("recipient", monthsSubscribed: 1, subscriptionTier: tier, loyaltyLeague: 5);
var bankMock = Substitute.For<IBank<User>>();
var userRepoMock = Substitute.For<IUserRepo>();
ISubscriptionProcessor subscriptionProcessor = new SubscriptionProcessor(
NullLogger<SubscriptionProcessor>.Instance,
bankMock, userRepoMock, Substitute.For<ISubscriptionLogRepo>(), Substitute.For<ILinkedAccountRepo>());

SubscriptionInfo subscriptionInfo = new(recipient, NumMonths: 1, StreakMonths: 0, tier, "Sub Plan Name",
Instant.MinValue, "sub message", ImmutableList<EmoteOccurrence>.Empty);
(ISubscriptionProcessor.SubResult subResult, ISubscriptionProcessor.SubGiftResult subGiftResult) =
await subscriptionProcessor.ProcessSubscriptionGift(
new SubscriptionGiftInfo(subscriptionInfo, gifter, 1, false));

Assert.That(subGiftResult, Is.InstanceOf<ISubscriptionProcessor.SubGiftResult.SameMonth>());
var sameMonthGiftResult = (ISubscriptionProcessor.SubGiftResult.SameMonth)subGiftResult;
Assert.That(sameMonthGiftResult.Month, Is.EqualTo(1));
Assert.That(bankMock.ReceivedCalls().Count(), Is.EqualTo(0));

Assert.That(subResult, Is.InstanceOf<ISubscriptionProcessor.SubResult.SameMonth>());
var sameMonthSubResult = (ISubscriptionProcessor.SubResult.SameMonth)subResult;
Assert.That(sameMonthSubResult.Month, Is.EqualTo(1));
Assert.That(bankMock.ReceivedCalls().Count(), Is.EqualTo(0));
}

[Test]
Expand Down

0 comments on commit 60a95a0

Please sign in to comment.