a fork of iceshrimp.net but a tweaked frontend to my personal liking. waow
fediverse social-media social iceshrimp fedi
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[backend/masto-client] Allow scheduling boosts

The reblog endpoint has a new schedule_at field that works exactly like
the new status endpoint, returning a ScheduledStatus. Scheduled boosts
are managed the same way as any other scheduled post.

ScheduledStatus for boosts has an autogenerated fallback text entry, and
the boosted post id can be accessed via the reblog_id field.

Kopper d4b2c163 1aefbcea

+39 -20
+12 -7
Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs
··· 60 60 var visibility = flags.IsPleroma.Value && note.LocalOnly 61 61 ? "local" 62 62 : StatusEntity.EncodeVisibility(note.Visibility); 63 + 64 + var renoteUrl = note is { IsPureRenote: true, Renote: not null } 65 + ? note.Renote.Url ?? note.Renote.Uri ?? $"https://{config.Value.WebDomain}/notes/{note.RenoteId}" 66 + : null; 63 67 64 68 return new ScheduledStatusEntity 65 69 { ··· 67 71 ScheduledAt = note.ScheduledAt ?? DateTime.MinValue, 68 72 Params = new ScheduledStatusEntity.Param 69 73 { 70 - Text = note.Text, 71 - MediaIds = attachments.Select(a => a.Id).ToList(), 72 - Sensitive = sensitive, 73 - Cw = note.Cw, 74 + Text = renoteUrl != null ? $"🔁 {renoteUrl}" : note.Text, 75 + MediaIds = attachments.Select(a => a.Id).ToList(), 76 + Sensitive = sensitive, 77 + Cw = note.Cw, 74 78 Visibility = visibility, 75 - ReplyId = note.ReplyId, 76 - LocalOnly = note.LocalOnly, 77 - QuoteId = note.IsQuote ? note.RenoteId : null, 79 + ReplyId = note.ReplyId, 80 + LocalOnly = note.LocalOnly, 81 + QuoteId = note.IsQuote ? note.RenoteId : null, 82 + ReblogId = note.IsPureRenote ? note.RenoteId : null, 78 83 Poll = poll == null ? null : new ScheduledStatusEntity.Param.PollData 79 84 { 80 85 ExpiresIn = (long)(DateTime.UtcNow - pollExpiresIn).TotalSeconds,
+2 -2
Iceshrimp.Backend/Controllers/Mastodon/ScheduledStatusController.cs
··· 23 23 [EnableCors("mastodon")] 24 24 [EnableRateLimiting("sliding")] 25 25 [Produces(MediaTypeNames.Application.Json)] 26 - public class ScheduledStatusController(DatabaseContext db, NoteRenderer noteRenderer, NoteService noteService) : ControllerBase, IScopedService 26 + public class ScheduledStatusController(DatabaseContext db, NoteRenderer noteRenderer, NoteService noteService, IHttpContextAccessor httpContextAccessor) : ControllerBase, IScopedService 27 27 { 28 28 [Authenticate("read:statuses")] 29 29 [LinkPagination(20, 40)] ··· 48 48 [ProducesErrors(HttpStatusCode.Forbidden, HttpStatusCode.NotFound)] 49 49 public async Task<ScheduledStatusEntity> GetScheduledNote(string id) 50 50 { 51 - var user = HttpContext.GetUserOrFail(); 51 + var user = httpContextAccessor.HttpContext!.GetUserOrFail(); // calling this function as a service breaks otherwise 52 52 53 53 var note = await db.Notes 54 54 .IncludeUnpublished()
+1
Iceshrimp.Backend/Controllers/Mastodon/Schemas/Entities/StatusEntity.cs
··· 178 178 [J("media_ids")] public List<string>? MediaIds { get; set; } 179 179 [J("local_only")] public bool LocalOnly { get; set; } = false; 180 180 [J("quote_id")] public string? QuoteId { get; set; } 181 + [J("reblog_id")] public string? ReblogId { get; set; } 181 182 [J("poll")] public PollData? Poll { get; set; } 182 183 183 184 public class PollData
+4
Iceshrimp.Backend/Controllers/Mastodon/Schemas/StatusSchemas.cs
··· 102 102 [B(Name = "visibility")] 103 103 [J("visibility")] 104 104 public string? Visibility { get; set; } 105 + 106 + [B(Name = "scheduled_at")] 107 + [J("scheduled_at")] 108 + public DateTime? ScheduledAt { get; set; } 105 109 } 106 110 107 111 public class RescheduleRequest
+10 -3
Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs
··· 305 305 [Authorize("write:favourites")] 306 306 [ProducesResults(HttpStatusCode.OK)] 307 307 [ProducesErrors(HttpStatusCode.NotFound)] 308 - public async Task<StatusEntity> Renote(string id, [FromHybrid] StatusSchemas.ReblogRequest? request) 308 + public async Task<IPostNotePayload> Renote(string id, [FromHybrid] StatusSchemas.ReblogRequest? request) 309 309 { 310 + var scheduled = request?.ScheduledAt != null; 311 + if (scheduled && request?.ScheduledAt < DateTime.UtcNow.AddMinutes(5)) 312 + throw GracefulException.UnprocessableEntity("Scheduled note must be at least 5 minutes in the future"); 313 + 310 314 var user = HttpContext.GetUserOrFail(); 311 - var renote = await db.Notes.IncludeCommonProperties() 315 + var renote = await db.Notes.IncludeUnpublished() 316 + .IncludeCommonProperties() 312 317 .FirstOrDefaultAsync(p => p.RenoteId == id && p.User == user && p.IsPureRenote); 313 318 314 319 if (renote == null) ··· 323 328 ? StatusEntity.DecodeVisibility(request.Visibility) 324 329 : user.UserSettings?.DefaultRenoteVisibility ?? Note.NoteVisibility.Public; 325 330 326 - renote = await noteSvc.RenoteNoteAsync(note, user, renoteVisibility) ?? 331 + renote = await noteSvc.RenoteNoteAsync(note, user, renoteVisibility, request?.ScheduledAt) ?? 327 332 throw new Exception("Created renote was null"); 333 + if (renote.ScheduledAt != null) return await scheduledStatusController.GetScheduledNote(renote.Id); 334 + 328 335 note.RenoteCount++; // we do not want to call save changes after this point 329 336 } 330 337
+10 -8
Iceshrimp.Backend/Core/Services/NoteService.cs
··· 47 47 48 48 // https://misskey-hub.net/en/tools/aid-converter/ 49 49 // this seems to be the timestamp for 'zzzzzzzzzzzzzzzz'. anything newer i would expect something to break 50 - private static DateTime _scheduleIdTimeLimit = new DateTime(2089, 05, 24); 50 + private static readonly DateTime ScheduleIdTimeLimit = new DateTime(2089, 05, 24); 51 51 52 52 private static readonly AsyncKeyedLocker<string> KeyedLocker = new(o => 53 53 { ··· 283 283 // we need the note ids to sort correctly, but we also don't want to limit clients to a specific time 284 284 // as some clients have "drafts" that are posts scheduled for absurd years. let's assume that if you're scheduling 285 285 // something like that you don't care where it'll be sorted (not that we'll *actually* schedule it anyways, see ScheduleNoteAsync) 286 - var idTimestamp = data.ScheduledAt != null && data.ScheduledAt < _scheduleIdTimeLimit 286 + var idTimestamp = data.ScheduledAt != null && data.ScheduledAt < ScheduleIdTimeLimit 287 287 ? data.ScheduledAt 288 288 : data.CreatedAt; 289 289 ··· 535 535 { 536 536 scheduledAt = scheduledAt.ToUniversalTime(); 537 537 538 + note.CreatedAt = scheduledAt; 538 539 note.ScheduledAt = scheduledAt; 539 540 await db.SaveChangesAsync(); 540 541 ··· 548 549 { 549 550 scheduledAt = scheduledAt.ToUniversalTime(); 550 551 551 - note.CreatedAt = scheduledAt; 552 + note.CreatedAt = scheduledAt; 552 553 note.ScheduledAt = scheduledAt; 553 554 await db.SaveChangesAsync(); 554 555 ··· 1657 1658 return true; 1658 1659 } 1659 1660 1660 - public async Task<Note?> RenoteNoteAsync(Note note, User user, Note.NoteVisibility? visibility = null) 1661 + public async Task<Note?> RenoteNoteAsync(Note note, User user, Note.NoteVisibility? visibility = null, DateTime? scheduledAt = null) 1661 1662 { 1662 1663 if (!note.Published) 1663 1664 throw GracefulException.BadRequest("This note has not been published yet"); ··· 1668 1669 if (note.IsPureRenote) 1669 1670 throw GracefulException.BadRequest("Cannot renote a pure renote"); 1670 1671 1671 - if (!await db.Notes.AnyAsync(p => p.Renote == note && p.IsPureRenote && p.User == user)) 1672 + if (!await db.Notes.IncludeUnpublished().AnyAsync(p => p.Renote == note && p.IsPureRenote && p.User == user)) 1672 1673 return await CreateNoteAsync(new NoteCreationData 1673 1674 { 1674 - User = user, 1675 - Visibility = visibility.Value, 1676 - Renote = note 1675 + User = user, 1676 + Visibility = visibility.Value, 1677 + Renote = note, 1678 + ScheduledAt = scheduledAt 1677 1679 }); 1678 1680 1679 1681 return null;