+1
AltBot.Api/AltBot.Api.csproj
+1
AltBot.Api/AltBot.Api.csproj
+1
-1
AltBot.Api/Program.cs
+1
-1
AltBot.Api/Program.cs
+63
AltBot.Api/StartupExtensions.cs
+63
AltBot.Api/StartupExtensions.cs
···
1
1
using AltBot.Core.Models;
2
2
using AltBot.Data;
3
3
using AltBot.ServiceDefaults;
4
+
using Duende.IdentityModel.OidcClient;
5
+
using idunno.AtProto;
6
+
using idunno.Bluesky;
7
+
using idunno.Bluesky.Actor;
8
+
using idunno.Bluesky.Feed;
9
+
using idunno.Bluesky.Graph;
10
+
using idunno.Bluesky.Record;
4
11
using Microsoft.EntityFrameworkCore;
12
+
using Cid = AltBot.Core.Models.Cid;
13
+
using Did = AltBot.Core.Models.Did;
14
+
using Like = idunno.Bluesky.Record.Like;
5
15
6
16
namespace AltBot.Api;
7
17
···
17
27
// Subscribers endpoints
18
28
app.MapGet("/subscribers", async (DataContext db) =>
19
29
{
30
+
var agent = new BlueskyAgent();
31
+
32
+
33
+
var likesResult = await agent.GetLikes(
34
+
new AtUri("at://did:plc:yb2gz6yxpebbzlundrrfkv4d/app.bsky.labeler.service/self"));
35
+
36
+
37
+
if (!likesResult.Succeeded || likesResult.Result.Count == 0)
38
+
{
39
+
return Results.NotFound();
40
+
}
41
+
42
+
var list = new List<ProfileView>();
43
+
list.AddRange(likesResult.Result.Select(x => x.Actor));
44
+
45
+
while (!string.IsNullOrEmpty(likesResult.Result.Cursor))
46
+
{
47
+
likesResult = await agent.GetLikes(
48
+
new AtUri("at://did:plc:yb2gz6yxpebbzlundrrfkv4d/app.bsky.labeler.service/self"),
49
+
cursor: likesResult.Result.Cursor);
50
+
51
+
list.AddRange(likesResult.Result.Select(x => x.Actor));
52
+
}
53
+
54
+
var x = list.Select(x => $"{x.Handle.Value} - {x.Did}");
55
+
56
+
return Results.Ok(x);
57
+
58
+
59
+
60
+
//var followersResult = await agent.GetFollowers(AtIdentifier.Create("did:plc:yb2gz6yxpebbzlundrrfkv4d"));
61
+
62
+
//var list = new List<Followers>();
63
+
64
+
//if (followersResult.Succeeded && followersResult.Result.Count > 0)
65
+
//{
66
+
67
+
//}
68
+
69
+
//var sfsda = new List<Followers>();
70
+
71
+
//do
72
+
//{
73
+
// var results = followers.Result.Select(follow => $"{follow.Handle.Value} - {follow.Did}");
74
+
//}
75
+
//while (!string.IsNullOrEmpty(followers.Result.Cursor))
76
+
77
+
78
+
79
+
//return Results.Ok(results);
80
+
81
+
82
+
20
83
var subscribers = await db.Subscribers
21
84
.Select(s => new
22
85
{
+5
AltBot.Core/Exceptions/HaltException.cs
+5
AltBot.Core/Exceptions/HaltException.cs
+1
AltBot.Core/Models/Subscriber.cs
+1
AltBot.Core/Models/Subscriber.cs
···
9
9
public string? Handle { get; set; }
10
10
public string? Rkey { get; set; }
11
11
public virtual ICollection<ImagePost> Posts { get; set; } = new List<ImagePost>();
12
+
public virtual ICollection<SubscriberHistory> History{ get; set; } = new List<SubscriberHistory>();
12
13
public virtual LabelLevel Label { get; set; }
13
14
}
+14
AltBot.Core/Models/SubscriberHistory.cs
+14
AltBot.Core/Models/SubscriberHistory.cs
···
1
+
namespace AltBot.Core.Models;
2
+
3
+
public class SubscriberHistory
4
+
{
5
+
public required Did Did { get; set; }
6
+
7
+
public string Category { get; set; }
8
+
9
+
public string Action { get; set; }
10
+
11
+
public DateTime OccurredAt { get; set; }
12
+
13
+
public virtual Subscriber? Subscriber { get; set; }
14
+
}
+22
AltBot.Data/DataContext.cs
+22
AltBot.Data/DataContext.cs
···
64
64
entity.Property(x => x.Rkey)
65
65
.HasMaxLength(100);
66
66
});
67
+
68
+
modelBuilder.Entity<SubscriberHistory>(entity =>
69
+
{
70
+
entity.ToTable("subscriber_history");
71
+
72
+
entity.HasKey(x => new {x.Did, x.OccurredAt});
73
+
74
+
entity.Property(x => x.Did)
75
+
.HasMaxLength(2048)
76
+
.HasConversion(didConverter);
77
+
78
+
entity.Property(x => x.Category)
79
+
.HasMaxLength(200);
80
+
81
+
entity.Property(x => x.Category)
82
+
.HasMaxLength(2048);
83
+
84
+
entity.HasOne(x => x.Subscriber)
85
+
.WithMany(x => x.History)
86
+
.HasForeignKey(x => x.Did)
87
+
.HasConstraintName("fk_subscriber_history_subscriber");
88
+
});
67
89
}
68
90
}
+160
AltBot.Data/Migrations/20251023162128_AddHistoryTable.Designer.cs
+160
AltBot.Data/Migrations/20251023162128_AddHistoryTable.Designer.cs
···
1
+
// <auto-generated />
2
+
using System;
3
+
using AltBot.Core.Models;
4
+
using AltBot.Data;
5
+
using Microsoft.EntityFrameworkCore;
6
+
using Microsoft.EntityFrameworkCore.Infrastructure;
7
+
using Microsoft.EntityFrameworkCore.Migrations;
8
+
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
9
+
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
10
+
11
+
#nullable disable
12
+
13
+
namespace AltBot.Data.Migrations
14
+
{
15
+
[DbContext(typeof(DataContext))]
16
+
[Migration("20251023162128_AddHistoryTable")]
17
+
partial class AddHistoryTable
18
+
{
19
+
/// <inheritdoc />
20
+
protected override void BuildTargetModel(ModelBuilder modelBuilder)
21
+
{
22
+
#pragma warning disable 612, 618
23
+
modelBuilder
24
+
.HasAnnotation("ProductVersion", "9.0.10")
25
+
.HasAnnotation("Relational:MaxIdentifierLength", 63);
26
+
27
+
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "label", new[] { "bronze", "gold", "hero", "none", "silver" });
28
+
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
29
+
30
+
modelBuilder.Entity("AltBot.Core.Models.ImagePost", b =>
31
+
{
32
+
b.Property<string>("Did")
33
+
.HasMaxLength(2048)
34
+
.HasColumnType("character varying(2048)")
35
+
.HasColumnName("did");
36
+
37
+
b.Property<string>("Cid")
38
+
.HasMaxLength(1000)
39
+
.HasColumnType("character varying(1000)")
40
+
.HasColumnName("cid");
41
+
42
+
b.Property<string>("Rkey")
43
+
.HasMaxLength(512)
44
+
.HasColumnType("character varying(512)")
45
+
.HasColumnName("rkey");
46
+
47
+
b.Property<DateTime>("SeenAt")
48
+
.HasColumnType("timestamp with time zone")
49
+
.HasColumnName("seen_at");
50
+
51
+
b.Property<bool>("ValidAlt")
52
+
.HasColumnType("boolean")
53
+
.HasColumnName("valid_alt");
54
+
55
+
b.HasKey("Did", "Cid")
56
+
.HasName("pk_image_post");
57
+
58
+
b.ToTable("image_post", (string)null);
59
+
});
60
+
61
+
modelBuilder.Entity("AltBot.Core.Models.Subscriber", b =>
62
+
{
63
+
b.Property<string>("Did")
64
+
.HasMaxLength(2048)
65
+
.HasColumnType("character varying(2048)")
66
+
.HasColumnName("did");
67
+
68
+
b.Property<bool>("Active")
69
+
.ValueGeneratedOnAdd()
70
+
.HasColumnType("boolean")
71
+
.HasDefaultValue(true)
72
+
.HasColumnName("active");
73
+
74
+
b.Property<string>("Handle")
75
+
.HasMaxLength(250)
76
+
.IsUnicode(true)
77
+
.HasColumnType("character varying(250)")
78
+
.HasColumnName("handle");
79
+
80
+
b.Property<LabelLevel>("Label")
81
+
.HasColumnType("label")
82
+
.HasColumnName("label");
83
+
84
+
b.Property<string>("Rkey")
85
+
.HasMaxLength(100)
86
+
.HasColumnType("character varying(100)")
87
+
.HasColumnName("rkey");
88
+
89
+
b.Property<DateTime>("SeenAt")
90
+
.HasColumnType("timestamp with time zone")
91
+
.HasColumnName("seen_at");
92
+
93
+
b.HasKey("Did")
94
+
.HasName("pk_subscriber");
95
+
96
+
b.ToTable("subscriber", (string)null);
97
+
});
98
+
99
+
modelBuilder.Entity("AltBot.Core.Models.SubscriberHistory", b =>
100
+
{
101
+
b.Property<string>("Did")
102
+
.HasMaxLength(2048)
103
+
.HasColumnType("character varying(2048)")
104
+
.HasColumnName("did");
105
+
106
+
b.Property<DateTime>("OccurredAt")
107
+
.HasColumnType("timestamp with time zone")
108
+
.HasColumnName("occurred_at");
109
+
110
+
b.Property<string>("Action")
111
+
.IsRequired()
112
+
.HasColumnType("text")
113
+
.HasColumnName("action");
114
+
115
+
b.Property<string>("Category")
116
+
.IsRequired()
117
+
.HasMaxLength(2048)
118
+
.HasColumnType("character varying(2048)")
119
+
.HasColumnName("category");
120
+
121
+
b.HasKey("Did", "OccurredAt")
122
+
.HasName("pk_subscriber_history");
123
+
124
+
b.ToTable("subscriber_history", (string)null);
125
+
});
126
+
127
+
modelBuilder.Entity("AltBot.Core.Models.ImagePost", b =>
128
+
{
129
+
b.HasOne("AltBot.Core.Models.Subscriber", "Subscriber")
130
+
.WithMany("Posts")
131
+
.HasForeignKey("Did")
132
+
.OnDelete(DeleteBehavior.Cascade)
133
+
.IsRequired()
134
+
.HasConstraintName("fk_image_post_subscriber");
135
+
136
+
b.Navigation("Subscriber");
137
+
});
138
+
139
+
modelBuilder.Entity("AltBot.Core.Models.SubscriberHistory", b =>
140
+
{
141
+
b.HasOne("AltBot.Core.Models.Subscriber", "Subscriber")
142
+
.WithMany("History")
143
+
.HasForeignKey("Did")
144
+
.OnDelete(DeleteBehavior.Cascade)
145
+
.IsRequired()
146
+
.HasConstraintName("fk_subscriber_history_subscriber");
147
+
148
+
b.Navigation("Subscriber");
149
+
});
150
+
151
+
modelBuilder.Entity("AltBot.Core.Models.Subscriber", b =>
152
+
{
153
+
b.Navigation("History");
154
+
155
+
b.Navigation("Posts");
156
+
});
157
+
#pragma warning restore 612, 618
158
+
}
159
+
}
160
+
}
+42
AltBot.Data/Migrations/20251023162128_AddHistoryTable.cs
+42
AltBot.Data/Migrations/20251023162128_AddHistoryTable.cs
···
1
+
using System;
2
+
using Microsoft.EntityFrameworkCore.Migrations;
3
+
4
+
#nullable disable
5
+
6
+
namespace AltBot.Data.Migrations
7
+
{
8
+
/// <inheritdoc />
9
+
public partial class AddHistoryTable : Migration
10
+
{
11
+
/// <inheritdoc />
12
+
protected override void Up(MigrationBuilder migrationBuilder)
13
+
{
14
+
migrationBuilder.CreateTable(
15
+
name: "subscriber_history",
16
+
columns: table => new
17
+
{
18
+
did = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
19
+
occurred_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
20
+
category = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: false),
21
+
action = table.Column<string>(type: "text", nullable: false)
22
+
},
23
+
constraints: table =>
24
+
{
25
+
table.PrimaryKey("pk_subscriber_history", x => new { x.did, x.occurred_at });
26
+
table.ForeignKey(
27
+
name: "fk_subscriber_history_subscriber",
28
+
column: x => x.did,
29
+
principalTable: "subscriber",
30
+
principalColumn: "did",
31
+
onDelete: ReferentialAction.Cascade);
32
+
});
33
+
}
34
+
35
+
/// <inheritdoc />
36
+
protected override void Down(MigrationBuilder migrationBuilder)
37
+
{
38
+
migrationBuilder.DropTable(
39
+
name: "subscriber_history");
40
+
}
41
+
}
42
+
}
+42
AltBot.Data/Migrations/DataContextModelSnapshot.cs
+42
AltBot.Data/Migrations/DataContextModelSnapshot.cs
···
93
93
b.ToTable("subscriber", (string)null);
94
94
});
95
95
96
+
modelBuilder.Entity("AltBot.Core.Models.SubscriberHistory", b =>
97
+
{
98
+
b.Property<string>("Did")
99
+
.HasMaxLength(2048)
100
+
.HasColumnType("character varying(2048)")
101
+
.HasColumnName("did");
102
+
103
+
b.Property<DateTime>("OccurredAt")
104
+
.HasColumnType("timestamp with time zone")
105
+
.HasColumnName("occurred_at");
106
+
107
+
b.Property<string>("Action")
108
+
.IsRequired()
109
+
.HasColumnType("text")
110
+
.HasColumnName("action");
111
+
112
+
b.Property<string>("Category")
113
+
.IsRequired()
114
+
.HasMaxLength(2048)
115
+
.HasColumnType("character varying(2048)")
116
+
.HasColumnName("category");
117
+
118
+
b.HasKey("Did", "OccurredAt")
119
+
.HasName("pk_subscriber_history");
120
+
121
+
b.ToTable("subscriber_history", (string)null);
122
+
});
123
+
96
124
modelBuilder.Entity("AltBot.Core.Models.ImagePost", b =>
97
125
{
98
126
b.HasOne("AltBot.Core.Models.Subscriber", "Subscriber")
···
105
133
b.Navigation("Subscriber");
106
134
});
107
135
136
+
modelBuilder.Entity("AltBot.Core.Models.SubscriberHistory", b =>
137
+
{
138
+
b.HasOne("AltBot.Core.Models.Subscriber", "Subscriber")
139
+
.WithMany("History")
140
+
.HasForeignKey("Did")
141
+
.OnDelete(DeleteBehavior.Cascade)
142
+
.IsRequired()
143
+
.HasConstraintName("fk_subscriber_history_subscriber");
144
+
145
+
b.Navigation("Subscriber");
146
+
});
147
+
108
148
modelBuilder.Entity("AltBot.Core.Models.Subscriber", b =>
109
149
{
150
+
b.Navigation("History");
151
+
110
152
b.Navigation("Posts");
111
153
});
112
154
#pragma warning restore 612, 618
+4
AltBot.ServiceDefaults/AltBot.ServiceDefaults.csproj
+4
AltBot.ServiceDefaults/AltBot.ServiceDefaults.csproj
+29
AltBot.ServiceDefaults/GlobalExceptionHandler.cs
+29
AltBot.ServiceDefaults/GlobalExceptionHandler.cs
···
1
+
using Microsoft.AspNetCore.Diagnostics;
2
+
using Microsoft.AspNetCore.Http;
3
+
using Microsoft.AspNetCore.Mvc;
4
+
using Microsoft.Extensions.Logging;
5
+
6
+
namespace AltBot.ServiceDefaults;
7
+
8
+
internal sealed class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
9
+
{
10
+
public async ValueTask<bool> TryHandleAsync(
11
+
HttpContext httpContext,
12
+
Exception exception,
13
+
CancellationToken cancellationToken)
14
+
{
15
+
logger.LogCritical(exception, "Unhandled exception occurred: {Message}", exception.Message);
16
+
17
+
var problemDetails = new ProblemDetails
18
+
{
19
+
Status = StatusCodes.Status500InternalServerError,
20
+
Title = "Server error"
21
+
};
22
+
23
+
httpContext.Response.StatusCode = problemDetails.Status.Value;
24
+
25
+
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
26
+
27
+
return true;
28
+
}
29
+
}
+37
AltBot.ServiceDefaults/HaltExceptionHandler.cs
+37
AltBot.ServiceDefaults/HaltExceptionHandler.cs
···
1
+
using AltBot.Core.Exceptions;
2
+
using Microsoft.AspNetCore.Diagnostics;
3
+
using Microsoft.AspNetCore.Http;
4
+
using Microsoft.AspNetCore.Mvc;
5
+
using Microsoft.Extensions.Logging;
6
+
7
+
namespace AltBot.ServiceDefaults;
8
+
9
+
internal sealed class HaltExceptionHandler(ILogger<HaltExceptionHandler> logger) : IExceptionHandler
10
+
{
11
+
public async ValueTask<bool> TryHandleAsync(
12
+
HttpContext httpContext,
13
+
Exception exception,
14
+
CancellationToken cancellationToken)
15
+
{
16
+
if (exception is not HaltException haltException)
17
+
{
18
+
return false;
19
+
}
20
+
21
+
logger.LogCritical(haltException, "AltBot crash: {Message}", haltException.Message);
22
+
23
+
var problemDetails = new ProblemDetails
24
+
{
25
+
Status = StatusCodes.Status500InternalServerError,
26
+
Title = "AltBot Halted",
27
+
Detail = haltException.Message
28
+
};
29
+
30
+
httpContext.Response.StatusCode = problemDetails.Status.Value;
31
+
32
+
await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);
33
+
34
+
return true;
35
+
}
36
+
}
37
+
+3
-1
AltBot.ServiceDefaults/StartupExtensions.cs
+3
-1
AltBot.ServiceDefaults/StartupExtensions.cs
···
4
4
using Microsoft.Extensions.Diagnostics.HealthChecks;
5
5
using Microsoft.Extensions.Hosting;
6
6
using Microsoft.Extensions.Logging;
7
-
using Microsoft.Extensions.ServiceDiscovery;
8
7
using OpenTelemetry;
9
8
using OpenTelemetry.Metrics;
10
9
using OpenTelemetry.Trace;
···
28
27
builder.ConfigureOpenTelemetry();
29
28
30
29
builder.AddDefaultHealthChecks();
30
+
31
+
builder.Services.AddExceptionHandler<HaltExceptionHandler>();
32
+
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
31
33
32
34
builder.Services.AddServiceDiscovery();
33
35
+1
Directory.Packages.props
+1
Directory.Packages.props
···
8
8
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.5.1" />
9
9
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.5.1" />
10
10
<PackageVersion Include="Aspire.Hosting.PostgreSQL" Version="9.5.1" />
11
+
<PackageVersion Include="idunno.Bluesky" Version="1.1.0" />
11
12
<PackageVersion Include="Scalar.Aspire" Version="0.6.0" />
12
13
<!-- Entity Framework -->
13
14
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.10" />