re-adding/reworking voting capabilities
This commit is contained in:
parent
69a02e0e15
commit
9fe75d930f
24 changed files with 400 additions and 361 deletions
src
EllieBot.VotesApi
Common
Controllers
EllieBot.VotesApi.csprojProtos
Services
Startup.csappsettings.jsonEllieBot
|
@ -14,6 +14,7 @@ namespace EllieBot.VotesApi
|
|||
public const string SchemeName = "AUTHORIZATION_SCHEME";
|
||||
public const string DiscordsClaim = "DISCORDS_CLAIM";
|
||||
public const string TopggClaim = "TOPGG_CLAIM";
|
||||
public const string DiscordbotlistClaim = "DISCORDBOTLIST_CLAIM";
|
||||
|
||||
private readonly IConfiguration _conf;
|
||||
|
||||
|
@ -33,6 +34,9 @@ namespace EllieBot.VotesApi
|
|||
|
||||
if (_conf[ConfKeys.TOPGG_KEY] == Request.Headers["Authorization"].ToString().Trim())
|
||||
claims.Add(new Claim(TopggClaim, "true"));
|
||||
|
||||
if (_conf[ConfKeys.DISCORDBOTLIST_KEY].Trim() == Request.Headers["Authorization"].ToString().Trim())
|
||||
claims.Add(new Claim(DiscordbotlistClaim, "true"));
|
||||
|
||||
return Task.FromResult(AuthenticateResult.Success(new(new(new ClaimsIdentity(claims)), SchemeName)));
|
||||
}
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
{
|
||||
public const string DISCORDS_KEY = "DiscordsKey";
|
||||
public const string TOPGG_KEY = "TopGGKey";
|
||||
public const string DISCORDBOTLIST_KEY = "DiscordbotListKey";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
namespace EllieBot.VotesApi
|
||||
{
|
||||
public class DiscordbotlistVoteWebhookModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The avatar hash of the user
|
||||
/// </summary>
|
||||
public string Avatar { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The username of the user who voted
|
||||
/// </summary>
|
||||
public string Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the user who voted
|
||||
/// </summary>
|
||||
public string Id { get; set; }
|
||||
}
|
||||
}
|
|
@ -12,12 +12,6 @@
|
|||
/// </summary>
|
||||
public string Bot { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains totalVotes, votesMonth, votes24, hasVoted - a list of IDs of users who have voted this month, and
|
||||
/// Voted24 - a list of IDs of users who have voted today
|
||||
/// </summary>
|
||||
public string Votes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of event, whether it is a vote event or test event
|
||||
/// </summary>
|
|
@ -4,5 +4,6 @@
|
|||
{
|
||||
public const string DiscordsAuth = "DiscordsAuth";
|
||||
public const string TopggAuth = "TopggAuth";
|
||||
public const string DiscordbotlistAuth = "DiscordbotlistAuth";
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class DiscordsController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<DiscordsController> _logger;
|
||||
private readonly IVotesCache _cache;
|
||||
|
||||
public DiscordsController(ILogger<DiscordsController> logger, IVotesCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
[Authorize(Policy = Policies.DiscordsAuth)]
|
||||
public async Task<IEnumerable<Vote>> New()
|
||||
{
|
||||
var votes = await _cache.GetNewDiscordsVotesAsync();
|
||||
if (votes.Count > 0)
|
||||
_logger.LogInformation("Sending {NewDiscordsVotes} new discords votes", votes.Count);
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class TopGgController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<TopGgController> _logger;
|
||||
private readonly IVotesCache _cache;
|
||||
|
||||
public TopGgController(ILogger<TopGgController> logger, IVotesCache cache)
|
||||
{
|
||||
_logger = logger;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet("new")]
|
||||
[Authorize(Policy = Policies.TopggAuth)]
|
||||
public async Task<IEnumerable<Vote>> New()
|
||||
{
|
||||
var votes = await _cache.GetNewTopGgVotesAsync();
|
||||
if (votes.Count > 0)
|
||||
_logger.LogInformation("Sending {NewTopggVotes} new topgg votes", votes.Count);
|
||||
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,33 +2,32 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using EllieBot.VotesApi.Services;
|
||||
using EllieBot.GrpcVotesApi;
|
||||
|
||||
namespace EllieBot.VotesApi.Controllers
|
||||
{
|
||||
[ApiController]
|
||||
public class WebhookController : ControllerBase
|
||||
public class WebhookController(ILogger<WebhookController> logger, VoteService.VoteServiceClient client)
|
||||
: ControllerBase
|
||||
{
|
||||
private readonly ILogger<WebhookController> _logger;
|
||||
private readonly IVotesCache _votesCache;
|
||||
|
||||
public WebhookController(ILogger<WebhookController> logger, IVotesCache votesCache)
|
||||
{
|
||||
_logger = logger;
|
||||
_votesCache = votesCache;
|
||||
}
|
||||
|
||||
[HttpPost("/discordswebhook")]
|
||||
[Authorize(Policy = Policies.DiscordsAuth)]
|
||||
public async Task<IActionResult> DiscordsWebhook([FromBody] DiscordsVoteWebhookModel data)
|
||||
{
|
||||
|
||||
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
if (data.Type != "vote")
|
||||
return Ok();
|
||||
|
||||
logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
data.User,
|
||||
data.Bot,
|
||||
"discords.com");
|
||||
|
||||
await _votesCache.AddNewDiscordsVote(data.User);
|
||||
await client.VoteReceivedAsync(new GrpcVoteData()
|
||||
{
|
||||
Type = VoteType.Discords,
|
||||
UserId = data.User,
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
|
@ -36,12 +35,34 @@ namespace EllieBot.VotesApi.Controllers
|
|||
[Authorize(Policy = Policies.TopggAuth)]
|
||||
public async Task<IActionResult> TopggWebhook([FromBody] TopggVoteWebhookModel data)
|
||||
{
|
||||
_logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
logger.LogInformation("User {UserId} has voted for Bot {BotId} on {Platform}",
|
||||
data.User,
|
||||
data.Bot,
|
||||
"top.gg");
|
||||
|
||||
await _votesCache.AddNewTopggVote(data.User);
|
||||
await client.VoteReceivedAsync(new GrpcVoteData()
|
||||
{
|
||||
Type = VoteType.Topgg,
|
||||
UserId = data.User,
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("/discordbotlistwebhook")]
|
||||
[Authorize(Policy = Policies.DiscordbotlistAuth)]
|
||||
public async Task<IActionResult> DiscordbotlistWebhook([FromBody] DiscordbotlistVoteWebhookModel data)
|
||||
{
|
||||
logger.LogInformation("User {UserId} has voted for Bot on {Platform}",
|
||||
data.Id,
|
||||
"discordbotlist.com");
|
||||
|
||||
await client.VoteReceivedAsync(new GrpcVoteData()
|
||||
{
|
||||
Type = VoteType.Discordbotlist,
|
||||
UserId = data.Id,
|
||||
});
|
||||
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
<ItemGroup>
|
||||
<PackageReference Include="MorseCode.ITask" Version="2.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
|
||||
<PackageReference Include="Grpc.AspNetCore" Version="2.70.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Protobuf Include="Protos/vote.proto" GrpcServices="Client" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
24
src/EllieBot.VotesApi/Protos/vote.proto
Normal file
24
src/EllieBot.VotesApi/Protos/vote.proto
Normal file
|
@ -0,0 +1,24 @@
|
|||
syntax = "proto3";
|
||||
|
||||
package vote;
|
||||
|
||||
option csharp_namespace = "EllieBot.GrpcVotesApi";
|
||||
|
||||
enum VoteType {
|
||||
TOPGG = 0;
|
||||
DISCORDBOTLIST = 1;
|
||||
DISCORDS = 2;
|
||||
}
|
||||
|
||||
message GrpcVoteData {
|
||||
string userId = 1;
|
||||
VoteType type = 2;
|
||||
}
|
||||
|
||||
message GrpcVoteResult {
|
||||
|
||||
}
|
||||
|
||||
service VoteService {
|
||||
rpc VoteReceived (GrpcVoteData) returns (GrpcVoteResult);
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace EllieBot.VotesApi.Services
|
||||
{
|
||||
public class FileVotesCache : IVotesCache
|
||||
{
|
||||
// private const string STATS_FILE = "store/stats.json";
|
||||
private const string TOPGG_FILE = "store/topgg.json";
|
||||
private const string DISCORDS_FILE = "store/discords.json";
|
||||
|
||||
private readonly SemaphoreSlim _locker = new SemaphoreSlim(1, 1);
|
||||
|
||||
public FileVotesCache()
|
||||
{
|
||||
if (!Directory.Exists("store"))
|
||||
Directory.CreateDirectory("store");
|
||||
|
||||
if (!File.Exists(TOPGG_FILE))
|
||||
File.WriteAllText(TOPGG_FILE, "[]");
|
||||
|
||||
if (!File.Exists(DISCORDS_FILE))
|
||||
File.WriteAllText(DISCORDS_FILE, "[]");
|
||||
}
|
||||
|
||||
public ITask AddNewTopggVote(string userId)
|
||||
=> AddNewVote(TOPGG_FILE, userId);
|
||||
|
||||
public ITask AddNewDiscordsVote(string userId)
|
||||
=> AddNewVote(DISCORDS_FILE, userId);
|
||||
|
||||
private async ITask AddNewVote(string file, string userId)
|
||||
{
|
||||
await _locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
var votes = await GetVotesAsync(file);
|
||||
votes.Add(userId);
|
||||
await File.WriteAllTextAsync(file, JsonSerializer.Serialize(votes));
|
||||
}
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async ITask<IList<Vote>> GetNewTopGgVotesAsync()
|
||||
{
|
||||
var votes = await EvictTopggVotes();
|
||||
return votes;
|
||||
}
|
||||
|
||||
public async ITask<IList<Vote>> GetNewDiscordsVotesAsync()
|
||||
{
|
||||
var votes = await EvictDiscordsVotes();
|
||||
return votes;
|
||||
}
|
||||
|
||||
private ITask<List<Vote>> EvictTopggVotes()
|
||||
=> EvictVotes(TOPGG_FILE);
|
||||
|
||||
private ITask<List<Vote>> EvictDiscordsVotes()
|
||||
=> EvictVotes(DISCORDS_FILE);
|
||||
|
||||
private async ITask<List<Vote>> EvictVotes(string file)
|
||||
{
|
||||
await _locker.WaitAsync();
|
||||
try
|
||||
{
|
||||
|
||||
var ids = await GetVotesAsync(file);
|
||||
await File.WriteAllTextAsync(file, "[]");
|
||||
|
||||
return ids?
|
||||
.Select(x => (Ok: ulong.TryParse(x, out var r), Id: r))
|
||||
.Where(x => x.Ok)
|
||||
.Select(x => new Vote
|
||||
{
|
||||
UserId = x.Id
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async ITask<IList<string>> GetVotesAsync(string file)
|
||||
{
|
||||
await using var fs = File.Open(file, FileMode.Open);
|
||||
var votes = await JsonSerializer.DeserializeAsync<List<string>>(fs);
|
||||
return votes;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using MorseCode.ITask;
|
||||
|
||||
namespace EllieBot.VotesApi.Services
|
||||
{
|
||||
public interface IVotesCache
|
||||
{
|
||||
ITask<IList<Vote>> GetNewTopGgVotesAsync();
|
||||
ITask<IList<Vote>> GetNewDiscordsVotesAsync();
|
||||
ITask AddNewTopggVote(string userId);
|
||||
ITask AddNewDiscordsVote(string userId);
|
||||
}
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
using Microsoft.AspNetCore.Authorization;
|
||||
using System;
|
||||
using Grpc.Core;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using EllieBot.VotesApi.Services;
|
||||
using EllieBot.GrpcVotesApi;
|
||||
|
||||
namespace EllieBot.VotesApi
|
||||
{
|
||||
|
@ -21,11 +22,17 @@ namespace EllieBot.VotesApi
|
|||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
services.AddControllers();
|
||||
services.AddSingleton<IVotesCache, FileVotesCache>();
|
||||
services.AddSwaggerGen(static c =>
|
||||
{
|
||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "EllieBot.VotesApi", Version = "v1" });
|
||||
});
|
||||
|
||||
services.AddGrpcClient<VoteService.VoteServiceClient>(options =>
|
||||
{
|
||||
var grpcServiceUrl = Configuration["GrpcServiceUrl"]!;
|
||||
options.Address = new Uri(grpcServiceUrl);
|
||||
})
|
||||
.ConfigureChannel((sp, c) =>
|
||||
{
|
||||
c.Credentials = ChannelCredentials.Insecure;
|
||||
c.ServiceProvider = sp;
|
||||
});
|
||||
|
||||
services
|
||||
.AddAuthentication(opts =>
|
||||
|
@ -40,8 +47,12 @@ namespace EllieBot.VotesApi
|
|||
opts.DefaultPolicy = new AuthorizationPolicyBuilder(AuthHandler.SchemeName)
|
||||
.RequireAssertion(static _ => false)
|
||||
.Build();
|
||||
opts.AddPolicy(Policies.DiscordsAuth, static policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
|
||||
opts.AddPolicy(Policies.TopggAuth, static policy => policy.RequireClaim(AuthHandler.TopggClaim));
|
||||
opts.AddPolicy(Policies.DiscordsAuth,
|
||||
static policy => policy.RequireClaim(AuthHandler.DiscordsClaim));
|
||||
opts.AddPolicy(Policies.TopggAuth,
|
||||
static policy => policy.RequireClaim(AuthHandler.TopggClaim));
|
||||
opts.AddPolicy(Policies.DiscordbotlistAuth,
|
||||
static policy => policy.RequireClaim(AuthHandler.DiscordbotlistClaim));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -51,8 +62,6 @@ namespace EllieBot.VotesApi
|
|||
if (env.IsDevelopment())
|
||||
{
|
||||
app.UseDeveloperExceptionPage();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI(static c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "EllieBot.VotesApi v1"));
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
},
|
||||
"DiscordsKey": "my_discords_key",
|
||||
"TopGGKey": "my_topgg_key",
|
||||
"DiscordBotListKey": "my_discordbotlist_key",
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
|
@ -124,6 +124,9 @@
|
|||
<Link>_common\CoordinatorProtos\coordinator.proto</Link>
|
||||
<GrpcServices>Client</GrpcServices>
|
||||
</Protobuf>
|
||||
<Protobuf Include="../EllieBot.VotesApi/Protos/*.proto">
|
||||
<GrpcServices>Server</GrpcServices>
|
||||
</Protobuf>
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'GlobalEllie' ">
|
||||
|
|
|
@ -35,6 +35,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
private readonly RakebackService _rb;
|
||||
private readonly IBotCache _cache;
|
||||
private readonly CaptchaService _captchaService;
|
||||
private readonly VoteRewardService _vrs;
|
||||
|
||||
public Gambling(
|
||||
IGamblingService gs,
|
||||
|
@ -50,7 +51,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
GamblingTxTracker gamblingTxTracker,
|
||||
RakebackService rb,
|
||||
IBotCache cache,
|
||||
CaptchaService captchaService)
|
||||
CaptchaService captchaService,
|
||||
VoteRewardService vrs)
|
||||
: base(configService)
|
||||
{
|
||||
_gs = gs;
|
||||
|
@ -65,6 +67,7 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
_captchaService = captchaService;
|
||||
_ps = patronage;
|
||||
_rng = new EllieRandom();
|
||||
_vrs = vrs;
|
||||
|
||||
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
|
||||
_enUsCulture.NumberDecimalDigits = 0;
|
||||
|
@ -131,6 +134,50 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
await ClaimTimely();
|
||||
});
|
||||
|
||||
[Cmd]
|
||||
public async Task Vote()
|
||||
{
|
||||
var reward = Config.VoteReward;
|
||||
if (reward <= 0)
|
||||
{
|
||||
if (Config.Timely.Amount > 0 && Config.Timely.Cooldown > 0)
|
||||
{
|
||||
await Timely();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (await _vrs.LastVoted(ctx.User.Id) is { } remainder)
|
||||
{
|
||||
// Get correct time form remainder
|
||||
var interaction = CreateRemindMeInteraction(remainder.TotalMilliseconds);
|
||||
|
||||
// Removes timely button if there is a timely reminder in DB
|
||||
if (_service.UserHasTimelyReminder(ctx.User.Id))
|
||||
{
|
||||
interaction = null;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var relativeTag = TimestampTag.FromDateTime(now.Add(remainder), TimestampTagStyles.Relative);
|
||||
await Response().Pending(strs.vote_already_claimed(relativeTag)).Interaction(interaction).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
var (amount, msg) = await _service.GetAmountAndMessage(ctx.User.Id, reward);
|
||||
|
||||
var prepend = GetText(strs.vote_suggest(N(amount)));
|
||||
msg = prepend + "\n\n" + msg;
|
||||
|
||||
var inter = CreateRemindMeInteraction(6);
|
||||
|
||||
await Response()
|
||||
.Confirm(msg)
|
||||
.Interaction(inter)
|
||||
.SendAsync();
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
public async Task Timely()
|
||||
{
|
||||
|
@ -138,10 +185,17 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
var period = Config.Timely.Cooldown;
|
||||
if (val <= 0 || period <= 0)
|
||||
{
|
||||
if (Config.VoteReward > 0)
|
||||
{
|
||||
await Vote();
|
||||
return;
|
||||
}
|
||||
|
||||
await Response().Error(strs.timely_none).SendAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
||||
if (Config.Timely.ProtType == TimelyProt.Button)
|
||||
{
|
||||
var interaction = CreateTimelyInteraction();
|
||||
|
@ -149,7 +203,8 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
await msg.DeleteAsync();
|
||||
return;
|
||||
}
|
||||
else if (Config.Timely.ProtType == TimelyProt.Captcha)
|
||||
|
||||
if (Config.Timely.ProtType == TimelyProt.Captcha)
|
||||
{
|
||||
var password = await _captchaService.GetUserCaptcha(ctx.User.Id);
|
||||
|
||||
|
@ -160,10 +215,10 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
var toSend = Response()
|
||||
.File(stream, "timely.png");
|
||||
|
||||
#if GLOBAL_ELLIE
|
||||
#if GLOBAL_NADEKO
|
||||
if (_rng.Next(0, 8) == 0)
|
||||
toSend = toSend
|
||||
.Text("*[Sub on Patreon](https://patreon.com/elliebot) to remove captcha.*");
|
||||
.Text("*[Sub on Patreon](https://patreon.com/nadekobot) to remove captcha.*");
|
||||
#endif
|
||||
|
||||
var captchaMessage = await toSend.SendAsync();
|
||||
|
@ -209,56 +264,19 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
|
||||
|
||||
var val = Config.Timely.Amount;
|
||||
var boostGuilds = Config.BoostBonus.GuildIds ?? new();
|
||||
var guildUsers = await boostGuilds
|
||||
.Select(async gid =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var guild = await _client.Rest.GetGuildAsync(gid, false);
|
||||
var user = await _client.Rest.GetGuildUserAsync(gid, ctx.User.Id);
|
||||
return (guild, user);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
})
|
||||
.WhenAll();
|
||||
|
||||
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
|
||||
var booster = userInfo != default;
|
||||
|
||||
if (booster)
|
||||
val += Config.BoostBonus.BaseTimelyBonus;
|
||||
|
||||
var patron = await _ps.GetPatronAsync(ctx.User.Id);
|
||||
|
||||
var percentBonus = (_ps.PercentBonus(patron) / 100f);
|
||||
|
||||
val += (int)(val * percentBonus);
|
||||
|
||||
var inter = CreateRemindMeInteraction(period);
|
||||
|
||||
await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
|
||||
var prepend = GetText(strs.timely(N(val), period));
|
||||
var (newVal, msg) = await _service.GetAmountAndMessage(ctx.User.Id, val);
|
||||
|
||||
var msg = GetText(strs.timely(N(val), period));
|
||||
if (booster || percentBonus > float.Epsilon)
|
||||
{
|
||||
msg += "\n\n";
|
||||
if (booster)
|
||||
msg += $"*+{N(Config.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n";
|
||||
msg = prepend + "\n\n" + msg;
|
||||
|
||||
if (percentBonus > float.Epsilon)
|
||||
msg +=
|
||||
$"*+{percentBonus:P0} bonus for the [Patreon](https://patreon.com/elliebot) pledge! <:hart:746995901758832712>*";
|
||||
await _cs.AddAsync(ctx.User.Id, newVal, new("timely", "claim"));
|
||||
|
||||
await Response().Confirm(msg).Interaction(inter).SendAsync();
|
||||
}
|
||||
else
|
||||
await Response().Confirm(strs.timely(N(val), period)).Interaction(inter).SendAsync();
|
||||
await Response().Confirm(msg).Interaction(inter).SendAsync();
|
||||
}
|
||||
|
||||
|
||||
[Cmd]
|
||||
[OwnerOnly]
|
||||
public async Task TimelyReset()
|
||||
|
@ -911,7 +929,6 @@ public partial class Gambling : GamblingModule<GamblingService>
|
|||
await Response().Embed(eb).SendAsync();
|
||||
}
|
||||
|
||||
|
||||
public enum GambleTestTarget
|
||||
{
|
||||
Slot,
|
||||
|
|
|
@ -63,6 +63,11 @@ public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
|
|||
This will work only if you've set up VotesApi and correct credentials for topgg and/or discords voting
|
||||
""")]
|
||||
public long VoteReward { get; set; } = 100;
|
||||
|
||||
[Comment("""
|
||||
Id of the channel to send a message to after a user votes
|
||||
""")]
|
||||
public ulong? VoteFeedChannelId { get; set; }
|
||||
|
||||
[Comment("""Slot config""")]
|
||||
public SlotsConfig Slots { get; set; }
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using EllieBot.Db.Models;
|
||||
using EllieBot.Modules.Gambling.Common;
|
||||
using EllieBot.Modules.Gambling.Common.Connect4;
|
||||
using EllieBot.Modules.Patronage;
|
||||
|
||||
namespace EllieBot.Modules.Gambling.Services;
|
||||
|
||||
|
@ -15,7 +17,8 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
private readonly DbService _db;
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCache _cache;
|
||||
private readonly GamblingConfigService _gss;
|
||||
private readonly GamblingConfigService _gcs;
|
||||
private readonly IPatronageService _ps;
|
||||
private readonly EllieRandom _rng;
|
||||
|
||||
private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay");
|
||||
|
@ -24,12 +27,14 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
DbService db,
|
||||
DiscordSocketClient client,
|
||||
IBotCache cache,
|
||||
GamblingConfigService gss)
|
||||
GamblingConfigService gcs,
|
||||
IPatronageService ps)
|
||||
{
|
||||
_db = db;
|
||||
_client = client;
|
||||
_cache = cache;
|
||||
_gss = gss;
|
||||
_gcs = gcs;
|
||||
_ps = ps;
|
||||
_rng = new EllieRandom();
|
||||
}
|
||||
|
||||
|
@ -53,7 +58,7 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
{
|
||||
try
|
||||
{
|
||||
var lifetime = _gss.Data.Currency.TransactionsLifetime;
|
||||
var lifetime = _gcs.Data.Currency.TransactionsLifetime;
|
||||
if (lifetime <= 0)
|
||||
continue;
|
||||
|
||||
|
@ -61,7 +66,7 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
var days = TimeSpan.FromDays(lifetime);
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.Set<CurrencyTransaction>()
|
||||
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
|
||||
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -82,7 +87,7 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
{
|
||||
try
|
||||
{
|
||||
var config = _gss.Data;
|
||||
var config = _gcs.Data;
|
||||
var maxDecay = config.Decay.MaxDecay;
|
||||
if (config.Decay.Percent is <= 0 or > 1 || maxDecay < 0)
|
||||
continue;
|
||||
|
@ -113,14 +118,14 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
|
||||
var decay = (double)config.Decay.Percent;
|
||||
await uow.Set<DiscordUser>()
|
||||
.Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id)
|
||||
.UpdateAsync(old => new()
|
||||
{
|
||||
CurrencyAmount =
|
||||
maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5)
|
||||
? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5))
|
||||
: old.CurrencyAmount - maxDecay
|
||||
});
|
||||
.Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id)
|
||||
.UpdateAsync(old => new()
|
||||
{
|
||||
CurrencyAmount =
|
||||
maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5)
|
||||
? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5))
|
||||
: old.CurrencyAmount - maxDecay
|
||||
});
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
|
@ -135,13 +140,14 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
}
|
||||
}
|
||||
|
||||
private static readonly TypedKey<EconomyResult> _ecoKey = new("ellie:economy");
|
||||
private static readonly TypedKey<EconomyResult> _ecoKey = new("nadeko:economy");
|
||||
|
||||
private static readonly SemaphoreSlim _timelyLock = new(1, 1);
|
||||
|
||||
private static TypedKey<Dictionary<ulong, long>> _timelyKey
|
||||
= new("timely:claims");
|
||||
|
||||
|
||||
public async Task<TimeSpan?> ClaimTimelyAsync(ulong userId, int period)
|
||||
{
|
||||
if (period == 0)
|
||||
|
@ -190,8 +196,63 @@ public class GamblingService : IEService, IReadyExecutor
|
|||
return db.GetTable<Reminder>()
|
||||
.Any(x => x.UserId == userId
|
||||
&& x.Type == ReminderType.Timely);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveAllTimelyClaimsAsync()
|
||||
=> await _cache.RemoveAsync(_timelyKey);
|
||||
|
||||
private string N(long amount)
|
||||
=> CurrencyHelper.N(amount, CultureInfo.InvariantCulture, _gcs.Data.Currency.Sign);
|
||||
|
||||
public async Task<(long val, string msg)> GetAmountAndMessage(ulong userId, long originalAmount)
|
||||
{
|
||||
var gcsData = _gcs.Data;
|
||||
var boostGuilds = gcsData.BoostBonus.GuildIds ?? [];
|
||||
var guildUsers = await boostGuilds
|
||||
.Select(async gid =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var guild = _client.GetGuild(gid) as IGuild ?? await _client.Rest.GetGuildAsync(gid, false);
|
||||
var user = await guild.GetUserAsync(gid) ?? await _client.Rest.GetGuildUserAsync(gid, userId);
|
||||
return (guild, user);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
})
|
||||
.WhenAll();
|
||||
|
||||
var userInfo = guildUsers.FirstOrDefault(x => x.user?.PremiumSince is not null);
|
||||
var booster = userInfo != default;
|
||||
|
||||
if (booster)
|
||||
originalAmount += gcsData.BoostBonus.BaseTimelyBonus;
|
||||
|
||||
var patron = await _ps.GetPatronAsync(userId);
|
||||
var percentBonus = (_ps.PercentBonus(patron) / 100f);
|
||||
|
||||
originalAmount += (int)(originalAmount * percentBonus);
|
||||
|
||||
var msg = $"{N(originalAmount)} base reward.\n";
|
||||
if (boostGuilds.Count > 0)
|
||||
{
|
||||
if (booster)
|
||||
msg += $"✅ *+{N(gcsData.BoostBonus.BaseTimelyBonus)} bonus for boosting {userInfo.guild}!*\n";
|
||||
else
|
||||
msg += $"❌ +0 bonus for boosting {userInfo.guild}.\n";
|
||||
}
|
||||
|
||||
if (_ps.GetConfig().IsEnabled)
|
||||
{
|
||||
if (percentBonus > float.Epsilon)
|
||||
msg +=
|
||||
$"✅ *+{percentBonus:P0} bonus for the [Patreon](https://patreon.com/nadekobot) pledge! <:hart:746995901758832712>*";
|
||||
else
|
||||
msg += $"❌ +0 bonus for the [Patreon](https://patreon.com/nadekobot) pledge.";
|
||||
}
|
||||
|
||||
return (originalAmount, msg);
|
||||
}
|
||||
}
|
|
@ -1,106 +1,128 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
using Grpc.Core;
|
||||
using EllieBot.Common.ModuleBehaviors;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using EllieBot.GrpcApi;
|
||||
using EllieBot.GrpcVotesApi;
|
||||
|
||||
namespace EllieBot.Modules.Gambling.Services;
|
||||
|
||||
public class VoteModel
|
||||
public class VoteRewardService(
|
||||
ShardData shardData,
|
||||
GamblingConfigService gcs,
|
||||
CurrencyService cs,
|
||||
IBotCache cache,
|
||||
DiscordSocketClient client,
|
||||
IMessageSenderService sender
|
||||
) : IEService, IReadyExecutor
|
||||
{
|
||||
[JsonPropertyName("userId")]
|
||||
public ulong UserId { get; set; }
|
||||
}
|
||||
private TypedKey<DateTime> VoteKey(ulong userId)
|
||||
=> new($"vote:{userId}");
|
||||
|
||||
public class VoteRewardService : IEService, IReadyExecutor
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly IBotCreds _creds;
|
||||
private readonly ICurrencyService _currencyService;
|
||||
private readonly GamblingConfigService _gamb;
|
||||
|
||||
public VoteRewardService(
|
||||
DiscordSocketClient client,
|
||||
IBotCreds creds,
|
||||
ICurrencyService currencyService,
|
||||
GamblingConfigService gamb)
|
||||
{
|
||||
_client = client;
|
||||
_creds = creds;
|
||||
_currencyService = currencyService;
|
||||
_gamb = gamb;
|
||||
}
|
||||
private Server? _app;
|
||||
private IMessageChannel? _voteFeedChannel;
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
if (_client.ShardId != 0)
|
||||
if (shardData.ShardId != 0)
|
||||
return;
|
||||
|
||||
using var http = new HttpClient(new HttpClientHandler
|
||||
var serverCreds = ServerCredentials.Insecure;
|
||||
var ssd = VoteService.BindService(new VotesGrpcService(this));
|
||||
|
||||
_app = new()
|
||||
{
|
||||
AllowAutoRedirect = false,
|
||||
ServerCertificateCustomValidationCallback = delegate { return true; }
|
||||
Ports =
|
||||
{
|
||||
new("127.0.0.1", 59384, serverCreds),
|
||||
}
|
||||
};
|
||||
|
||||
_app.Services.Add(ssd);
|
||||
_app.Start();
|
||||
|
||||
if (gcs.Data.VoteFeedChannelId is ulong cid)
|
||||
{
|
||||
_voteFeedChannel = await client.GetChannelAsync(cid) as IMessageChannel;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public void SetVoiceChannel(IMessageChannel? channel)
|
||||
{
|
||||
gcs.ModifyConfig(c => { c.VoteFeedChannelId = channel?.Id; });
|
||||
_voteFeedChannel = channel;
|
||||
}
|
||||
|
||||
public async Task UserVotedAsync(ulong userId, VoteType requestType)
|
||||
{
|
||||
var gcsData = gcs.Data;
|
||||
var reward = gcsData.VoteReward;
|
||||
if (reward <= 0)
|
||||
return;
|
||||
|
||||
var key = VoteKey(userId);
|
||||
if (!await cache.AddAsync(key, DateTime.UtcNow, expiry: TimeSpan.FromHours(6)))
|
||||
return;
|
||||
|
||||
await cs.AddAsync(userId, reward, new("vote", requestType.ToString()));
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = await client.GetUserAsync(userId);
|
||||
|
||||
await sender.Response(user)
|
||||
.Confirm(strs.vote_reward(N(reward)))
|
||||
.SendAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Unable to send vote confirmation message to user {UserId}", userId);
|
||||
}
|
||||
});
|
||||
|
||||
while (true)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(30000);
|
||||
|
||||
var topggKey = _creds.Votes?.TopggKey;
|
||||
var topggServiceUrl = _creds.Votes?.TopggServiceUrl;
|
||||
|
||||
try
|
||||
if (_voteFeedChannel is not null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(topggKey) && !string.IsNullOrWhiteSpace(topggServiceUrl))
|
||||
try
|
||||
{
|
||||
http.DefaultRequestHeaders.Authorization = new(topggKey);
|
||||
var uri = new Uri(new(topggServiceUrl), "topgg/new");
|
||||
var res = await http.GetStringAsync(uri);
|
||||
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
|
||||
|
||||
if (data is { Count: > 0 })
|
||||
{
|
||||
var ids = data.Select(x => x.UserId).ToList();
|
||||
|
||||
await _currencyService.AddBulkAsync(ids,
|
||||
_gamb.Data.VoteReward,
|
||||
new("vote", "top.gg", "top.gg vote reward"));
|
||||
|
||||
Log.Information("Rewarding {Count} top.gg voters", ids.Count());
|
||||
}
|
||||
var user = await client.GetUserAsync(userId);
|
||||
await _voteFeedChannel.SendMessageAsync(
|
||||
$"{user} just received {strs.vote_reward(N(reward))} for voting!",
|
||||
allowedMentions: AllowedMentions.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Unable to send vote reward message to user {UserId}", userId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Critical error loading top.gg vote rewards");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var discordsKey = _creds.Votes?.DiscordsKey;
|
||||
var discordsServiceUrl = _creds.Votes?.DiscordsServiceUrl;
|
||||
public async Task<TimeSpan?> LastVoted(ulong userId)
|
||||
{
|
||||
var key = VoteKey(userId);
|
||||
var last = await cache.GetAsync(key);
|
||||
return last.Match(
|
||||
static x => DateTime.UtcNow.Subtract(x),
|
||||
static _ => default(TimeSpan?));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(discordsKey) && !string.IsNullOrWhiteSpace(discordsServiceUrl))
|
||||
{
|
||||
http.DefaultRequestHeaders.Authorization = new(discordsKey);
|
||||
var res = await http.GetStringAsync(new Uri(new(discordsServiceUrl), "discords/new"));
|
||||
var data = JsonSerializer.Deserialize<List<VoteModel>>(res);
|
||||
private string N(long amount)
|
||||
=> CurrencyHelper.N(amount, CultureInfo.InvariantCulture, gcs.Data.Currency.Sign);
|
||||
}
|
||||
|
||||
if (data is { Count: > 0 })
|
||||
{
|
||||
var ids = data.Select(x => x.UserId).ToList();
|
||||
public sealed class VotesGrpcService(VoteRewardService vrs)
|
||||
: VoteService.VoteServiceBase, IEService
|
||||
{
|
||||
[GrpcNoAuthRequired]
|
||||
public override async Task<GrpcVoteResult> VoteReceived(GrpcVoteData request, ServerCallContext context)
|
||||
{
|
||||
await vrs.UserVotedAsync(ulong.Parse(request.UserId), request.Type);
|
||||
|
||||
await _currencyService.AddBulkAsync(ids,
|
||||
_gamb.Data.VoteReward,
|
||||
new("vote", "discords", "discords.com vote reward"));
|
||||
|
||||
Log.Information("Rewarding {Count} discords.com voters", ids.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Critical error loading discords.com vote rewards");
|
||||
}
|
||||
}
|
||||
return new();
|
||||
}
|
||||
}
|
14
src/EllieBot/Modules/Owner/OwnerCommands.cs
Normal file
14
src/EllieBot/Modules/Owner/OwnerCommands.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using EllieBot.Modules.Gambling.Services;
|
||||
|
||||
namespace EllieBot.Modules.Owner;
|
||||
|
||||
[OwnerOnly]
|
||||
public class Owner(VoteRewardService vrs) : EllieModule
|
||||
{
|
||||
[Cmd]
|
||||
public async Task VoteFeed()
|
||||
{
|
||||
vrs.SetVoiceChannel(ctx.Channel);
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
}
|
|
@ -1652,4 +1652,8 @@ linkfix:
|
|||
- lfix
|
||||
linkfixlist:
|
||||
- linkfixlist
|
||||
- lfixlist
|
||||
- lfixlist
|
||||
votefeed:
|
||||
- votefeed
|
||||
vote:
|
||||
- vote
|
|
@ -5181,5 +5181,14 @@ linkfixlist:
|
|||
Lists all configured link fixes for the server.
|
||||
ex:
|
||||
- ''
|
||||
params:
|
||||
- { }
|
||||
votefeed:
|
||||
desc: |-
|
||||
Shows bot votes in real time in the specified channel.
|
||||
Omit channel to disable.
|
||||
ex:
|
||||
- '#votefeed'
|
||||
- ''
|
||||
params:
|
||||
- { }
|
|
@ -930,6 +930,7 @@
|
|||
"autodc_disable": "I will no longer disconnect from the voice channel when there are no more tracks to play.",
|
||||
"timely_none": "Bot owner didn't specify a timely reward.",
|
||||
"timely_already_claimed": "You've already claimed your timely reward. You can get it again {0}.",
|
||||
"vote_already_claimed": "You've already voted. You can vote again {0}.",
|
||||
"timely": "You've claimed your {0}. You can claim again in {1}h",
|
||||
"timely_set": "Users will be able to claim {0} every {1}h",
|
||||
"timely_set_none": "Users will not be able to claim any timely currency.",
|
||||
|
@ -1240,5 +1241,8 @@
|
|||
"linkfix_list_title": "Link Fixes",
|
||||
"linkfix_removed": "Link fix for {0} has been removed.",
|
||||
"linkfix_not_found": "No link fix found for {0}.",
|
||||
"notify_cant_set": "This event doesn't support origin channel, Please specify a channel"
|
||||
"notify_cant_set": "This event doesn't support origin channel, Please specify a channel",
|
||||
"vote_reward": "Thank you for voting! You've received {0}.",
|
||||
"vote_suggest": "Voting for the bot will get you {0}!",
|
||||
"vote_disabled": "Voting is disabled."
|
||||
}
|
Loading…
Add table
Reference in a new issue