Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ Generated\ Files/
# NUnit
*.VisualState.xml
TestResult.xml

TiktokExplode.Bot/appsettings.json
nunit-*.xml

# Build Results of an ATL Project
Expand Down
5 changes: 3 additions & 2 deletions TiktokExplode.All/TiktokExplode.All.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
Expand All @@ -10,7 +10,7 @@

<PropertyGroup>
<PackageId>TiktokExplode.All</PackageId>
<Version>1.2.1</Version>
<Version>1.2.2</Version>
<Authors>Ts-Pytham</Authors>
<Owners>Ts-Pytham</Owners>
<Description>Meta-package that installs TiktokExplode (domain layer) and TiktokExplode.Infrastructure (HTTP/browser fetchers, parser, download client) in a single command.</Description>
Expand All @@ -24,6 +24,7 @@

<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\"/>
<None Include="..\assets\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
Expand Down
77 changes: 77 additions & 0 deletions TiktokExplode.Bot/CDN/CloudflareR2CdnProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using TiktokExplode.Bot.Configuration;

namespace TiktokExplode.Bot.CDN;

public sealed class CloudflareR2CdnProvider : ICdnProvider
{
public string Name => "Cloudflare R2";

private readonly AmazonS3Client _s3;
private readonly CloudflareR2Options _options;
private readonly ILogger _logger;

public CloudflareR2CdnProvider(IOptions<CloudflareR2Options> options, ILogger<CloudflareR2CdnProvider> logger)
{
_options = options.Value;
_logger = logger;

var config = new AmazonS3Config
{
ServiceURL = $"https://{_options.AccountId}.r2.cloudflarestorage.com",
ForcePathStyle = true,
AuthenticationRegion = "auto"
};

_s3 = new AmazonS3Client(
new BasicAWSCredentials(_options.AccessKeyId, _options.SecretAccessKey),
config);
}

public async Task<string> UploadAsync(byte[] data, string filename, CancellationToken ct = default)
{
if (await GetUrlAsync(filename, ct) is string existingUrl && !string.IsNullOrWhiteSpace(existingUrl))
{
return existingUrl;
}

var key = $"{filename}.mp4";
_logger.LogInformation("Subiendo archivo {Filename} a R2", key);

using var stream = new MemoryStream(data, writable: false);

var request = new PutObjectRequest
{
BucketName = _options.BucketName,
Key = key,
InputStream = stream,
ContentType = "video/mp4",
DisablePayloadSigning = true
};

await _s3.PutObjectAsync(request, ct);

return $"{_options.PublicBaseUrl.TrimEnd('/')}/{key}";
}

public async Task<string> GetUrlAsync(string filename, CancellationToken ct = default)
{
var key = $"{filename}.mp4";
var publicUrl = $"{_options.PublicBaseUrl.TrimEnd('/')}/{key}";
try
{
await _s3.GetObjectMetadataAsync(_options.BucketName, key, ct);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
_logger.LogInformation("Archivo {Filename} no encontrado en R2", key);
return string.Empty;
}

return publicUrl;
}
}
54 changes: 54 additions & 0 deletions TiktokExplode.Bot/CDN/CompositeCdnProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Microsoft.Extensions.Logging;

namespace TiktokExplode.Bot.CDN;

/// <summary>
/// Itera los <see cref="ICdnProvider"/> registrados en orden y devuelve la primera URL exitosa.
/// Si todos fallan, lanza <see cref="InvalidOperationException"/>.
/// </summary>
public sealed class CompositeCdnProvider(
IEnumerable<ICdnProvider> providers,
ILogger<CompositeCdnProvider> logger)
{
public async Task<string> UploadAsync(byte[] data, string filename, CancellationToken ct = default)
{
foreach (var provider in providers)
{
try
{
var url = await provider.UploadAsync(data, filename, ct);
logger.LogInformation("Upload exitoso via {Provider}: {Url}", provider.Name, url);
return url;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Provider {Provider} falló ({Msg}), probando el siguiente", provider.Name, ex.Message);
}
}

throw new InvalidOperationException("Todos los proveedores CDN fallaron.");
}

public async Task<string> GetUrlAsync(string filename, CancellationToken ct = default)
{
foreach (var provider in providers)
{
try
{
var url = await provider.GetUrlAsync(filename, ct);

if (string.IsNullOrWhiteSpace(url))
continue;

logger.LogInformation("GetUrl exitoso via {Provider}: {Url}", provider.Name, url);
return url;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Provider {Provider} falló en GetUrl ({Msg}), probando el siguiente", provider.Name, ex.Message);
}
}

return string.Empty;
}
}
8 changes: 8 additions & 0 deletions TiktokExplode.Bot/CDN/ICdnProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace TiktokExplode.Bot.CDN;

public interface ICdnProvider
{
string Name { get; }
Task<string> UploadAsync(byte[] data, string filename, CancellationToken ct = default);
Task<string> GetUrlAsync(string filename, CancellationToken ct = default);
}
34 changes: 34 additions & 0 deletions TiktokExplode.Bot/CDN/LitterboxCdnProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.Net.Http.Headers;

namespace TiktokExplode.Bot.CDN;

public sealed class LitterboxCdnProvider(IHttpClientFactory httpClientFactory) : ICdnProvider
{
public string Name => "Litterbox (catbox.moe)";

private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;

public async Task<string> UploadAsync(byte[] data, string filename, CancellationToken ct = default)
{
var http = _httpClientFactory.CreateClient();
http.DefaultRequestHeaders.UserAgent.ParseAdd("TiktokExplodeBot/1.1");

using var form = new MultipartFormDataContent();
form.Add(new StringContent("fileupload"), "reqtype");
form.Add(new StringContent("72h"), "time");
var fc = new ByteArrayContent(data);
fc.Headers.ContentType = new MediaTypeHeaderValue("video/mp4");
form.Add(fc, "fileToUpload", $"{filename}.mp4");

var req = new HttpRequestMessage(HttpMethod.Post,
"https://litterbox.catbox.moe/resources/internals/api.php")
{ Content = form };
using var resp = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();

return (await resp.Content.ReadAsStringAsync(ct)).Trim();
}

public Task<string> GetUrlAsync(string filename, CancellationToken ct = default)
=> Task.FromResult($"https://litterbox.catbox.moe/{filename}.mp4");
}
28 changes: 28 additions & 0 deletions TiktokExplode.Bot/CDN/ZeroXZeroCdnProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Net.Http.Headers;

namespace TiktokExplode.Bot.CDN;

public sealed class ZeroXZeroCdnProvider(IHttpClientFactory httpClientFactory) : ICdnProvider
{
public string Name => "0x0.st";

public async Task<string> UploadAsync(byte[] data, string filename, CancellationToken ct = default)
{
var http = httpClientFactory.CreateClient();
http.DefaultRequestHeaders.UserAgent.ParseAdd("TiktokExplodeBot/1.1");

using var form = new MultipartFormDataContent();
var fc = new ByteArrayContent(data);
fc.Headers.ContentType = new MediaTypeHeaderValue("video/mp4");
form.Add(fc, "file", $"{filename}.mp4");

var req = new HttpRequestMessage(HttpMethod.Post, "https://0x0.st") { Content = form };
using var resp = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();

return (await resp.Content.ReadAsStringAsync(ct)).Trim();
}

public Task<string> GetUrlAsync(string filename, CancellationToken ct = default)
=> Task.FromResult($"https://0x0.st/{filename}.mp4");
}
12 changes: 12 additions & 0 deletions TiktokExplode.Bot/Configuration/BotSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace TiktokExplode.Bot.Configuration;

public class BotSettings
{
public required string Token { get; init; }

/// <summary>
/// ID del servidor donde registrar los comandos al instante (desarrollo).
/// Si es 0 o no está configurado, se registran globalmente (hasta 1 hora de propagación).
/// </summary>
public ulong GuildId { get; init; }
}
12 changes: 12 additions & 0 deletions TiktokExplode.Bot/Configuration/CloudflareR2Options.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace TiktokExplode.Bot.Configuration;

public sealed class CloudflareR2Options
{
public const string Section = "CloudflareR2";

public string AccountId { get; set; } = string.Empty;
public string AccessKeyId { get; set; } = string.Empty;
public string SecretAccessKey { get; set; } = string.Empty;
public string BucketName { get; set; } = string.Empty;
public string PublicBaseUrl { get; set; } = string.Empty;
}
Loading
Loading