mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	Patreon Access and Refresh Tokens should now be automatically updated
This commit is contained in:
		@@ -28,7 +28,7 @@ namespace NadekoBot
 | 
			
		||||
        private readonly IBotCredentials _creds;
 | 
			
		||||
        private readonly CommandService _commandService;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly BotCredsProvider _credsProvider;
 | 
			
		||||
        private readonly IBotCredsProvider _credsProvider;
 | 
			
		||||
        
 | 
			
		||||
        public event Func<GuildConfig, Task> JoinedGuild = delegate { return Task.CompletedTask; };
 | 
			
		||||
        
 | 
			
		||||
@@ -95,8 +95,8 @@ namespace NadekoBot
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            var svcs = new ServiceCollection()
 | 
			
		||||
                .AddTransient<IBotCredentials>(_ => _creds) // bot creds
 | 
			
		||||
                .AddSingleton(_credsProvider)
 | 
			
		||||
                .AddTransient<IBotCredentials>(_ => _credsProvider.GetCreds()) // bot creds
 | 
			
		||||
                .AddSingleton<IBotCredsProvider>(_credsProvider)
 | 
			
		||||
                .AddSingleton(_db) // database
 | 
			
		||||
                .AddRedis(_creds.RedisOptions) // redis
 | 
			
		||||
                .AddSingleton(Client) // discord socket client
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ namespace NadekoBot.Common.Attributes
 | 
			
		||||
    {
 | 
			
		||||
        public override Task<PreconditionResult> CheckPermissionsAsync(ICommandContext context, CommandInfo executingCommand, IServiceProvider services)
 | 
			
		||||
        {
 | 
			
		||||
            var creds = services.GetRequiredService<BotCredsProvider>().GetCreds();
 | 
			
		||||
            var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
 | 
			
		||||
 | 
			
		||||
            return Task.FromResult((creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id ? PreconditionResult.FromSuccess() : PreconditionResult.FromError("Not owner")));
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -73,11 +73,6 @@ go to https://www.patreon.com/portal -> my clients -> create client")]
 | 
			
		||||
Change only if you've changed the coordinator address or port.")]
 | 
			
		||||
        public string CoordinatorUrl { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [YamlIgnore]
 | 
			
		||||
        public string PatreonCampaignId => Patreon?.CampaignId;
 | 
			
		||||
        [YamlIgnore]
 | 
			
		||||
        public string PatreonAccessToken => Patreon?.AccessToken;
 | 
			
		||||
        
 | 
			
		||||
        [Comment(@"Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)")]
 | 
			
		||||
        public string RapidApiKey { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -121,11 +116,9 @@ Windows default
 | 
			
		||||
        // todo fixup patreon
 | 
			
		||||
        public sealed record PatreonSettings
 | 
			
		||||
        {
 | 
			
		||||
            [Comment(@"Access token. You have to manually update this 1st of each month by refreshing the token on https://patreon.com/portal")]
 | 
			
		||||
            public string ClientId { get; set; }
 | 
			
		||||
            public string AccessToken { get; set; }
 | 
			
		||||
            [Comment(@"Unused atm")]
 | 
			
		||||
            public string RefreshToken { get; set; }
 | 
			
		||||
            [Comment(@"Unused atm")]
 | 
			
		||||
            public string ClientSecret { get; set; }
 | 
			
		||||
 | 
			
		||||
            [Comment(@"Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type ""prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);"" in the console. (ctrl + shift + i)")]
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,11 @@ namespace NadekoBot
 | 
			
		||||
        string GoogleApiKey { get; }
 | 
			
		||||
        ICollection<ulong> OwnerIds { get; }
 | 
			
		||||
        string RapidApiKey { get; }
 | 
			
		||||
        string PatreonAccessToken { get; }
 | 
			
		||||
 | 
			
		||||
        Creds.DbOptions Db { get; }
 | 
			
		||||
        string OsuApiKey { get; }
 | 
			
		||||
        int TotalShards { get; }
 | 
			
		||||
        string PatreonCampaignId { get; }
 | 
			
		||||
        Creds.PatreonSettings Patreon { get; }
 | 
			
		||||
        string CleverbotApiKey { get; }
 | 
			
		||||
        RestartConfig RestartCommand { get; }
 | 
			
		||||
        Creds.VotesSettings Votes { get; }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
using Discord.Commands;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using NadekoBot.Common;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Services;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
@@ -23,6 +24,7 @@ namespace NadekoBot.Modules.Games
 | 
			
		||||
                _db = db;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            [NoPublicBot]
 | 
			
		||||
            [NadekoCommand, Aliases]
 | 
			
		||||
            [RequireContext(ContextType.Guild)]
 | 
			
		||||
            [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,134 @@
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Utility.Common.Patreon
 | 
			
		||||
{
 | 
			
		||||
    public class PatreonData
 | 
			
		||||
    public sealed class Attributes
 | 
			
		||||
    {
 | 
			
		||||
        public JObject[] Included { get; set; }
 | 
			
		||||
        public JObject[] Data { get; set; }
 | 
			
		||||
        public PatreonDataLinks Links { get; set; }
 | 
			
		||||
        [JsonPropertyName("full_name")]
 | 
			
		||||
        public string FullName { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("is_follower")]
 | 
			
		||||
        public bool IsFollower { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("last_charge_date")]
 | 
			
		||||
        public DateTime LastChargeDate { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("last_charge_status")]
 | 
			
		||||
        public string LastChargeStatus { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("lifetime_support_cents")]
 | 
			
		||||
        public int LifetimeSupportCents { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("currently_entitled_amount_cents")]
 | 
			
		||||
        public int CurrentlyEntitledAmountCents { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("patron_status")]
 | 
			
		||||
        public string PatronStatus { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class PatreonDataLinks
 | 
			
		||||
    public sealed class Data
 | 
			
		||||
    {
 | 
			
		||||
        public string first { get; set; }
 | 
			
		||||
        public string next { get; set; }
 | 
			
		||||
        [JsonPropertyName("id")]
 | 
			
		||||
        public string Id { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("type")]
 | 
			
		||||
        public string Type { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class PatreonUserAndReward
 | 
			
		||||
    public sealed class Address
 | 
			
		||||
    {
 | 
			
		||||
        public PatreonUser User { get; set; }
 | 
			
		||||
        public PatreonPledge Reward { get; set; }
 | 
			
		||||
        [JsonPropertyName("data")]
 | 
			
		||||
        public Data Data { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // public sealed class CurrentlyEntitledTiers
 | 
			
		||||
    // {
 | 
			
		||||
    //     [JsonPropertyName("data")]
 | 
			
		||||
    //     public List<Datum> Data { get; set; }
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    // public sealed class Relationships
 | 
			
		||||
    // {
 | 
			
		||||
    //     [JsonPropertyName("address")]
 | 
			
		||||
    //     public Address Address { get; set; }
 | 
			
		||||
    //
 | 
			
		||||
    //     // [JsonPropertyName("currently_entitled_tiers")]
 | 
			
		||||
    //     // public CurrentlyEntitledTiers CurrentlyEntitledTiers { get; set; }
 | 
			
		||||
    // }
 | 
			
		||||
 | 
			
		||||
    public sealed class PatreonResponse
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("data")]
 | 
			
		||||
        public List<PatreonMember> Data { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [JsonPropertyName("included")]
 | 
			
		||||
        public List<PatreonUser> Included { get; set; }
 | 
			
		||||
        
 | 
			
		||||
        [JsonPropertyName("links")]
 | 
			
		||||
        public PatreonLinks Links { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed class PatreonLinks
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("next")]
 | 
			
		||||
        public string Next { get; set; }
 | 
			
		||||
    } 
 | 
			
		||||
 | 
			
		||||
    public sealed class PatreonUser
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("attributes")]
 | 
			
		||||
        public PatreonUserAttributes Attributes { get; set; }
 | 
			
		||||
        [JsonPropertyName("id")]
 | 
			
		||||
        public string Id { get; set; }
 | 
			
		||||
        // public string Type { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed class PatreonUserAttributes
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("social_connections")]
 | 
			
		||||
        public PatreonSocials SocialConnections { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
    public sealed class PatreonSocials
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("discord")]
 | 
			
		||||
        public DiscordSocial Discord { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed class DiscordSocial
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("user_id")]
 | 
			
		||||
        public string UserId { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public sealed class PatreonMember
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("attributes")]
 | 
			
		||||
        public Attributes Attributes { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("relationships")]
 | 
			
		||||
        public Relationships Relationships { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("type")]
 | 
			
		||||
        public string Type { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed class Relationships
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("user")]
 | 
			
		||||
        public PatreonRelationshipUser User { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed class PatreonRelationshipUser
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("data")]
 | 
			
		||||
        public PatreonUserData Data { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public sealed class PatreonUserData
 | 
			
		||||
    {
 | 
			
		||||
        [JsonPropertyName("id")]
 | 
			
		||||
        public string Id { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Utility.Common.Patreon
 | 
			
		||||
{
 | 
			
		||||
    public class Attributes
 | 
			
		||||
    {
 | 
			
		||||
        public int amount_cents { get; set; }
 | 
			
		||||
        public string created_at { get; set; }
 | 
			
		||||
        public object declined_since { get; set; }
 | 
			
		||||
        public bool is_twitch_pledge { get; set; }
 | 
			
		||||
        public bool patron_pays_fees { get; set; }
 | 
			
		||||
        public int? pledge_cap_cents { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Address
 | 
			
		||||
    {
 | 
			
		||||
        public object data { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Data
 | 
			
		||||
    {
 | 
			
		||||
        public string id { get; set; }
 | 
			
		||||
        public string type { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Links
 | 
			
		||||
    {
 | 
			
		||||
        public string related { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Creator
 | 
			
		||||
    {
 | 
			
		||||
        public Data data { get; set; }
 | 
			
		||||
        public Links links { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Patron
 | 
			
		||||
    {
 | 
			
		||||
        public Data data { get; set; }
 | 
			
		||||
        public Links links { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Reward
 | 
			
		||||
    {
 | 
			
		||||
        public Data data { get; set; }
 | 
			
		||||
        public Links links { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Relationships
 | 
			
		||||
    {
 | 
			
		||||
        public Address address { get; set; }
 | 
			
		||||
        public Creator creator { get; set; }
 | 
			
		||||
        public Patron patron { get; set; }
 | 
			
		||||
        public Reward reward { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class PatreonPledge
 | 
			
		||||
    {
 | 
			
		||||
        public Attributes attributes { get; set; }
 | 
			
		||||
        public string id { get; set; }
 | 
			
		||||
        public Relationships relationships { get; set; }
 | 
			
		||||
        public string type { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,64 +0,0 @@
 | 
			
		||||
namespace NadekoBot.Modules.Utility.Common.Patreon
 | 
			
		||||
{
 | 
			
		||||
    public class DiscordConnection
 | 
			
		||||
    {
 | 
			
		||||
        public string user_id { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class SocialConnections
 | 
			
		||||
    {
 | 
			
		||||
        public object deviantart { get; set; }
 | 
			
		||||
        public DiscordConnection discord { get; set; }
 | 
			
		||||
        public object facebook { get; set; }
 | 
			
		||||
        public object spotify { get; set; }
 | 
			
		||||
        public object twitch { get; set; }
 | 
			
		||||
        public object twitter { get; set; }
 | 
			
		||||
        public object youtube { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class UserAttributes
 | 
			
		||||
    {
 | 
			
		||||
        public string about { get; set; }
 | 
			
		||||
        public string created { get; set; }
 | 
			
		||||
        public object discord_id { get; set; }
 | 
			
		||||
        public string email { get; set; }
 | 
			
		||||
        public object facebook { get; set; }
 | 
			
		||||
        public object facebook_id { get; set; }
 | 
			
		||||
        public string first_name { get; set; }
 | 
			
		||||
        public string full_name { get; set; }
 | 
			
		||||
        public int gender { get; set; }
 | 
			
		||||
        public bool has_password { get; set; }
 | 
			
		||||
        public string image_url { get; set; }
 | 
			
		||||
        public bool is_deleted { get; set; }
 | 
			
		||||
        public bool is_nuked { get; set; }
 | 
			
		||||
        public bool is_suspended { get; set; }
 | 
			
		||||
        public string last_name { get; set; }
 | 
			
		||||
        public SocialConnections social_connections { get; set; }
 | 
			
		||||
        public int status { get; set; }
 | 
			
		||||
        public string thumb_url { get; set; }
 | 
			
		||||
        public object twitch { get; set; }
 | 
			
		||||
        public string twitter { get; set; }
 | 
			
		||||
        public string url { get; set; }
 | 
			
		||||
        public string vanity { get; set; }
 | 
			
		||||
        public object youtube { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class Campaign
 | 
			
		||||
    {
 | 
			
		||||
        public Data data { get; set; }
 | 
			
		||||
        public Links links { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class UserRelationships
 | 
			
		||||
    {
 | 
			
		||||
        public Campaign campaign { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public class PatreonUser
 | 
			
		||||
    {
 | 
			
		||||
        public UserAttributes attributes { get; set; }
 | 
			
		||||
        public string id { get; set; }
 | 
			
		||||
        public UserRelationships relationships { get; set; }
 | 
			
		||||
        public string type { get; set; }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,6 +6,7 @@ using NadekoBot.Extensions;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Common.Attributes;
 | 
			
		||||
using NadekoBot.Modules.Utility.Services;
 | 
			
		||||
using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Utility
 | 
			
		||||
{
 | 
			
		||||
@@ -25,8 +26,12 @@ namespace NadekoBot.Modules.Utility
 | 
			
		||||
            [RequireContext(ContextType.DM)]
 | 
			
		||||
            public async Task ClaimPatreonRewards()
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken))
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(_creds.Patreon.AccessToken))
 | 
			
		||||
                {
 | 
			
		||||
                    Log.Warning("In order to use patreon reward commands, " +
 | 
			
		||||
                                "you need to specify CampaignId and AccessToken in creds.yml");
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (DateTime.UtcNow.Day < 5)
 | 
			
		||||
                {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,17 +2,21 @@
 | 
			
		||||
using NadekoBot.Services;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using NadekoBot.Modules.Utility.Common.Patreon;
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using System;
 | 
			
		||||
using System.Collections.Generic;
 | 
			
		||||
using System.Linq;
 | 
			
		||||
using System.Net.Http;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
using System.Threading;
 | 
			
		||||
using System.Threading.Tasks;
 | 
			
		||||
using System.Web;
 | 
			
		||||
using Discord;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Extensions;
 | 
			
		||||
using Serilog;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
using JsonSerializer = System.Text.Json.JsonSerializer;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Utility.Services
 | 
			
		||||
{
 | 
			
		||||
@@ -20,96 +24,195 @@ namespace NadekoBot.Modules.Utility.Services
 | 
			
		||||
    {
 | 
			
		||||
        private readonly SemaphoreSlim getPledgesLocker = new SemaphoreSlim(1, 1);
 | 
			
		||||
 | 
			
		||||
        private PatreonUserAndReward[] _pledges;
 | 
			
		||||
 | 
			
		||||
        private readonly Timer _updater;
 | 
			
		||||
        private readonly SemaphoreSlim claimLockJustInCase = new SemaphoreSlim(1, 1);
 | 
			
		||||
        
 | 
			
		||||
        public TimeSpan Interval { get; } = TimeSpan.FromMinutes(3);
 | 
			
		||||
        private readonly IBotCredentials _creds;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
        private readonly ICurrencyService _currency;
 | 
			
		||||
        private readonly GamblingConfigService _gamblingConfigService;
 | 
			
		||||
        private readonly ConnectionMultiplexer _redis;
 | 
			
		||||
        private readonly IBotCredsProvider _credsProvider;
 | 
			
		||||
        private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
        private readonly IEmbedBuilderService _eb;
 | 
			
		||||
        private readonly DiscordSocketClient _client;
 | 
			
		||||
 | 
			
		||||
        public DateTime LastUpdate { get; private set; } = DateTime.UtcNow;
 | 
			
		||||
 | 
			
		||||
        public PatreonRewardsService(IBotCredentials creds, DbService db,
 | 
			
		||||
            ICurrencyService currency, IHttpClientFactory factory, IEmbedBuilderService eb,
 | 
			
		||||
            DiscordSocketClient client, GamblingConfigService gamblingConfigService)
 | 
			
		||||
        public PatreonRewardsService(
 | 
			
		||||
            DbService db,
 | 
			
		||||
            ICurrencyService currency,
 | 
			
		||||
            IHttpClientFactory factory,
 | 
			
		||||
            IEmbedBuilderService eb,
 | 
			
		||||
            DiscordSocketClient client,
 | 
			
		||||
            GamblingConfigService gamblingConfigService,
 | 
			
		||||
            ConnectionMultiplexer redis,
 | 
			
		||||
            IBotCredsProvider credsProvider)
 | 
			
		||||
        {
 | 
			
		||||
            _creds = creds;
 | 
			
		||||
            _db = db;
 | 
			
		||||
            _currency = currency;
 | 
			
		||||
            _gamblingConfigService = gamblingConfigService;
 | 
			
		||||
            _redis = redis;
 | 
			
		||||
            _credsProvider = credsProvider;
 | 
			
		||||
            _httpFactory = factory;
 | 
			
		||||
            _eb = eb;
 | 
			
		||||
            _client = client;
 | 
			
		||||
 | 
			
		||||
            if (client.ShardId == 0)
 | 
			
		||||
                _updater = new Timer(async _ => await RefreshPledges().ConfigureAwait(false),
 | 
			
		||||
                _updater = new Timer(async _ => await RefreshPledges(_credsProvider.GetCreds()).ConfigureAwait(false),
 | 
			
		||||
                    null, TimeSpan.Zero, Interval);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task RefreshPledges()
 | 
			
		||||
        private DateTime LastAccessTokenUpdate(IBotCredentials creds)
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(_creds.PatreonAccessToken)
 | 
			
		||||
                || string.IsNullOrWhiteSpace(_creds.PatreonAccessToken))
 | 
			
		||||
                return;
 | 
			
		||||
            var db = _redis.GetDatabase();
 | 
			
		||||
            var val = db.StringGet($"{creds.RedisKey()}_patreon_update");
 | 
			
		||||
 | 
			
		||||
            if (val == default)
 | 
			
		||||
                return DateTime.MinValue;
 | 
			
		||||
 | 
			
		||||
            var lastTime = DateTime.FromBinary((long)val);
 | 
			
		||||
            return lastTime;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        private sealed class PatreonRefreshData
 | 
			
		||||
        {
 | 
			
		||||
            [JsonPropertyName("access_token")]
 | 
			
		||||
            public string AccessToken { get; set; }
 | 
			
		||||
            
 | 
			
		||||
            [JsonPropertyName("refresh_token")]
 | 
			
		||||
            public string RefreshToken { get; set; }
 | 
			
		||||
            
 | 
			
		||||
            [JsonPropertyName("expires_in")]
 | 
			
		||||
            public long ExpiresIn { get; set; }
 | 
			
		||||
            
 | 
			
		||||
            [JsonPropertyName("scope")]
 | 
			
		||||
            public string Scope { get; set; }
 | 
			
		||||
            
 | 
			
		||||
            [JsonPropertyName("token_type")]
 | 
			
		||||
            public string TokenType { get; set; }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        private async Task<bool> UpdateAccessToken(IBotCredentials creds)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Information("Updating patreon access token...");
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using var http = _httpFactory.CreateClient();
 | 
			
		||||
                var res = await http.PostAsync($"https://www.patreon.com/api/oauth2/token" +
 | 
			
		||||
                                               $"?grant_type=refresh_token" +
 | 
			
		||||
                                               $"&refresh_token={creds.Patreon.RefreshToken}" +
 | 
			
		||||
                                               $"&client_id={creds.Patreon.ClientId}" +
 | 
			
		||||
                                               $"&client_secret={creds.Patreon.ClientSecret}",
 | 
			
		||||
                    new StringContent(string.Empty));
 | 
			
		||||
 | 
			
		||||
                res.EnsureSuccessStatusCode();
 | 
			
		||||
 | 
			
		||||
                var data = await res.Content.ReadFromJsonAsync<PatreonRefreshData>();
 | 
			
		||||
 | 
			
		||||
                if (data is null)
 | 
			
		||||
                    throw new("Invalid patreon response.");
 | 
			
		||||
                
 | 
			
		||||
                _credsProvider.ModifyCredsFile(oldData =>
 | 
			
		||||
                {
 | 
			
		||||
                    oldData.Patreon.AccessToken = data.AccessToken;
 | 
			
		||||
                    oldData.Patreon.RefreshToken = data.RefreshToken;
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                var db = _redis.GetDatabase();
 | 
			
		||||
                await db.StringSetAsync($"{creds.RedisKey()}_patreon_update", DateTime.UtcNow.ToBinary());
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Error("Failed updating patreon access token: {ErrorMessage}", ex.ToString());
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool HasPatreonCreds(IBotCredentials creds)
 | 
			
		||||
        {
 | 
			
		||||
            var _1 = creds.Patreon.ClientId;
 | 
			
		||||
            var _2 = creds.Patreon.ClientSecret;
 | 
			
		||||
            var _4 = creds.Patreon.RefreshToken;
 | 
			
		||||
            return !(string.IsNullOrWhiteSpace(_1)
 | 
			
		||||
                     || string.IsNullOrWhiteSpace(_2)
 | 
			
		||||
                     || string.IsNullOrWhiteSpace(_4));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task RefreshPledges(IBotCredentials creds)
 | 
			
		||||
        {
 | 
			
		||||
            if (DateTime.UtcNow.Day < 5)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            // if the user has the necessary patreon creds
 | 
			
		||||
            // and the access token expired or doesn't exist
 | 
			
		||||
            // -> update access token
 | 
			
		||||
            if (!HasPatreonCreds(creds))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (LastAccessTokenUpdate(creds).Month < DateTime.UtcNow.Month
 | 
			
		||||
                || string.IsNullOrWhiteSpace(creds.Patreon.AccessToken))
 | 
			
		||||
            {
 | 
			
		||||
                var success = await UpdateAccessToken(creds);
 | 
			
		||||
                if (!success)
 | 
			
		||||
                    return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            LastUpdate = DateTime.UtcNow;
 | 
			
		||||
            await getPledgesLocker.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var rewards = new List<PatreonPledge>();
 | 
			
		||||
                
 | 
			
		||||
                var members = new List<PatreonMember>();
 | 
			
		||||
                var users = new List<PatreonUser>();
 | 
			
		||||
                using (var http = _httpFactory.CreateClient())
 | 
			
		||||
                {
 | 
			
		||||
                    http.DefaultRequestHeaders.Clear();
 | 
			
		||||
                    http.DefaultRequestHeaders.Add("Authorization", "Bearer " + _creds.PatreonAccessToken);
 | 
			
		||||
                    var data = new PatreonData()
 | 
			
		||||
                    {
 | 
			
		||||
                        Links = new PatreonDataLinks()
 | 
			
		||||
                        {
 | 
			
		||||
                            next = $"https://api.patreon.com/oauth2/api/campaigns/{_creds.PatreonCampaignId}/pledges"
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
                    http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization",
 | 
			
		||||
                        $"Bearer {creds.Patreon.AccessToken}");
 | 
			
		||||
 | 
			
		||||
                    var page = $"https://www.patreon.com/api/oauth2/v2/campaigns/{creds.Patreon.CampaignId}/members" +
 | 
			
		||||
                               "?fields%5Bmember%5D=full_name,currently_entitled_amount_cents" +
 | 
			
		||||
                               "&fields%5Buser%5D=social_connections" +
 | 
			
		||||
                               "&include=user";
 | 
			
		||||
                    PatreonResponse data = null;
 | 
			
		||||
                    do
 | 
			
		||||
                    {
 | 
			
		||||
                        var res = await http.GetStringAsync(data.Links.next)
 | 
			
		||||
                            .ConfigureAwait(false);
 | 
			
		||||
                        data = JsonConvert.DeserializeObject<PatreonData>(res);
 | 
			
		||||
                        var pledgers = data.Data.Where(x => x["type"].ToString() == "pledge");
 | 
			
		||||
                        rewards.AddRange(pledgers.Select(x => JsonConvert.DeserializeObject<PatreonPledge>(x.ToString()))
 | 
			
		||||
                            .Where(x => x.attributes.declined_since is null));
 | 
			
		||||
                        if (data.Included != null)
 | 
			
		||||
                        {
 | 
			
		||||
                            users.AddRange(data.Included
 | 
			
		||||
                                .Where(x => x["type"].ToString() == "user")
 | 
			
		||||
                                .Select(x => JsonConvert.DeserializeObject<PatreonUser>(x.ToString())));
 | 
			
		||||
                        }
 | 
			
		||||
                    } while (!string.IsNullOrWhiteSpace(data.Links.next));
 | 
			
		||||
                        var res = await http.GetStringAsync(page).ConfigureAwait(false);
 | 
			
		||||
                        data = JsonSerializer.Deserialize<PatreonResponse>(res);
 | 
			
		||||
 | 
			
		||||
                        if (data is null)
 | 
			
		||||
                            break;
 | 
			
		||||
                        
 | 
			
		||||
                        members.AddRange(data.Data);
 | 
			
		||||
                        users.AddRange(data.Included);
 | 
			
		||||
                    } while (!string.IsNullOrWhiteSpace(page = data?.Links?.Next));
 | 
			
		||||
                }
 | 
			
		||||
                var toSet = rewards.Join(users, (r) => r.relationships?.patron?.data?.id, (u) => u.id, (x, y) => new PatreonUserAndReward()
 | 
			
		||||
                {
 | 
			
		||||
                    User = y,
 | 
			
		||||
                    Reward = x,
 | 
			
		||||
                }).ToArray();
 | 
			
		||||
 | 
			
		||||
                _pledges = toSet;
 | 
			
		||||
 | 
			
		||||
                foreach (var pledge in _pledges)
 | 
			
		||||
                {
 | 
			
		||||
                    var userIdStr = pledge.User.attributes?.social_connections?.discord?.user_id;
 | 
			
		||||
                    if (userIdStr != null && ulong.TryParse(userIdStr, out var userId))
 | 
			
		||||
                var userData = members.Join(users,
 | 
			
		||||
                    (m) => m.Relationships.User.Data.Id,
 | 
			
		||||
                    (u) => u.Id,
 | 
			
		||||
                    (m, u) => new
 | 
			
		||||
                    {
 | 
			
		||||
                        await ClaimReward(userId);
 | 
			
		||||
                    }
 | 
			
		||||
                        PatreonUserId = m.Relationships.User.Data.Id,
 | 
			
		||||
                        UserId = ulong.TryParse(u.Attributes?.SocialConnections?.Discord?.UserId ?? string.Empty,
 | 
			
		||||
                            out var userId)
 | 
			
		||||
                            ? userId
 | 
			
		||||
                            : 0,
 | 
			
		||||
                        EntitledTo = m.Attributes.CurrentlyEntitledAmountCents,
 | 
			
		||||
                    })
 | 
			
		||||
                    .Where(x => x is
 | 
			
		||||
                    {
 | 
			
		||||
                        UserId: not 0,
 | 
			
		||||
                        EntitledTo: > 0
 | 
			
		||||
                    })
 | 
			
		||||
                    .ToList();
 | 
			
		||||
 | 
			
		||||
                foreach (var pledge in userData)
 | 
			
		||||
                {
 | 
			
		||||
                    await ClaimReward(pledge.UserId, pledge.PatreonUserId, pledge.EntitledTo);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception ex)
 | 
			
		||||
@@ -123,80 +226,73 @@ namespace NadekoBot.Modules.Utility.Services
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async Task<int> ClaimReward(ulong userId)
 | 
			
		||||
        public async Task<int> ClaimReward(ulong userId, string patreonUserId, int cents)
 | 
			
		||||
        {
 | 
			
		||||
            await claimLockJustInCase.WaitAsync().ConfigureAwait(false);
 | 
			
		||||
            var settings = _gamblingConfigService.Data;
 | 
			
		||||
            var now = DateTime.UtcNow;
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var datas = _pledges?.Where(x => x.User.attributes?.social_connections?.discord?.user_id == userId.ToString())
 | 
			
		||||
                    ?? Enumerable.Empty<PatreonUserAndReward>();
 | 
			
		||||
                var eligibleFor = (int)(cents * settings.PatreonCurrencyPerCent);
 | 
			
		||||
 | 
			
		||||
                var totalAmount = 0;
 | 
			
		||||
                foreach (var data in datas)
 | 
			
		||||
                using (var uow = _db.GetDbContext())
 | 
			
		||||
                {
 | 
			
		||||
                    var amount = (int)(data.Reward.attributes.amount_cents * settings.PatreonCurrencyPerCent);
 | 
			
		||||
                    var users = uow.Set<RewardedUser>();
 | 
			
		||||
                    var usr = await users.FirstOrDefaultAsync(x => x.PatreonUserId == patreonUserId);
 | 
			
		||||
 | 
			
		||||
                    using (var uow = _db.GetDbContext())
 | 
			
		||||
                    if (usr is null)
 | 
			
		||||
                    {
 | 
			
		||||
                        var users = uow.Set<RewardedUser>();
 | 
			
		||||
                        var usr = users.FirstOrDefault(x => x.PatreonUserId == data.User.id);
 | 
			
		||||
 | 
			
		||||
                        if (usr is null)
 | 
			
		||||
                        users.Add(new RewardedUser()
 | 
			
		||||
                        {
 | 
			
		||||
                            users.Add(new RewardedUser()
 | 
			
		||||
                            {
 | 
			
		||||
                                PatreonUserId = data.User.id,
 | 
			
		||||
                                LastReward = now,
 | 
			
		||||
                                AmountRewardedThisMonth = amount,
 | 
			
		||||
                            });
 | 
			
		||||
                            PatreonUserId = patreonUserId,
 | 
			
		||||
                            LastReward = now,
 | 
			
		||||
                            AmountRewardedThisMonth = eligibleFor,
 | 
			
		||||
                        });
 | 
			
		||||
 | 
			
		||||
                            await uow.SaveChangesAsync();
 | 
			
		||||
                        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
                            await _currency.AddAsync(userId, "Patreon reward - new", amount, gamble: true);
 | 
			
		||||
                            totalAmount += amount;
 | 
			
		||||
                            
 | 
			
		||||
                            Log.Information($"Sending new currency reward to {userId}");
 | 
			
		||||
                            await SendMessageToUser(userId, $"Thank you for your pledge! " +
 | 
			
		||||
                                                            $"You've been awarded **{amount}**{settings.Currency.Sign} !");
 | 
			
		||||
                            continue;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (usr.LastReward.Month != now.Month)
 | 
			
		||||
                        {
 | 
			
		||||
                            usr.LastReward = now;
 | 
			
		||||
                            usr.AmountRewardedThisMonth = amount;
 | 
			
		||||
 | 
			
		||||
                            await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
                            await _currency.AddAsync(userId, "Patreon reward - recurring", amount, gamble: true);
 | 
			
		||||
                            totalAmount += amount;
 | 
			
		||||
                            Log.Information($"Sending recurring currency reward to {userId}");
 | 
			
		||||
                            await SendMessageToUser(userId, $"Thank you for your continued support! " +
 | 
			
		||||
                                                            $"You've been awarded **{amount}**{settings.Currency.Sign} for this month's support!");
 | 
			
		||||
                            continue;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        if (usr.AmountRewardedThisMonth < amount)
 | 
			
		||||
                        {
 | 
			
		||||
                            var toAward = amount - usr.AmountRewardedThisMonth;
 | 
			
		||||
 | 
			
		||||
                            usr.LastReward = now;
 | 
			
		||||
                            usr.AmountRewardedThisMonth = amount;
 | 
			
		||||
                            await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
                            await _currency.AddAsync(userId, "Patreon reward - update", toAward, gamble: true);
 | 
			
		||||
                            totalAmount += toAward;
 | 
			
		||||
                            Log.Information($"Sending updated currency reward to {userId}");
 | 
			
		||||
                            await SendMessageToUser(userId, $"Thank you for increasing your pledge! " +
 | 
			
		||||
                                $"You've been awarded an additional **{toAward}**{settings.Currency.Sign} !");
 | 
			
		||||
                            continue;
 | 
			
		||||
                        }
 | 
			
		||||
                        await _currency.AddAsync(userId, "Patreon reward - new", eligibleFor, gamble: true);
 | 
			
		||||
                        
 | 
			
		||||
                        Log.Information($"Sending new currency reward to {userId}");
 | 
			
		||||
                        await SendMessageToUser(userId, $"Thank you for your pledge! " +
 | 
			
		||||
                                                        $"You've been awarded **{eligibleFor}**{settings.Currency.Sign} !");
 | 
			
		||||
                        return eligibleFor;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                return totalAmount;
 | 
			
		||||
                    if (usr.LastReward.Month != now.Month)
 | 
			
		||||
                    {
 | 
			
		||||
                        usr.LastReward = now;
 | 
			
		||||
                        usr.AmountRewardedThisMonth = eligibleFor;
 | 
			
		||||
 | 
			
		||||
                        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
                        await _currency.AddAsync(userId, "Patreon reward - recurring", eligibleFor, gamble: true);
 | 
			
		||||
 | 
			
		||||
                        Log.Information($"Sending recurring currency reward to {userId}");
 | 
			
		||||
                        await SendMessageToUser(userId, $"Thank you for your continued support! " +
 | 
			
		||||
                                                        $"You've been awarded **{eligibleFor}**{settings.Currency.Sign} for this month's support!");
 | 
			
		||||
 | 
			
		||||
                        return eligibleFor;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (usr.AmountRewardedThisMonth < eligibleFor)
 | 
			
		||||
                    {
 | 
			
		||||
                        var toAward = eligibleFor - usr.AmountRewardedThisMonth;
 | 
			
		||||
 | 
			
		||||
                        usr.LastReward = now;
 | 
			
		||||
                        usr.AmountRewardedThisMonth = toAward;
 | 
			
		||||
                        await uow.SaveChangesAsync();
 | 
			
		||||
 | 
			
		||||
                        await _currency.AddAsync(userId, "Patreon reward - update", toAward, gamble: true);
 | 
			
		||||
                        
 | 
			
		||||
                        Log.Information($"Sending updated currency reward to {userId}");
 | 
			
		||||
                        await SendMessageToUser(userId, $"Thank you for increasing your pledge! " +
 | 
			
		||||
                                                        $"You've been awarded an additional **{toAward}**{settings.Currency.Sign} !");
 | 
			
		||||
                        return toAward;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    return 0;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            finally
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,14 @@ using Serilog;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services
 | 
			
		||||
{
 | 
			
		||||
    public sealed class BotCredsProvider
 | 
			
		||||
    public interface IBotCredsProvider
 | 
			
		||||
    {
 | 
			
		||||
        public void Reload();
 | 
			
		||||
        public IBotCredentials GetCreds();
 | 
			
		||||
        public void ModifyCredsFile(Action<Creds> func);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public sealed class BotCredsProvider : IBotCredsProvider
 | 
			
		||||
    {
 | 
			
		||||
        private readonly int? _totalShards;
 | 
			
		||||
        private const string _credsFileName = "creds.yml";
 | 
			
		||||
@@ -27,7 +34,7 @@ namespace NadekoBot.Services
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        private readonly object reloadLock = new object();
 | 
			
		||||
        private void Reload()
 | 
			
		||||
        public void Reload()
 | 
			
		||||
        {
 | 
			
		||||
            lock (reloadLock)
 | 
			
		||||
            {
 | 
			
		||||
@@ -102,6 +109,19 @@ namespace NadekoBot.Services
 | 
			
		||||
            Reload();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public void ModifyCredsFile(Action<Creds> func)
 | 
			
		||||
        {
 | 
			
		||||
            var ymlData = File.ReadAllText(_credsFileName);
 | 
			
		||||
            var creds = Yaml.Deserializer.Deserialize<Creds>(ymlData);
 | 
			
		||||
 | 
			
		||||
            func(creds);
 | 
			
		||||
 | 
			
		||||
            ymlData = Yaml.Serializer.Serialize(creds);
 | 
			
		||||
            File.WriteAllText(_credsFileName, ymlData);
 | 
			
		||||
            
 | 
			
		||||
            Reload();
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Checks if there's a V2 credentials file present, loads it if it exists,
 | 
			
		||||
        /// converts it to new model, and saves it to YAML. Also backs up old credentials to credentials.json.bak
 | 
			
		||||
@@ -157,6 +177,6 @@ namespace NadekoBot.Services
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public Creds GetCreds() => _creds;
 | 
			
		||||
        public IBotCredentials GetCreds() => _creds;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -357,7 +357,7 @@ namespace NadekoBot.Extensions
 | 
			
		||||
 | 
			
		||||
        public static bool IsImage(this HttpResponseMessage msg, out string mimeType)
 | 
			
		||||
        {
 | 
			
		||||
            mimeType = msg.Content.Headers.ContentType.MediaType;
 | 
			
		||||
            mimeType = msg.Content.Headers.ContentType?.MediaType;
 | 
			
		||||
            if (mimeType == "image/png"
 | 
			
		||||
                    || mimeType == "image/jpeg"
 | 
			
		||||
                    || mimeType == "image/gif")
 | 
			
		||||
 
 | 
			
		||||
@@ -31,11 +31,9 @@ votes:
 | 
			
		||||
# Patreon auto reward system settings.
 | 
			
		||||
# go to https://www.patreon.com/portal -> my clients -> create client
 | 
			
		||||
patreon:
 | 
			
		||||
# Access token. You have to manually update this 1st of each month by refreshing the token on https://patreon.com/portal
 | 
			
		||||
  clientId: 
 | 
			
		||||
  accessToken: ''
 | 
			
		||||
  # Unused atm
 | 
			
		||||
  refreshToken: ''
 | 
			
		||||
  # Unused atm
 | 
			
		||||
  clientSecret: ''
 | 
			
		||||
  # Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i)
 | 
			
		||||
  campaignId: ''
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user