Skip to content
Open
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
109 changes: 109 additions & 0 deletions DiscordChatExporter.Core/Discord/DiscordClient.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using DiscordChatExporter.Core.Discord.Data;
Expand All @@ -18,6 +21,16 @@

namespace DiscordChatExporter.Core.Discord;

[JsonSerializable(typeof(XSuperProperties))]
internal partial class XSuperPropertiesJsonContext : JsonSerializerContext;

internal sealed record XSuperProperties(
[property: JsonPropertyName("os")] string OperatingSystem,
[property: JsonPropertyName("client_build_number")] string ClientBuildNumber,
[property: JsonPropertyName("client_launch_id")] Guid ClientLaunchId,
[property: JsonPropertyName("launch_signature")] Guid LaunchSignature
);

public class DiscordClient(
string token,
RateLimitPreference rateLimitPreference = RateLimitPreference.RespectAll
Expand All @@ -26,6 +39,86 @@ public class DiscordClient(
private readonly Uri _baseUri = new("https://discord.com/api/v10/", UriKind.Absolute);
private TokenKind? _resolvedTokenKind;

private static string? _xSuperPropertiesHeader;

private static Guid GenerateLaunchSignature()
{
var bytes = Guid.NewGuid().ToByteArray();
var bits = new BitArray(bytes);

// Clear the mod-detection bits
int[] bitPositions = [119, 108, 100, 91, 84, 75, 61, 55, 48, 38, 24, 11];
foreach (int bit in bitPositions)
bits[bit] = false;

bits.CopyTo(bytes, 0);

return new Guid(bytes);
}

// We could generate a random UserAgent. However, that way we could run into issues where certain older or newer versions
// and certain browsers could trigger experimental or unsupported features on Discord's site, resulting in the requests potentially failing,
// creating impossible to replicate issues for users
private const string UserAgent =
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36";

private static async Task<string?> GetXSuperPropertiesHeaderAsync(
CancellationToken cancellationToken = default
)
{
var buildNumber = await GetBuildNumberAsync();

if (buildNumber == null)
return null;

// https://github.com/greg6775/Discord-Api-Endpoints/blob/master/README.md
// https://docs.discord.food/reference#client-properties
var json = JsonSerializer.Serialize(
new XSuperProperties(
OperatingSystem: "Linux", // Operating System based on UserAgent
ClientBuildNumber: buildNumber,
ClientLaunchId: Guid.NewGuid(),
LaunchSignature: GenerateLaunchSignature()
),
XSuperPropertiesJsonContext.Default.XSuperProperties
);

return Convert.ToBase64String(Encoding.UTF8.GetBytes(json));

async Task<string?> GetBuildNumberAsync()
{
using var request = new HttpRequestMessage(HttpMethod.Get, "https://discord.com/app");
request.Headers.UserAgent.ParseAdd(UserAgent);

var response = await Http.Client.SendAsync(
request,
HttpCompletionOption.ResponseContentRead,
cancellationToken
);
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);

// Parse BUILD_NUMBER from the response body
// Example being: "<script nonce="...==">window.GLOBAL_ENV = {"NODE_ENV":"production","BUILT_AT":"1773386344946","HTML_TIMESTAMP":Date.now(),"BUILD_NUMBER":"510733","...
var buildNumberIndex = responseBody.IndexOf(
"\"BUILD_NUMBER\":\"",
StringComparison.Ordinal
);
if (buildNumberIndex == -1)
return null;

var buildNumberStart = buildNumberIndex + "\"BUILD_NUMBER\":\"".Length;
var buildNumberEnd = responseBody.IndexOf(
"\"",
buildNumberStart,
StringComparison.Ordinal
);
if (buildNumberEnd == -1)
return null;

return responseBody.Substring(buildNumberStart, buildNumberEnd - buildNumberStart);
}
}

private async ValueTask<HttpResponseMessage> GetResponseAsync(
string url,
TokenKind tokenKind,
Expand All @@ -44,6 +137,22 @@ private async ValueTask<HttpResponseMessage> GetResponseAsync(
tokenKind == TokenKind.Bot ? $"Bot {token}" : token
);

if (tokenKind == TokenKind.User)
{
_xSuperPropertiesHeader ??= await GetXSuperPropertiesHeaderAsync(
innerCancellationToken
);
if (_xSuperPropertiesHeader != null)
{
request.Headers.TryAddWithoutValidation(
"X-Super-Properties",
_xSuperPropertiesHeader
);
}

request.Headers.UserAgent.ParseAdd(UserAgent);
}

var response = await Http.Client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead,
Expand Down
Loading