diff --git a/DysonNetwork.Insight/Migrations/20251116123552_SharableThought.Designer.cs b/DysonNetwork.Insight/Migrations/20251116123552_SharableThought.Designer.cs
new file mode 100644
index 0000000..41339a0
--- /dev/null
+++ b/DysonNetwork.Insight/Migrations/20251116123552_SharableThought.Designer.cs
@@ -0,0 +1,163 @@
+//
+using System;
+using System.Collections.Generic;
+using DysonNetwork.Insight;
+using DysonNetwork.Shared.Models;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using NodaTime;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace DysonNetwork.Insight.Migrations
+{
+ [DbContext(typeof(AppDatabase))]
+ [Migration("20251116123552_SharableThought")]
+ partial class SharableThought
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.11")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingSequence", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("AccountId")
+ .HasColumnType("uuid")
+ .HasColumnName("account_id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("IsPublic")
+ .HasColumnType("boolean")
+ .HasColumnName("is_public");
+
+ b.Property("PaidToken")
+ .HasColumnType("bigint")
+ .HasColumnName("paid_token");
+
+ b.Property("Topic")
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("topic");
+
+ b.Property("TotalToken")
+ .HasColumnType("bigint")
+ .HasColumnName("total_token");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.HasKey("Id")
+ .HasName("pk_thinking_sequences");
+
+ b.ToTable("thinking_sequences", (string)null);
+ });
+
+ modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property>("Files")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("files");
+
+ b.Property("ModelName")
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("model_name");
+
+ b.Property>("Parts")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("parts");
+
+ b.Property("Role")
+ .HasColumnType("integer")
+ .HasColumnName("role");
+
+ b.Property("SequenceId")
+ .HasColumnType("uuid")
+ .HasColumnName("sequence_id");
+
+ b.Property("TokenCount")
+ .HasColumnType("bigint")
+ .HasColumnName("token_count");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated_at");
+
+ b.HasKey("Id")
+ .HasName("pk_thinking_thoughts");
+
+ b.HasIndex("SequenceId")
+ .HasDatabaseName("ix_thinking_thoughts_sequence_id");
+
+ b.ToTable("thinking_thoughts", (string)null);
+ });
+
+ modelBuilder.Entity("DysonNetwork.Shared.Models.SnUnpaidAccount", b =>
+ {
+ b.Property("AccountId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("account_id");
+
+ b.Property("MarkedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("marked_at");
+
+ b.HasKey("AccountId")
+ .HasName("pk_unpaid_accounts");
+
+ b.ToTable("unpaid_accounts", (string)null);
+ });
+
+ modelBuilder.Entity("DysonNetwork.Shared.Models.SnThinkingThought", b =>
+ {
+ b.HasOne("DysonNetwork.Shared.Models.SnThinkingSequence", "Sequence")
+ .WithMany()
+ .HasForeignKey("SequenceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("fk_thinking_thoughts_thinking_sequences_sequence_id");
+
+ b.Navigation("Sequence");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/DysonNetwork.Insight/Migrations/20251116123552_SharableThought.cs b/DysonNetwork.Insight/Migrations/20251116123552_SharableThought.cs
new file mode 100644
index 0000000..06704a7
--- /dev/null
+++ b/DysonNetwork.Insight/Migrations/20251116123552_SharableThought.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace DysonNetwork.Insight.Migrations
+{
+ ///
+ public partial class SharableThought : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "is_public",
+ table: "thinking_sequences",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "is_public",
+ table: "thinking_sequences");
+ }
+ }
+}
diff --git a/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs b/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs
index a1a0266..4615c96 100644
--- a/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs
+++ b/DysonNetwork.Insight/Migrations/AppDatabaseModelSnapshot.cs
@@ -44,6 +44,10 @@ namespace DysonNetwork.Insight.Migrations
.HasColumnType("timestamp with time zone")
.HasColumnName("deleted_at");
+ b.Property("IsPublic")
+ .HasColumnType("boolean")
+ .HasColumnName("is_public");
+
b.Property("PaidToken")
.HasColumnType("bigint")
.HasColumnName("paid_token");
diff --git a/DysonNetwork.Insight/Thought/ThoughtController.cs b/DysonNetwork.Insight/Thought/ThoughtController.cs
index ebf857e..18cf1f5 100644
--- a/DysonNetwork.Insight/Thought/ThoughtController.cs
+++ b/DysonNetwork.Insight/Thought/ThoughtController.cs
@@ -22,11 +22,16 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
[Required] public string UserMessage { get; set; } = null!;
public string? ServiceId { get; set; }
public Guid? SequenceId { get; set; }
- public List? AttachedPosts { get; set; }
+ public List? AttachedPosts { get; set; } = [];
public List>? AttachedMessages { get; set; }
public List AcceptProposals { get; set; } = [];
}
+ public class UpdateSharingRequest
+ {
+ public bool IsPublic { get; set; }
+ }
+
public class ThoughtServiceInfo
{
public string ServiceId { get; set; } = null!;
@@ -76,7 +81,6 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
return BadRequest("Service not found or configured.");
}
- // TODO: Check perk level from `currentUser`
if (serviceInfo.PerkLevel > 0 && !currentUser.IsSuperuser)
if (currentUser.PerkSubscription is null ||
PerkSubscriptionPrivilege.GetPrivilegeFromIdentifier(currentUser.PerkSubscription.Identifier) <
@@ -407,6 +411,25 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
return Ok(sequences);
}
+ [HttpPatch("sequences/{sequenceId:guid}/sharing")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task UpdateSequenceSharing(Guid sequenceId, [FromBody] UpdateSharingRequest request)
+ {
+ if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
+ var accountId = Guid.Parse(currentUser.Id);
+
+ var sequence = await service.GetSequenceAsync(sequenceId);
+ if (sequence == null) return NotFound();
+ if (sequence.AccountId != accountId) return Forbid();
+
+ sequence.IsPublic = request.IsPublic;
+ await service.UpdateSequenceAsync(sequence);
+
+ return NoContent();
+ }
+
///
/// Retrieves the thoughts in a specific thinking sequence.
///
@@ -419,12 +442,18 @@ public class ThoughtController(ThoughtProvider provider, ThoughtService service)
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task>> GetSequenceThoughts(Guid sequenceId)
{
- if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
- var accountId = Guid.Parse(currentUser.Id);
-
- var sequence = await service.GetOrCreateSequenceAsync(accountId, sequenceId);
+ var sequence = await service.GetSequenceAsync(sequenceId);
if (sequence == null) return NotFound();
+ if (!sequence.IsPublic)
+ {
+ if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized();
+ var accountId = Guid.Parse(currentUser.Id);
+
+ if (sequence.AccountId != accountId)
+ return StatusCode(403);
+ }
+
var thoughts = await service.GetPreviousThoughtsAsync(sequence);
return Ok(thoughts);
diff --git a/DysonNetwork.Insight/Thought/ThoughtService.cs b/DysonNetwork.Insight/Thought/ThoughtService.cs
index 19bc45e..57ef3eb 100644
--- a/DysonNetwork.Insight/Thought/ThoughtService.cs
+++ b/DysonNetwork.Insight/Thought/ThoughtService.cs
@@ -36,6 +36,17 @@ public class ThoughtService(
}
}
+ public async Task GetSequenceAsync(Guid sequenceId)
+ {
+ return await db.ThinkingSequences.FindAsync(sequenceId);
+ }
+
+ public async Task UpdateSequenceAsync(SnThinkingSequence sequence)
+ {
+ db.ThinkingSequences.Update(sequence);
+ await db.SaveChangesAsync();
+ }
+
public async Task SaveThoughtAsync(
SnThinkingSequence sequence,
List parts,
diff --git a/DysonNetwork.Shared/Models/ThinkingSequence.cs b/DysonNetwork.Shared/Models/ThinkingSequence.cs
index 85ec9e0..6de24bf 100644
--- a/DysonNetwork.Shared/Models/ThinkingSequence.cs
+++ b/DysonNetwork.Shared/Models/ThinkingSequence.cs
@@ -12,6 +12,8 @@ public class SnThinkingSequence : ModelBase
public long TotalToken { get; set; }
public long PaidToken { get; set; }
+ public bool IsPublic { get; set; } = false;
+
public Guid AccountId { get; set; }
}