diff --git a/src/Discord.Net.Core/CDN.cs b/src/Discord.Net.Core/CDN.cs index d3ade3722..415c0c30d 100644 --- a/src/Discord.Net.Core/CDN.cs +++ b/src/Discord.Net.Core/CDN.cs @@ -22,6 +22,12 @@ namespace Discord public static string GetEmojiUrl(ulong emojiId) => $"{DiscordConfig.CDNUrl}emojis/{emojiId}.png"; + public static string GetRichAssetUrl(ulong appId, string assetId, ushort size, ImageFormat format) + { + string extension = FormatToExtension(format, ""); + return $"{DiscordConfig.CDNUrl}app-assets/{appId}/{assetId}.{extension}?size={size}"; + } + private static string FormatToExtension(ImageFormat format, string imageId) { if (format == ImageFormat.Auto) diff --git a/src/Discord.Net.Core/Entities/Activities/Game.cs b/src/Discord.Net.Core/Entities/Activities/Game.cs new file mode 100644 index 000000000..f2b7e8eb6 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/Game.cs @@ -0,0 +1,19 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class Game : IActivity + { + public string Name { get; internal set; } + + internal Game() { } + public Game(string name) + { + Name = name; + } + + public override string ToString() => Name; + private string DebuggerDisplay => Name; + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/GameAsset.cs b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs new file mode 100644 index 000000000..385f37214 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameAsset.cs @@ -0,0 +1,15 @@ +namespace Discord +{ + public class GameAsset + { + internal GameAsset() { } + + internal ulong ApplicationId { get; set; } + + public string Text { get; internal set; } + public string ImageId { get; internal set; } + + public string GetImageUrl(ImageFormat format = ImageFormat.Auto, ushort size = 128) + => CDN.GetRichAssetUrl(ApplicationId, ImageId, size, format); + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameParty.cs b/src/Discord.Net.Core/Entities/Activities/GameParty.cs new file mode 100644 index 000000000..dbfe5b6ce --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameParty.cs @@ -0,0 +1,11 @@ +namespace Discord +{ + public class GameParty + { + internal GameParty() { } + + public string Id { get; internal set; } + public int Members { get; internal set; } + public int Capacity { get; internal set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs new file mode 100644 index 000000000..e9d988ba9 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameSecrets.cs @@ -0,0 +1,16 @@ +namespace Discord +{ + public class GameSecrets + { + public string Match { get; } + public string Join { get; } + public string Spectate { get; } + + internal GameSecrets(string match, string join, string spectate) + { + Match = match; + Join = join; + Spectate = spectate; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs new file mode 100644 index 000000000..8c8c992fa --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/GameTimestamps.cs @@ -0,0 +1,16 @@ +using System; + +namespace Discord +{ + public class GameTimestamps + { + public DateTimeOffset? Start { get; } + public DateTimeOffset? End { get; } + + internal GameTimestamps(DateTimeOffset? start, DateTimeOffset? end) + { + Start = start; + End = end; + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/IActivity.cs b/src/Discord.Net.Core/Entities/Activities/IActivity.cs new file mode 100644 index 000000000..0dcf34273 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/IActivity.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Discord +{ + public interface IActivity + { + string Name { get; } + } +} diff --git a/src/Discord.Net.Core/Entities/Activities/RichGame.cs b/src/Discord.Net.Core/Entities/Activities/RichGame.cs new file mode 100644 index 000000000..e66eac1d2 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/RichGame.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class RichGame : Game + { + internal RichGame() { } + + public string Details { get; internal set;} + public string State { get; internal set;} + public ulong ApplicationId { get; internal set; } + public GameAsset SmallAsset { get; internal set; } + public GameAsset LargeAsset { get; internal set; } + public GameParty Party { get; internal set; } + public GameSecrets Secrets { get; internal set; } + public GameTimestamps Timestamps { get; internal set; } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} (Rich)"; + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs new file mode 100644 index 000000000..140024272 --- /dev/null +++ b/src/Discord.Net.Core/Entities/Activities/StreamingGame.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace Discord +{ + [DebuggerDisplay(@"{DebuggerDisplay,nq}")] + public class StreamingGame : Game + { + public string Url { get; internal set; } + public StreamType StreamType { get; internal set; } + + public StreamingGame(string name, string url, StreamType streamType) + { + Name = name; + Url = url; + StreamType = streamType; + } + + public override string ToString() => Name; + private string DebuggerDisplay => $"{Name} ({Url})"; + } +} \ No newline at end of file diff --git a/src/Discord.Net.Core/Entities/Users/Game.cs b/src/Discord.Net.Core/Entities/Users/Game.cs deleted file mode 100644 index 3405b0dd4..000000000 --- a/src/Discord.Net.Core/Entities/Users/Game.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Diagnostics; - -namespace Discord -{ - [DebuggerDisplay(@"{DebuggerDisplay,nq}")] - public struct Game - { - public string Name { get; } - public string StreamUrl { get; } - public StreamType StreamType { get; } - - public Game(string name, string streamUrl, StreamType type) - { - Name = name; - StreamUrl = streamUrl; - StreamType = type; - } - private Game(string name) - : this(name, null, StreamType.NotStreaming) { } - - public override string ToString() => Name; - private string DebuggerDisplay => StreamUrl != null ? $"{Name} ({StreamUrl})" : Name; - } -} diff --git a/src/Discord.Net.Core/Entities/Users/IPresence.cs b/src/Discord.Net.Core/Entities/Users/IPresence.cs index 7f182241b..25adcc9c4 100644 --- a/src/Discord.Net.Core/Entities/Users/IPresence.cs +++ b/src/Discord.Net.Core/Entities/Users/IPresence.cs @@ -2,8 +2,8 @@ { public interface IPresence { - /// Gets the game this user is currently playing, if any. - Game? Game { get; } + /// Gets the activity this user is currently doing. + IActivity Activity { get; } /// Gets the current status of this user. UserStatus Status { get; } } diff --git a/src/Discord.Net.Rest/API/Common/Game.cs b/src/Discord.Net.Rest/API/Common/Game.cs index a499d83b0..bfb861692 100644 --- a/src/Discord.Net.Rest/API/Common/Game.cs +++ b/src/Discord.Net.Rest/API/Common/Game.cs @@ -13,6 +13,22 @@ namespace Discord.API public Optional StreamUrl { get; set; } [JsonProperty("type")] public Optional StreamType { get; set; } + [JsonProperty("details")] + public Optional Details { get; set; } + [JsonProperty("state")] + public Optional State { get; set; } + [JsonProperty("application_id")] + public Optional ApplicationId { get; set; } + [JsonProperty("assets")] + public Optional Assets { get; set; } + [JsonProperty("party")] + public Optional Party { get; set; } + [JsonProperty("secrets")] + public Optional Secrets { get; set; } + [JsonProperty("timestamps")] + public Optional Timestamps { get; set; } + [JsonProperty("instance")] + public Optional Instance { get; set; } [OnError] internal void OnError(StreamingContext context, ErrorContext errorContext) diff --git a/src/Discord.Net.Rest/API/Common/GameAssets.cs b/src/Discord.Net.Rest/API/Common/GameAssets.cs new file mode 100644 index 000000000..b5928a8ab --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameAssets.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameAssets + { + [JsonProperty("small_text")] + public Optional SmallText { get; set; } + [JsonProperty("small_image")] + public Optional SmallImage { get; set; } + [JsonProperty("large_image")] + public Optional LargeText { get; set; } + [JsonProperty("large_text")] + public Optional LargeImage { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameParty.cs b/src/Discord.Net.Rest/API/Common/GameParty.cs new file mode 100644 index 000000000..e0da4a098 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameParty.cs @@ -0,0 +1,12 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameParty + { + [JsonProperty("id")] + public string Id { get; set; } + [JsonProperty("size")] + public int[] Size { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameSecrets.cs b/src/Discord.Net.Rest/API/Common/GameSecrets.cs new file mode 100644 index 000000000..e70b48ff0 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameSecrets.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameSecrets + { + [JsonProperty("match")] + public string Match { get; set; } + [JsonProperty("join")] + public string Join { get; set; } + [JsonProperty("spectate")] + public string Spectate { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/Common/GameTimestamps.cs b/src/Discord.Net.Rest/API/Common/GameTimestamps.cs new file mode 100644 index 000000000..5c6f10b86 --- /dev/null +++ b/src/Discord.Net.Rest/API/Common/GameTimestamps.cs @@ -0,0 +1,15 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.API +{ + internal class GameTimestamps + { + [JsonProperty("start")] + [UnixTimestamp] + public Optional Start { get; set; } + [JsonProperty("end")] + [UnixTimestamp] + public Optional End { get; set; } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs b/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs new file mode 100644 index 000000000..3890ffc46 --- /dev/null +++ b/src/Discord.Net.Rest/API/UnixTimestampAttribute.cs @@ -0,0 +1,7 @@ +using System; + +namespace Discord.API +{ + [AttributeUsage(AttributeTargets.Property)] + internal class UnixTimestampAttribute : Attribute { } +} \ No newline at end of file diff --git a/src/Discord.Net.Rest/Discord.Net.Rest.csproj b/src/Discord.Net.Rest/Discord.Net.Rest.csproj index 439b7bbb1..29f79e410 100644 --- a/src/Discord.Net.Rest/Discord.Net.Rest.csproj +++ b/src/Discord.Net.Rest/Discord.Net.Rest.csproj @@ -10,7 +10,8 @@ - + + diff --git a/src/Discord.Net.Rest/Entities/Users/RestUser.cs b/src/Discord.Net.Rest/Entities/Users/RestUser.cs index d8ade3a6b..c6cf6103a 100644 --- a/src/Discord.Net.Rest/Entities/Users/RestUser.cs +++ b/src/Discord.Net.Rest/Entities/Users/RestUser.cs @@ -16,7 +16,7 @@ namespace Discord.Rest public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public virtual Game? Game => null; + public virtual IActivity Activity => null; public virtual UserStatus Status => UserStatus.Offline; public virtual bool IsWebhook => false; diff --git a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs index b465fbed2..9213c5d75 100644 --- a/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs +++ b/src/Discord.Net.Rest/Net/Converters/DiscordContractResolver.cs @@ -66,6 +66,12 @@ namespace Discord.Net.Converters if (type == typeof(ulong)) return UInt64Converter.Instance; } + bool hasUnixStamp = propInfo.GetCustomAttribute() != null; + if (hasUnixStamp) + { + if (type == typeof(DateTimeOffset)) + return UnixTimestampConverter.Instance; + } //Enums if (type == typeof(PermissionTarget)) diff --git a/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs new file mode 100644 index 000000000..d4660dc44 --- /dev/null +++ b/src/Discord.Net.Rest/Net/Converters/UnixTimestampConverter.cs @@ -0,0 +1,28 @@ +using System; +using Newtonsoft.Json; + +namespace Discord.Net.Converters +{ + public class UnixTimestampConverter : JsonConverter + { + public static readonly UnixTimestampConverter Instance = new UnixTimestampConverter(); + + public override bool CanConvert(Type objectType) => true; + public override bool CanRead => true; + public override bool CanWrite => true; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Discord doesn't validate if timestamps contain decimals or not + if (reader.Value is double d) + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(d); + long offset = (long)reader.Value; + return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero).AddMilliseconds(offset); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs index c6b0b2fd8..f55c83b75 100644 --- a/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs +++ b/src/Discord.Net.Rpc/Entities/Users/RpcUser.cs @@ -18,7 +18,7 @@ namespace Discord.Rpc public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); public virtual bool IsWebhook => false; - public virtual Game? Game => null; + public virtual IActivity Activity => null; public virtual UserStatus Status => UserStatus.Offline; internal RpcUser(DiscordRpcClient discord, ulong id) diff --git a/src/Discord.Net.WebSocket/BaseSocketClient.cs b/src/Discord.Net.WebSocket/BaseSocketClient.cs index d248285cd..2ab244aeb 100644 --- a/src/Discord.Net.WebSocket/BaseSocketClient.cs +++ b/src/Discord.Net.WebSocket/BaseSocketClient.cs @@ -13,7 +13,7 @@ namespace Discord.WebSocket /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public abstract int Latency { get; protected set; } public abstract UserStatus Status { get; protected set; } - public abstract Game? Game { get; protected set; } + public abstract IActivity Activity { get; protected set; } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; @@ -45,6 +45,7 @@ namespace Discord.WebSocket public abstract Task StopAsync(); public abstract Task SetStatusAsync(UserStatus status); public abstract Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming); + public abstract Task SetActivityAsync(IActivity activity); public abstract Task DownloadUsersAsync(IEnumerable guilds); /// diff --git a/src/Discord.Net.WebSocket/DiscordShardedClient.cs b/src/Discord.Net.WebSocket/DiscordShardedClient.cs index 6c2a0f3b9..e827909d9 100644 --- a/src/Discord.Net.WebSocket/DiscordShardedClient.cs +++ b/src/Discord.Net.WebSocket/DiscordShardedClient.cs @@ -22,7 +22,7 @@ namespace Discord.WebSocket /// Gets the estimated round-trip latency, in milliseconds, to the gateway server. public override int Latency { get => GetLatency(); protected set { } } public override UserStatus Status { get => _shards[0].Status; protected set { } } - public override Game? Game { get => _shards[0].Game; protected set { } } + public override IActivity Activity { get => _shards[0].Activity; protected set { } } internal new DiscordSocketApiClient ApiClient => base.ApiClient as DiscordSocketApiClient; public override IReadOnlyCollection Guilds => GetGuilds().ToReadOnlyCollection(() => GetGuildCount()); @@ -239,9 +239,18 @@ namespace Discord.WebSocket await _shards[i].SetStatusAsync(status).ConfigureAwait(false); } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) + { + IActivity activity = null; + if (streamUrl != null) + activity = new StreamingGame(name, streamUrl, streamType); + else if (name != null) + activity = new Game(name); + await SetActivityAsync(activity).ConfigureAwait(false); + } + public override async Task SetActivityAsync(IActivity activity) { for (int i = 0; i < _shards.Length; i++) - await _shards[i].SetGameAsync(name, streamUrl, streamType).ConfigureAwait(false); + await _shards[i].SetActivityAsync(activity).ConfigureAwait(false); } private void RegisterEvents(DiscordSocketClient client, bool isPrimary) diff --git a/src/Discord.Net.WebSocket/DiscordSocketClient.cs b/src/Discord.Net.WebSocket/DiscordSocketClient.cs index d152bbc03..35a22edc4 100644 --- a/src/Discord.Net.WebSocket/DiscordSocketClient.cs +++ b/src/Discord.Net.WebSocket/DiscordSocketClient.cs @@ -48,7 +48,7 @@ namespace Discord.WebSocket /// public override int Latency { get; protected set; } public override UserStatus Status { get; protected set; } = UserStatus.Online; - public override Game? Game { get; protected set; } + public override IActivity Activity { get; protected set; } //From DiscordSocketConfig internal int TotalShards { get; private set; } @@ -328,33 +328,39 @@ namespace Discord.WebSocket } public override async Task SetGameAsync(string name, string streamUrl = null, StreamType streamType = StreamType.NotStreaming) { - if (name != null) - Game = new Game(name, streamUrl, streamType); + if (streamUrl != null) + Activity = new StreamingGame(name, streamUrl, streamType); + else if (name != null) + Activity = new Game(name); else - Game = null; + Activity = null; await SendStatusAsync().ConfigureAwait(false); } + public override async Task SetActivityAsync(IActivity activity) + { + Activity = activity; + await SendStatusAsync().ConfigureAwait(false); + } + private async Task SendStatusAsync() { if (CurrentUser == null) return; - var game = Game; + var activity = Activity; var status = Status; var statusSince = _statusSince; - CurrentUser.Presence = new SocketPresence(status, game); + CurrentUser.Presence = new SocketPresence(status, activity); - GameModel gameModel; - if (game != null) + var gameModel = new GameModel(); + // Discord only accepts rich presence over RPC, don't even bother building a payload + if (activity is RichGame game) throw new NotSupportedException("Outgoing Rich Presences are not supported"); + if (activity is StreamingGame stream) { - gameModel = new API.Game - { - Name = game.Value.Name, - StreamType = game.Value.StreamType, - StreamUrl = game.Value.StreamUrl - }; + gameModel.StreamUrl = stream.Url; + gameModel.StreamType = stream.StreamType; } - else - gameModel = null; + else if (activity != null) + gameModel.Name = activity.Name; await ApiClient.SendStatusUpdateAsync( status, diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs index 00d4b4bbc..7d7ba16ce 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketPresence.cs @@ -8,20 +8,20 @@ namespace Discord.WebSocket public struct SocketPresence : IPresence { public UserStatus Status { get; } - public Game? Game { get; } + public IActivity Activity { get; } - internal SocketPresence(UserStatus status, Game? game) + internal SocketPresence(UserStatus status, IActivity activity) { Status = status; - Game = game; + Activity= activity; } internal static SocketPresence Create(Model model) { - return new SocketPresence(model.Status, model.Game != null ? model.Game.ToEntity() : (Game?)null); + return new SocketPresence(model.Status, model.Game?.ToEntity()); } public override string ToString() => Status.ToString(); - private string DebuggerDisplay => $"{Status}{(Game != null ? $", {Game.Value.Name} ({Game.Value.StreamType})" : "")}"; + private string DebuggerDisplay => $"{Status}{(Activity != null ? $", {Activity.Name}": "")}"; internal SocketPresence Clone() => this; } diff --git a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs index a0c78b93f..58d5c62a1 100644 --- a/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs +++ b/src/Discord.Net.WebSocket/Entities/Users/SocketUser.cs @@ -18,7 +18,7 @@ namespace Discord.WebSocket public DateTimeOffset CreatedAt => SnowflakeUtils.FromSnowflake(Id); public string Discriminator => DiscriminatorValue.ToString("D4"); public string Mention => MentionUtils.MentionUser(Id); - public Game? Game => Presence.Game; + public IActivity Activity => Presence.Activity; public UserStatus Status => Presence.Status; internal SocketUser(DiscordSocketClient discord, ulong id) diff --git a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs index 636ef68f4..4aff13753 100644 --- a/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs +++ b/src/Discord.Net.WebSocket/Extensions/EntityExtensions.cs @@ -2,11 +2,83 @@ { internal static class EntityExtensions { - public static Game ToEntity(this API.Game model) + public static IActivity ToEntity(this API.Game model) { - return new Game(model.Name, - model.StreamUrl.GetValueOrDefault(null), - model.StreamType.GetValueOrDefault(null) ?? StreamType.NotStreaming); + // Rich Game + if (model.ApplicationId.IsSpecified) + { + ulong appId = model.ApplicationId.Value; + var assets = model.Assets.GetValueOrDefault()?.ToEntity(appId); + return new RichGame + { + ApplicationId = appId, + Name = model.Name, + Details = model.Details.GetValueOrDefault(), + State = model.State.GetValueOrDefault(), + SmallAsset = assets?[0], + LargeAsset = assets?[1], + Party = model.Party.GetValueOrDefault()?.ToEntity(), + Secrets = model.Secrets.GetValueOrDefault()?.ToEntity(), + Timestamps = model.Timestamps.GetValueOrDefault()?.ToEntity() + }; + } + // Stream Game + if (model.StreamUrl.IsSpecified) + { + return new StreamingGame( + model.Name, + model.StreamUrl.Value, + model.StreamType.Value.GetValueOrDefault()); + } + // Normal Game + return new Game(model.Name); + } + + // (Small, Large) + public static GameAsset[] ToEntity(this API.GameAssets model, ulong appId) + { + return new GameAsset[] + { + model.SmallImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.SmallImage.GetValueOrDefault(), + Text = model.SmallText.GetValueOrDefault() + } : null, + model.LargeImage.IsSpecified ? new GameAsset + { + ApplicationId = appId, + ImageId = model.LargeImage.GetValueOrDefault(), + Text = model.LargeText.GetValueOrDefault() + } : null, + }; + } + + public static GameParty ToEntity(this API.GameParty model) + { + // Discord will probably send bad data since they don't validate anything + int current = 0, cap = 0; + if (model.Size.Length == 2) + { + current = model.Size[0]; + cap = model.Size[1]; + } + return new GameParty + { + Id = model.Id, + Members = current, + Capacity = cap, + }; + } + + public static GameSecrets ToEntity(this API.GameSecrets model) + { + return new GameSecrets(model.Match, model.Join, model.Spectate); + } + + public static GameTimestamps ToEntity(this API.GameTimestamps model) + { + return new GameTimestamps(model.Start.ToNullable(), model.End.ToNullable()); } } }