Skip to content

Commit

Permalink
feat: clyde
Browse files Browse the repository at this point in the history
feat: clyde endpoints
feat: personality generation
docs: mark features as experimental
  • Loading branch information
Lulalaby committed Nov 9, 2023
1 parent 5602c8c commit adedbe3
Show file tree
Hide file tree
Showing 9 changed files with 452 additions and 18 deletions.
5 changes: 4 additions & 1 deletion DisCatSharp.Attributes/Features.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ public enum Features : long
ServerSubscription = 1<<6,

[FeatureDescription("Requires that the application has monetization enabled.")]
MonetizedApplication = 1<<7
MonetizedApplication = 1<<7,

[FeatureDescription("Requires that the user and/or guild has a specific experiment and/or treatment.")]
Experiment = 1<<8,
}


Expand Down
15 changes: 0 additions & 15 deletions DisCatSharp.Experimental/DisCatSharp.cs

This file was deleted.

98 changes: 98 additions & 0 deletions DisCatSharp.Experimental/DisCatSharpExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using DisCatSharp.Attributes;
using DisCatSharp.Entities;
using DisCatSharp.Experimental.Entities;

namespace DisCatSharp.Experimental;

/// <summary>
/// Represents experimental extension methods for DisCatSharp.
/// </summary>
public static class DisCatSharpExtensions
{
[Experimental("This function is being tested and might change at any time."), RequiresFeature(Features.MonetizedApplication)]
public static async Task<string> GetUsernameAsync(this DiscordClient client, ulong id)
{
var user = await client.ApiClient.GetUserAsync(id);
return user.UsernameWithDiscriminator;
}

/// <summary>
/// Gets the clyde profile for the given <paramref name="profileId"/>.
/// </summary>
/// <param name="client">The discord client.</param>
/// <param name="profileId">The profile id to get.</param>
[RequiresFeature(Features.Override, "This method requires the guild and/or user to have access to clyde with treatment 5.")] // TODO: Change to Features.Experiment
public static async Task<ClydeProfile> GetClydeProfileAsync(this DiscordClient client, ulong profileId)
{
DiscordApiClientHook hook = new(client.ApiClient);
return await hook.GetClydeProfileAsync(profileId);
}

/// <summary>
/// Gets the clyde settings for the given <paramref name="guild"/>.
/// </summary>
/// <param name="guild">The guild to get clyde's settings for.</param>
[RequiresFeature(Features.Override, "This method requires the guild and/or user to have access to clyde with treatment 5.")] // TODO: Change to Features.Experiment
public static async Task<ClydeSettings> GetClydeSettingsAsync(this DiscordGuild guild)
{
DiscordApiClientHook hook = new(guild.Discord.ApiClient);
return await hook.GetClydeSettingsAsync(guild.Id);
}

/// <summary>
/// Modifies the clyde settings for the given <paramref name="guild"/> by applying a <paramref name="profileId"/>.
/// </summary>
/// <param name="guild">The guild to modify clyde's settings for.</param>
/// <param name="profileId">The profile id to apply.</param>
[RequiresFeature(Features.Override, "This method requires the guild and/or user to have access to clyde with treatment 5.")] // TODO: Change to Features.Experiment
public static async Task<ClydeSettings> ModifyClydeSettingsAsync(this DiscordGuild guild, ulong profileId)
{
DiscordApiClientHook hook = new(guild.Discord.ApiClient);
return await hook.ModifyClydeSettingsAsync(guild.Id, profileId);
}

/// <summary>
/// Modifies the clyde settings for the given <paramref name="guild"/>.
/// </summary>
/// <param name="guild">The guild to modify clyde's settings for.</param>
/// <param name="name">The new name.</param>
/// <param name="personality">The new basePersonality.</param>
/// <param name="avatar">The new avatar.</param>
/// <param name="banner">The new banner.</param>
/// <param name="themeColors">The new theme colors.</param>
[RequiresFeature(Features.Override, "This method requires the guild and/or user to have access to clyde with treatment 5.")] // TODO: Change to Features.Experiment
public static async Task<ClydeSettings> ModifyClydeSettingsAsync(
this DiscordGuild guild,
Optional<string?> name,
Optional<string> personality,
Optional<Stream?> avatar,
Optional<Stream?> banner,
Optional<List<DiscordColor>?> themeColors
)
{
DiscordApiClientHook hook = new(guild.Discord.ApiClient);

return await hook.ModifyClydeSettingsAsync(guild.Id, name, personality, ImageTool.Base64FromStream(avatar), ImageTool.Base64FromStream(banner), themeColors.HasValue && themeColors.Value.Any()
? themeColors.Value.Select(x => x.Value).ToList()
: themeColors.HasValue
? Optional.FromNullable<List<int>?>(null)
: Optional.None);
}

/// <summary>
/// Generates a basePersonality for clyde based on the given <paramref name="basePersonality"/>.
/// </summary>
/// <param name="client">The discord client.</param>
/// <param name="basePersonality">The base base personality to generate a new one from.</param>
[RequiresFeature(Features.Override, "This method requires the guild and/or user to have access to clyde with treatment 5.")] // TODO: Change to Features.Experiment
public static async Task<string> GenerateClydePersonalityAsync(this DiscordClient client, string? basePersonality = null)
{
DiscordApiClientHook hook = new(client.ApiClient);
return await hook.GenerateClydePersonalityAsync(basePersonality);
}
}
153 changes: 151 additions & 2 deletions DisCatSharp.Experimental/DiscordApiClientHook.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,162 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

using DisCatSharp.Entities;
using DisCatSharp.Experimental.Entities;
using DisCatSharp.Experimental.Payloads;
using DisCatSharp.Net;
using DisCatSharp.Net.Serialization;

using Newtonsoft.Json.Linq;

namespace DisCatSharp.Experimental;

internal class DiscordApiClientHook
/// <summary>
/// Represents a hook for the discord api client.
/// </summary>
internal sealed class DiscordApiClientHook
{
/// <summary>
/// Gets the api client.
/// </summary>
internal DiscordApiClient ApiClient { get; set; }

public DiscordApiClientHook(DiscordApiClient apiClient)
/// <summary>
/// Initializes a new instance of the <see cref="DiscordApiClientHook"/> class.
/// </summary>
/// <param name="apiClient">The api client.</param>
internal DiscordApiClientHook(DiscordApiClient apiClient)
{
this.ApiClient = apiClient;
}

/// <summary>
/// Gets the clyde profile for the given <paramref name="profileId"/>.
/// </summary>
/// <param name="profileId">The profile id to get.</param>
internal async Task<ClydeProfile> GetClydeProfileAsync(ulong profileId)
{
var route = $"{Endpoints.CLYDE_PROFILES}/:profile_id";
var bucket = this.ApiClient.Rest.GetBucket(RestRequestMethod.GET, route, new
{
profile_id = profileId
}, out var path);

var url = Utilities.GetApiUriFor(path, this.ApiClient.Discord.Configuration);
var res = await this.ApiClient.DoRequestAsync(this.ApiClient.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);

var profile = DiscordJson.DeserializeObject<ClydeProfile>(res.Response, this.ApiClient.Discord);
return profile;
}

/// <summary>
/// Gets the clyde settings for the given <paramref name="guildId"/>.
/// </summary>
/// <param name="guildId">The guild id to get clyde's settings for.</param>
internal async Task<ClydeSettings> GetClydeSettingsAsync(ulong guildId)
{
var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CLYDE_SETTINGS}";
var bucket = this.ApiClient.Rest.GetBucket(RestRequestMethod.GET, route, new
{
guild_id = guildId
}, out var path);

var url = Utilities.GetApiUriFor(path, this.ApiClient.Discord.Configuration);
var res = await this.ApiClient.DoRequestAsync(this.ApiClient.Discord, bucket, url, RestRequestMethod.GET, route).ConfigureAwait(false);

var settings = DiscordJson.DeserializeObject<ClydeSettings>(res.Response, this.ApiClient.Discord);
return settings;
}

/// <summary>
/// Modifies the clyde settings for the given <paramref name="guildId"/> by applying a <paramref name="profileId"/>.
/// </summary>
/// <param name="guildId">The guild id to modify clyde's settings for.</param>
/// <param name="profileId">The profile id to apply.</param>
internal async Task<ClydeSettings> ModifyClydeSettingsAsync(ulong guildId, ulong profileId)
{
ClydeSettingsProfileIdOnlyUpdatePayload pld = new()
{
ClydeProfileId = profileId
};

var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CLYDE_SETTINGS}";
var bucket = this.ApiClient.Rest.GetBucket(RestRequestMethod.PATCH, route, new
{
guild_id = guildId
}, out var path);

var url = Utilities.GetApiUriFor(path, this.ApiClient.Discord.Configuration);
var res = await this.ApiClient.DoRequestAsync(this.ApiClient.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);

var obj = JObject.Parse(res.Response);
var settingsString = obj.GetValue("settings")!.ToString();
var settings = DiscordJson.DeserializeObject<ClydeSettings>(settingsString, this.ApiClient.Discord);
return settings;
}

/// <summary>
/// Modifies the clyde settings for the given <paramref name="guildId"/>.
/// </summary>
/// <param name="guildId">The guild id to modify clyde's settings for.</param>
/// <param name="name">The new name.</param>
/// <param name="personality">The new basePersonality.</param>
/// <param name="avatarBase64">The new avatar.</param>
/// <param name="bannerBase64">The new banner.</param>
/// <param name="themeColors">The new theme colors.</param>
internal async Task<ClydeSettings> ModifyClydeSettingsAsync(
ulong guildId,
Optional<string?> name,
Optional<string> personality,
Optional<string?> avatarBase64,
Optional<string?> bannerBase64,
Optional<List<int>?> themeColors
)
{
ClydeSettingsProfileUpdatePayload pld = new()
{
Nick = name,
Personality = personality,
Avatar = avatarBase64,
Banner = bannerBase64,
ThemeColors = themeColors
};

var route = $"{Endpoints.GUILDS}/:guild_id{Endpoints.CLYDE_SETTINGS}";
var bucket = this.ApiClient.Rest.GetBucket(RestRequestMethod.PATCH, route, new
{
guild_id = guildId
}, out var path);

var url = Utilities.GetApiUriFor(path, this.ApiClient.Discord.Configuration);
var res = await this.ApiClient.DoRequestAsync(this.ApiClient.Discord, bucket, url, RestRequestMethod.PATCH, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);

var obj = JObject.Parse(res.Response);
var settingsString = obj.GetValue("settings")!.ToString();
var settings = DiscordJson.DeserializeObject<ClydeSettings>(settingsString, this.ApiClient.Discord);
return settings;
}

/// <summary>
/// Generates a basePersonality for clyde based on the given <paramref name="basePersonality"/>.
/// </summary>
/// <param name="basePersonality">The base base personality to generate a new one from.</param>
internal async Task<string> GenerateClydePersonalityAsync(string? basePersonality = null)
{
PersonalityGenerationPayload pld = new()
{
Personality = basePersonality ?? string.Empty
};

var route = $"{Endpoints.CLYDE_PROFILES}{Endpoints.GENERATE_PERSONALITY}";
var bucket = this.ApiClient.Rest.GetBucket(RestRequestMethod.POST, route, new
{ }, out var path);

var url = Utilities.GetApiUriFor(path, this.ApiClient.Discord.Configuration);
var res = await this.ApiClient.DoRequestAsync(this.ApiClient.Discord, bucket, url, RestRequestMethod.POST, route, payload: DiscordJson.SerializeObject(pld)).ConfigureAwait(false);

var generatedPersonality = DiscordJson.DeserializeObject<PersonalityGenerationPayload>(res.Response, this.ApiClient.Discord);
return generatedPersonality.Personality;
}
}
73 changes: 73 additions & 0 deletions DisCatSharp.Experimental/Entities/Clyde/ClydeProfile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

using DisCatSharp.Entities;
using DisCatSharp.Enums;
using DisCatSharp.Net;

using Newtonsoft.Json;

namespace DisCatSharp.Experimental.Entities;

/// <summary>
/// Represents a clyde profile.
/// </summary>
public sealed class ClydeProfile : ObservableApiObject
{
/// <summary>
/// Gets clyde's profile name.
/// </summary>
[JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
public string? Name { get; internal set; }

/// <summary>
/// Gets clyde's profile personality.
/// </summary>
[JsonProperty("personality", NullValueHandling = NullValueHandling.Ignore)]
public string Personality { get; internal set; }

/// <summary>
/// Gets clyde's profile avatar hash.
/// </summary>
[JsonProperty("avatar_hash", NullValueHandling = NullValueHandling.Ignore)]
public string? AvatarHash { get; internal set; }

/// <summary>
/// Gets clyde's profile banner hash.
/// </summary>
[JsonProperty("banner_hash", NullValueHandling = NullValueHandling.Ignore)]
public string? BannerHash { get; internal set; }

/// <summary>
/// Gets clyde's profile bio.
/// </summary>
[JsonProperty("bio", NullValueHandling = NullValueHandling.Ignore)]
public string Bio { get; internal set; }

/// <summary>
/// Gets the user who authored this clyde profile.
/// </summary>
[JsonProperty("author_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong AuthorId { get; internal set; }

/// <summary>
/// Gets clyde's profile theme color ints.
/// </summary>
[JsonProperty("theme_colors", NullValueHandling = NullValueHandling.Ignore)]
internal List<int>? ThemeColorsInternal { get; set; }

/// <summary>
/// Gets the clyde's profile theme colors, if set.
/// </summary>
[JsonIgnore]
public IReadOnlyList<DiscordColor>? ThemeColors
=> !(this.ThemeColorsInternal is not null && this.ThemeColorsInternal.Count != 0) ? null : this.ThemeColorsInternal.Select(x => new DiscordColor(x)).ToList();

/// <summary>
/// Gets clyde's profile id.
/// </summary>
[JsonProperty("clyde_profile_id", NullValueHandling = NullValueHandling.Ignore)]
public ulong ClydeProfileId { get; internal set; }
}
Loading

0 comments on commit adedbe3

Please sign in to comment.