Refactor Games, support reading Rich Presences (#877)

* Add API-level support for Rich Presences

* Add library-level support for Game presences

* Add model conversions for outgoing+incoming rich presences

* Refactored Game into Activities

* Integrated Activities with user entities

rebase hell from 5f3cb947a92f4fd01cc4df47ca548180036b47f3

* Fix JSON converters for Activities

* Finish rebase, activity should be set on BaseSocketClient

* Use ApplicationId to define a rich presence

* Added SetActivityAsync to Base and Sharded Socket clients

* Remove public parameterless Game constructor

* Remove GameAssets, refactored to GameAsset

* Hide constructors for types that should be read-only

* Revert changes to Discord.Net.sln

got damned visual studio caching

* Refactor GameParty to use dedicated current/capacity values

Per feedback from @khionu
This commit is contained in:
Christopher F
2017-12-23 14:58:35 -05:00
committed by GitHub
parent 678a7238e6
commit 34b4e5a6d2
28 changed files with 376 additions and 58 deletions

View File

@@ -13,7 +13,7 @@ namespace Discord.WebSocket
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary>
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<IGuild> guilds);
/// <inheritdoc />

View File

@@ -22,7 +22,7 @@ namespace Discord.WebSocket
/// <summary> Gets the estimated round-trip latency, in milliseconds, to the gateway server. </summary>
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<SocketGuild> 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)

View File

@@ -48,7 +48,7 @@ namespace Discord.WebSocket
/// <inheritdoc />
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,

View File

@@ -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;
}

View File

@@ -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)

View File

@@ -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());
}
}
}