Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 22, 2026

Description

Invite APIs now leave caller-provided target user CSV streams entirely untouched after the request; only internally created CSV streams are disposed. This prevents disposing user-owned streams while keeping ownership clear for internal streams.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested?

  • Test A
  • Test B

Test Configuration:

  • Firmware version:
  • Hardware:
  • Toolchain:
  • SDK:

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules

@Aiko-IT-Systems/discatsharp

Original prompt

@copilot address the comments mentioned in #758 (review) in a new pull request


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI and others added 2 commits January 22, 2026 05:20
Co-authored-by: Lulalaby <14029133+Lulalaby@users.noreply.github.com>
Co-authored-by: Lulalaby <14029133+Lulalaby@users.noreply.github.com>
@Lulalaby
Copy link
Member

Lulalaby commented Jan 22, 2026

@copilot as codex mentioned. We shouldn't dispose if it's not our stream. Relying on reset position is wrong here. Find another way

Copilot AI changed the title [WIP] Address comments from pull request review 758 Respect caller-owned invite CSV streams Jan 22, 2026
Copilot AI requested a review from Lulalaby January 22, 2026 05:25
Co-authored-by: Lulalaby <14029133+Lulalaby@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 22, 2026

@copilot as codex mentioned. We shouldn't dispose if it's not our stream. Relying on reset position is wrong here. Find another way

Adjusted disposal to only run for internally created CSV streams; caller-supplied streams are left untouched. Commit fa07919.

@Lulalaby Lulalaby marked this pull request as ready for review January 22, 2026 05:28
@Lulalaby Lulalaby requested a review from a team as a code owner January 22, 2026 05:28
@Lulalaby Lulalaby merged commit 721846d into main Jan 22, 2026
7 checks passed
@Lulalaby Lulalaby deleted the copilot/address-review-comments-758 branch January 22, 2026 05:28
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa07919dea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 3897 to 3901
var ret = DiscordJson.DeserializeObject<DiscordInvite>(res.Response, this.Discord);
ret.Discord = this.Discord;

if (targetUsersCsv is not null && targetUsersCsv.ResetPositionTo is null)
if (disposeTargetUsersCsv)
{

Choose a reason for hiding this comment

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

P2 Badge Restore caller stream position after invite upload

When targetUsersFile is caller-provided and seekable, CreateTargetUsersFile seeks it to position 0 and records ResetPositionTo. In CreateChannelInviteAsync the post-request reset was removed, so after the multipart upload the stream stays at the end rather than returning to the original position. This means caller-owned streams are no longer “untouched” and any subsequent reuse (e.g., re-sending the same CSV or reading from the original offset) will fail or read nothing. Consider restoring targetUsersCsv.Stream.Position when ResetPositionTo is set, as is still done in UpdateInviteTargetUsersAsync.

Useful? React with 👍 / 👎.

Comment on lines 3896 to 3897

if (targetUsersCsv?.ResetPositionTo is not null)
targetUsersCsv.Stream.Position = targetUsersCsv.ResetPositionTo.Value;

var ret = DiscordJson.DeserializeObject<DiscordInvite>(res.Response, this.Discord);
Copy link

Choose a reason for hiding this comment

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

Bug: The CreateChannelInviteAsync method fails to restore the original position of a caller-provided stream in targetUsersCsv, leaving it at an unexpected position after the operation completes.
Severity: MEDIUM

Suggested Fix

Reintroduce the stream position reset logic in CreateChannelInviteAsync before the method exits. Add back the check if (targetUsersCsv?.ResetPositionTo is not null) targetUsersCsv.Stream.Position = targetUsersCsv.ResetPositionTo.Value; to ensure the stream's position is restored for caller-owned streams, mirroring the implementation in UpdateInviteTargetUsersAsync.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: DisCatSharp/Net/Rest/DiscordApiClient.cs#L3896-L3897

Potential issue: The `CreateChannelInviteAsync` method, when provided with a
caller-owned stream via `targetUsersCsv`, repositions the stream to read it but fails to
restore its original position afterward. The logic to reset the stream's position was
removed in the pull request. This leaves the caller's stream at an unexpected position
(likely the end), which can cause bugs if the caller attempts to reuse the stream. This
behavior is inconsistent with the similar `UpdateInviteTargetUsersAsync` method, which
correctly restores the stream's position.

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a bug where caller-provided CSV streams for invite target users were being disposed by the API client, violating stream ownership principles. The fix introduces explicit tracking of stream ownership: only internally-created streams (from BuildTargetUsersCsvFile) are disposed, while caller-provided streams remain untouched.

Changes:

  • Introduced disposeTargetUsersCsv flag to track whether the CSV stream was created internally
  • Modified disposal logic to only dispose internally-created streams
  • Adjusted position reset behavior: removed for CreateChannelInviteAsync (POST), retained for UpdateInviteTargetUsersAsync (PUT)
Comments suppressed due to low confidence (32)

DisCatSharp/Net/Rest/DiscordApiClient.cs:3139

			foreach (var embed in embeds)
				if (embed.Timestamp != null)
					embed.Timestamp = embed.Timestamp.Value.ToUniversalTime();

DisCatSharp/Net/Rest/DiscordApiClient.cs:3192

			foreach (var embed in builder.Embeds)
				if (embed?.Timestamp != null)
					embed.Timestamp = embed.Timestamp.Value.ToUniversalTime();

DisCatSharp/Net/Rest/DiscordApiClient.cs:3569

			foreach (var embed in builder.Embeds)
				if (embed.Timestamp != null)
					embed.Timestamp = embed.Timestamp.Value.ToUniversalTime();

DisCatSharp/Net/Rest/DiscordApiClient.cs:5568

			foreach (var embed in builder.Embeds)
				if (embed.Timestamp != null)
					embed.Timestamp = embed.Timestamp.Value.ToUniversalTime();

DisCatSharp/Net/Rest/DiscordApiClient.cs:7457

			foreach (var embed in builder.Embeds)
				if (embed.Timestamp != null)
					embed.Timestamp = embed.Timestamp.Value.ToUniversalTime();

DisCatSharp/Net/Rest/DiscordApiClient.cs:7705

			foreach (var embed in builder.Embeds)
				if (embed.Timestamp != null)
					embed.Timestamp = embed.Timestamp.Value.ToUniversalTime();

DisCatSharp/Net/Rest/DiscordApiClient.cs:3783

		foreach (var voterRaw in votersRawList)
		{
			var xr = voterRaw.ToDiscordObject<TransportUser>();
			var usr = new DiscordUser(xr)
			{
				Discord = this.Discord
			};
			usr = this.Discord.UserCache.AddOrUpdate(xr.Id, usr, (id, old) =>
			{
				old.Username = usr.Username;
				old.Discriminator = usr.Discriminator;
				old.AvatarHash = usr.AvatarHash;
				old.GlobalName = usr.GlobalName;
				return old;
			});

			voters.Add(usr);
		}

DisCatSharp/Net/Rest/DiscordApiClient.cs:7010

  • The contents of this container are never accessed.
		var values = new Dictionary<string, string>

DisCatSharp/Net/Rest/DiscordApiClient.cs:7615

  • The contents of this container are never accessed.
		var values = new Dictionary<string, string>();

DisCatSharp/Net/Rest/DiscordApiClient.cs:7652

  • The contents of this container are never accessed.
		var values = new Dictionary<string, string>();

DisCatSharp/Net/Rest/DiscordApiClient.cs:1486

  • Condition is always not null because of ... != ....
			urlParams["after"] = after?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:1488

  • Condition is always not null because of ... != ....
			urlParams["before"] = before?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:1490

  • Condition is always not null because of ... != ....
			urlParams["user_id"] = responsible?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:1492

  • Condition is always not null because of ... != ....
			urlParams["action_type"] = actionType?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:2565

			urlParams["with_user_count"] = withUserCount?.ToString();

DisCatSharp/Net/Rest/DiscordApiClient.cs:2609

			urlParams["with_user_count"] = withUserCount?.ToString();

DisCatSharp/Net/Rest/DiscordApiClient.cs:3508

  • Condition is always not null because of ... != ....
			urlParams["around"] = around?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:3510

  • Condition is always not null because of ... != ....
			urlParams["before"] = before?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:3512

  • Condition is always not null because of ... != ....
			urlParams["after"] = after?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:3748

  • Condition is always not null because of ... != ....
			urlParams["after"] = after?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:3750

  • Condition is always not null because of ... > ....
			urlParams["limit"] = limit?.ToString(CultureInfo.InvariantCulture);

DisCatSharp/Net/Rest/DiscordApiClient.cs:5085

			urlParams["with_counts"] = withCounts?.ToString();

DisCatSharp/Net/Rest/DiscordApiClient.cs:5087

			urlParams["guild_scheduled_event_id"] = guildScheduledEventId?.ToString();

DisCatSharp/Net/Rest/DiscordApiClient.cs:5089

			urlParams["with_permissions"] = withPermissions?.ToString();

DisCatSharp/Net/Rest/DiscordApiClient.cs:7486

  • Condition is always not null because of ... is ....
					DiscordPollRequest = builder?.Poll?.Build()

DisCatSharp/Net/Rest/DiscordApiClient.cs:1548

  • Inefficient use of 'ContainsKey' and indexer.
		ret.Guild = this.Discord.Guilds.ContainsKey(guildId) ? this.Discord.Guilds[guildId] : null;

DisCatSharp/Net/Rest/DiscordApiClient.cs:2579

  • This assignment to guild is useless, since its value is never read.
		var guild = this.Discord.Guilds[guildId];

DisCatSharp/Net/Rest/DiscordApiClient.cs:2622

  • This assignment to guild is useless, since its value is never read.
		var guild = this.Discord.Guilds[guildId];

DisCatSharp/Net/Rest/DiscordApiClient.cs:7013

  • This assignment to values is useless, since its value is never read.
		var values = new Dictionary<string, string>
		{
			["payload_json"] = DiscordJson.SerializeObject(pld)
		};

DisCatSharp/Net/Rest/DiscordApiClient.cs:7615

  • This assignment to values is useless, since its value is never read.
		var values = new Dictionary<string, string>();

DisCatSharp/Net/Rest/DiscordApiClient.cs:7652

  • This assignment to values is useless, since its value is never read.
		var values = new Dictionary<string, string>();

DisCatSharp/Net/Rest/DiscordApiClient.cs:7561

  • These 'if' statements can be combined.
		if (builder is not null)
			if (!string.IsNullOrEmpty(builder.Content) || builder.Embeds?.Count > 0 || builder.IsTts || builder.Mentions?.Count > 0 || builder.Files?.Count > 0 || builder.Components?.Count > 0)
				values["payload_json"] = DiscordJson.SerializeObject(pld);

try
{
targetUsersCsv.Stream.Dispose();
targetUsersCsv?.Stream.Dispose();
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The null-conditional operator on targetUsersCsv?.Stream.Dispose() is redundant. The outer condition if (disposeTargetUsersCsv) already ensures targetUsersCsv is not null because disposeTargetUsersCsv is only true when targetUsersFile is null && targetUsersCsv is not null. You can safely use targetUsersCsv.Stream.Dispose() instead.

Suggested change
targetUsersCsv?.Stream.Dispose();
targetUsersCsv.Stream.Dispose();

Copilot uses AI. Check for mistakes.
try
{
targetUsersCsv.Stream.Dispose();
targetUsersCsv?.Stream.Dispose();
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The null-conditional operator on targetUsersCsv?.Stream.Dispose() is redundant. The outer condition if (disposeTargetUsersCsv) already ensures targetUsersCsv is not null because disposeTargetUsersCsv is only true when targetUsersFile is null && targetUsersCsv is not null. You can safely use targetUsersCsv.Stream.Dispose() instead.

Suggested change
targetUsersCsv?.Stream.Dispose();
targetUsersCsv.Stream.Dispose();

Copilot uses AI. Check for mistakes.
await this.DoMultipartAsync(this.Discord, bucket, url, RestRequestMethod.PUT, route, headers, files: new[] { targetUsersCsv }, fileFieldNameFactory: _ => "target_users_file").ConfigureAwait(false);

if (targetUsersCsv.ResetPositionTo is not null)
if (targetUsersCsv is not null && targetUsersCsv.ResetPositionTo is not null)
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The null check targetUsersCsv is not null is redundant. At this point in the code flow, targetUsersCsv is guaranteed to be non-null because line 5139 throws an ArgumentException if it's null. The condition can be simplified to just checking targetUsersCsv.ResetPositionTo is not null.

Suggested change
if (targetUsersCsv is not null && targetUsersCsv.ResetPositionTo is not null)
if (targetUsersCsv.ResetPositionTo is not null)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants