Skip to content
Open
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
136 changes: 136 additions & 0 deletions MiniTwitch.Helix.Test/DataSources/PinnedMessageDataSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Collections;
using MiniTwitch.Helix.Enums;
using MiniTwitch.Helix.Internal.Json;
using MiniTwitch.Helix.Responses;

namespace MiniTwitch.Helix.Test.DataSources;

public class PinnedMessageDataSource : IEnumerable<object[]>
{
private static readonly PinnedChatMessages.Pin Base = new(
MessageId: "a42a84b2-7ad7-4ac1-95bb-0843d70e005a",
BroadcasterId: "46673989",
SenderUserId: "77481472",
SenderUserLogin: "jpsauce",
SenderUserDisplayName: "JPSauce",
PinnedByUserId: "82674227",
PinnedByUserLogin: "jammehge",
PinnedByUserDisplayName: "JAMMEHGE",
Message: new PinnedChatMessages.Message(
Text: "fix it! now!!!",
Fragments: [
new PinnedChatMessages.MessageFragment(
Type: MessageFragmentType.Text,
Text: "fix it! now!!!",
Cheermote: null,
Emote: null,
Mention: null
)
]
),
StartsAt: new DateTimeOffset(2026, 05, 16, 23, 10, 16, TimeSpan.Zero),
EndsAt: new DateTimeOffset(2026, 01, 02, 03, 04, 55, TimeSpan.Zero),
UpdatedAt: new DateTimeOffset(2026, 01, 02, 03, 04, 55, TimeSpan.Zero)
);

private static string JsonFromModel(PinnedChatMessages.Pin pin) =>
$$"""
{
"data": [
{
"broadcaster_id": "{{pin.BroadcasterId}}",
"ends_at": {{(pin.EndsAt is {} ea ? $"\"{ea:yyyy-MM-ddTHH:mm:sszzz}\"" : "null")}},
"message": {
"fragments": [
{{string.Join(", ", pin.Message.Fragments.Select(BuildFragment))}}
],
"text": "{{pin.Message.Text}}"
},
"message_id": "{{pin.MessageId}}",
"pinned_by_user_id": "{{pin.PinnedByUserId}}",
"pinned_by_user_login": "{{pin.PinnedByUserLogin}}",
"pinned_by_user_name": "{{pin.PinnedByUserDisplayName}}",
"sender_user_id": "{{pin.SenderUserId}}",
"sender_user_login": "{{pin.SenderUserLogin}}",
"sender_user_name": "{{pin.SenderUserDisplayName}}",
"starts_at": "{{pin.StartsAt:yyyy-MM-ddTHH:mm:sszzz}}",
"updated_at": "{{pin.UpdatedAt:yyyy-MM-ddTHH:mm:sszzz}}"
}
]
}
""";

private static string BuildFragment(PinnedChatMessages.MessageFragment fragment) =>
$$"""
{
"cheermote": {{(fragment.Cheermote is { } c ? $"{{\"prefix\": \"{c.Prefix}\", \"bits\": {c.Bits}, \"tier\": {c.Tier}}}" : "null")}},
"emote": {{(fragment.Emote is { } e ? $"{{\"id\": \"{e.Id}\", \"emote_set_id\": \"{e.EmoteSetId}\", \"owner_id\": \"{e.OwnerId}\", \"format\": [{string.Join(", ", e.Format.Select(f => $"\"{f}\""))}]}}" : "null")}},
"mention": {{(fragment.Mention is { } m ? $"{{\"user_id\": \"{m.UserId}\", \"user_login\": \"{m.UserLogin}\", \"user_name\": \"{m.UserDisplayName}\"}}" : "null")}},
"text": "{{fragment.Text}}",
"type": "{{SnakeCase.Instance.ConvertToCase(fragment.Type.ToString())}}"
}
""";

private static IEnumerable<PinnedChatMessages.Pin> Cases =>
[
Base with { EndsAt = null },
Base with
{
Message = Base.Message with
{
Fragments =
[
Base.Message.Fragments[0] with
{
Type = MessageFragmentType.Cheermote,
Cheermote = new PinnedChatMessages.CheermoteFragment("prefix_1", 1, 2),
Emote = null,
Mention = null
}
]
}
},
Base with
{
Message = Base.Message with
{
Fragments =
[
Base.Message.Fragments[0] with
{
Type = MessageFragmentType.Emote,
Cheermote = null,
Emote = new PinnedChatMessages.EmoteFragment("emoteId_1", "emoteSetId_1", "ownerId_1", ["format_1"]),
Mention = null
}
]
}
},
Base with
{
Message = Base.Message with
{
Fragments =
[
Base.Message.Fragments[0] with
{
Type = MessageFragmentType.Mention,
Cheermote = null,
Emote = null,
Mention = new PinnedChatMessages.MentionFragment("userId_1", "userLogin_1", "userDisplayName_1")
}
]
}
}
];

public IEnumerator<object[]> GetEnumerator() => Cases
.Select(@case => new object[]
{
JsonFromModel(@case),
new PinnedChatMessages { Data = [@case] }
})
.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
11 changes: 10 additions & 1 deletion MiniTwitch.Helix.Test/DeserializeTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using MiniTwitch.Helix.Internal;
using MiniTwitch.Helix.Requests;
using MiniTwitch.Helix.Responses;
using MiniTwitch.Helix.Test.DataSources;

namespace MiniTwitch.Helix.Test;

Expand Down Expand Up @@ -180,5 +181,13 @@ public void AddSuspiciousStatusToChatUser()
var asJson = JsonSerializer.Serialize(requestObject, options);
Assert.Equal(requestJson, asJson);
}
}

[Theory]
[ClassData(typeof(PinnedMessageDataSource))]
public void GetPinnedMessage(string jsonResponse, PinnedChatMessages expected)
{
var result = JsonSerializer.Deserialize<PinnedChatMessages>(jsonResponse, options);

Assert.Equivalent(expected, result);
}
}
9 changes: 9 additions & 0 deletions MiniTwitch.Helix/Enums/MessageFragmentType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace MiniTwitch.Helix.Enums;

public enum MessageFragmentType
{
Text,
Emote,
Cheermote,
Mention
}
72 changes: 71 additions & 1 deletion MiniTwitch.Helix/HelixWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2902,4 +2902,74 @@ public Task<HelixResult<SuspiciousUserInfo>> RemoveSuspiciousStatusFromChatUser(

return HelixResultFactory.Create<SuspiciousUserInfo>(Client, request, endpoint, cancellationToken);
}
}

/// <summary>
/// <see href="https://dev.twitch.tv/docs/api/reference#get-pinned-chat-message">API Reference</see>
/// </summary>
public Task<HelixResult<PinnedChatMessages>> GetPinnedChatMessage(
long broadcasterId,
CancellationToken cancellationToken = default)
{
HelixEndpoint endpoint = Endpoints.GetPinnedChatMessage;
RequestData request = new RequestData(_baseUrl, endpoint)
.AddParam(QueryParams.BroadcasterId, broadcasterId)
.AddParam(QueryParams.ModeratorId, this.UserId);

return HelixResultFactory.Create<PinnedChatMessages>(Client, request, endpoint, cancellationToken);
}

/// <summary>
/// <see href="https://dev.twitch.tv/docs/api/reference#pin-chat-message">API Reference</see>
/// </summary>
public Task<HelixResult> PinChatMessage(
long broadcasterId,
string messageId,
int? durationSeconds = null,
CancellationToken cancellationToken = default)
{
HelixEndpoint endpoint = Endpoints.PinChatMessage;
RequestData request = new RequestData(_baseUrl, endpoint)
.AddParam(QueryParams.BroadcasterId, broadcasterId)
.AddParam(QueryParams.ModeratorId, this.UserId)
.AddParam(QueryParams.MessageId, messageId)
.AddParam(QueryParams.DurationSeconds, durationSeconds);

return HelixResultFactory.Create(Client, request, endpoint, cancellationToken);
}

/// <summary>
/// <see href="https://dev.twitch.tv/docs/api/reference#update-pinned-chat-message">API Reference</see>
/// </summary>
public Task<HelixResult> UpdatePinChatMessage(
long broadcasterId,
string messageId,
int? durationSeconds = null,
CancellationToken cancellationToken = default)
{
HelixEndpoint endpoint = Endpoints.UpdatePinnedChatMessage;
RequestData request = new RequestData(_baseUrl, endpoint)
.AddParam(QueryParams.BroadcasterId, broadcasterId)
.AddParam(QueryParams.ModeratorId, this.UserId)
.AddParam(QueryParams.MessageId, messageId)
.AddParam(QueryParams.DurationSeconds, durationSeconds);

return HelixResultFactory.Create(Client, request, endpoint, cancellationToken);
}

/// <summary>
/// <see href="https://dev.twitch.tv/docs/api/reference#unpin-chat-message">API Reference</see>
/// </summary>
public Task<HelixResult> UnpinChatMessage(
long broadcasterId,
string messageId,
CancellationToken cancellationToken = default)
{
HelixEndpoint endpoint = Endpoints.UnpinChatMessage;
RequestData request = new RequestData(_baseUrl, endpoint)
.AddParam(QueryParams.BroadcasterId, broadcasterId)
.AddParam(QueryParams.ModeratorId, this.UserId)
.AddParam(QueryParams.MessageId, messageId);

return HelixResultFactory.Create(Client, request, endpoint, cancellationToken);
}
}
26 changes: 25 additions & 1 deletion MiniTwitch.Helix/Internal/Models/Endpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -897,4 +897,28 @@ internal static class Endpoints
Method = HttpMethod.Delete,
Route = "/moderation/suspicious_users"
};
}

public static readonly HelixEndpoint GetPinnedChatMessage = new()
{
Method = HttpMethod.Get,
Route = "/chat/pins"
};

public static readonly HelixEndpoint PinChatMessage = new()
{
Method = HttpMethod.Put,
Route = "/chat/pins"
};

public static readonly HelixEndpoint UpdatePinnedChatMessage = new()
{
Method = HttpMethod.Patch,
Route = "/chat/pins"
};

public static readonly HelixEndpoint UnpinChatMessage = new()
{
Method = HttpMethod.Delete,
Route = "/chat/pins"
};
}
3 changes: 2 additions & 1 deletion MiniTwitch.Helix/Internal/Models/QueryParams.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ internal static class QueryParams
public const string ClipId = "clip_id";
public const string Title = "title";
public const string Duration = "duration";
public const string DurationSeconds = "duration_seconds";
public const string VodId = "vod_id";
public const string VodOffset = "vod_offset";
}
}
5 changes: 4 additions & 1 deletion MiniTwitch.Helix/Requests/ChatMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace MiniTwitch.Helix.Requests;

public class ChatMessage(long broadcasterId, string message, string? replyParentMessageId = null, bool? forSourceOnly = null)
public class ChatMessage(long broadcasterId, string message, string? replyParentMessageId = null, bool? forSourceOnly = null, bool? shouldPin = null)
{
[JsonConverter(typeof(LongConverter))]
public long BroadcasterId { get; } = broadcasterId;
Expand All @@ -14,4 +14,7 @@ public class ChatMessage(long broadcasterId, string message, string? replyParent
/// This value is assigned automatically
/// </summary>
public long SenderId { get; internal set; }

[JsonPropertyName("pin")]
public bool? Pin { get; } = shouldPin;
}
86 changes: 86 additions & 0 deletions MiniTwitch.Helix/Responses/PinnedChatMessages.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
using System.Text.Json.Serialization;
using MiniTwitch.Helix.Enums;
using MiniTwitch.Helix.Internal.Json;
using MiniTwitch.Helix.Models;

namespace MiniTwitch.Helix.Responses;

public class PinnedChatMessages : BaseResponse<PinnedChatMessages.Pin>
{
public record Pin(
[property: JsonPropertyName("message_id")]
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deserializer has PropertyNamingPolicy = new SnakeCaseNamingPolicy(), you can get rid of most of these.

string MessageId,
[property: JsonPropertyName("broadcaster_id")]
string BroadcasterId,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user ids are treated as int64 across all of minitwitch, update user id props to be long for consistency

[property: JsonPropertyName("sender_user_id")]
string SenderUserId,
[property: JsonPropertyName("sender_user_login")]
string SenderUserLogin,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

user_login is Username across the library. Rename this to SenderUsername. Update PinnedByUserLogin and MentionFragment.UserLogin as well

[property: JsonPropertyName("sender_user_name")]
string SenderUserDisplayName,
[property: JsonPropertyName("pinned_by_user_id")]
string PinnedByUserId,
[property: JsonPropertyName("pinned_by_user_login")]
string PinnedByUserLogin,
[property: JsonPropertyName("pinned_by_user_name")]
string PinnedByUserDisplayName,
[property: JsonPropertyName("message")]
Message Message,
[property: JsonPropertyName("starts_at")]
DateTimeOffset StartsAt,
[property: JsonPropertyName("ends_at")]
DateTimeOffset? EndsAt,
[property: JsonPropertyName("updated_at")]
DateTimeOffset UpdatedAt
);

public record Message(
[property: JsonPropertyName("text")]
string Text,
[property: JsonPropertyName("fragments")]
IReadOnlyList<MessageFragment> Fragments
);

public record MessageFragment(
[property: JsonPropertyName("type")]
[property: JsonConverter(typeof(EnumConverter<MessageFragmentType>))]
MessageFragmentType Type,
[property: JsonPropertyName("text")]
string Text,
[property: JsonPropertyName("cheermote")]
CheermoteFragment? Cheermote,
[property: JsonPropertyName("emote")]
EmoteFragment? Emote,
[property: JsonPropertyName("mention")]
MentionFragment? Mention
);

public record CheermoteFragment(
[property: JsonPropertyName("prefix")]
string Prefix,
[property: JsonPropertyName("bits")]
int Bits,
[property: JsonPropertyName("tier")]
int Tier
);

public record EmoteFragment(
[property: JsonPropertyName("id")]
string Id,
[property: JsonPropertyName("emote_set_id")]
string EmoteSetId,
[property: JsonPropertyName("owner_id")]
string OwnerId,
[property: JsonPropertyName("format")]
IReadOnlyList<string> Format
);

public record MentionFragment(
[property: JsonPropertyName("user_id")]
string UserId,
[property: JsonPropertyName("user_login")]
string UserLogin,
[property: JsonPropertyName("user_name")]
string UserDisplayName
);
}
Loading