- Custom emoji (Emoji tags) on posts, display names, bios and profile fields, at most 64 per object, proxied, in Status.emojis and Account.emojis. - Remote profiles keep their header, profile fields, locked flag, published date, movedTo, indexable, memorial and image descriptions (a locked GoToSocial account no longer shows as open). - Polls: incoming Questions (Mastodon, Misskey, Pleroma, GoToSocial shapes) with counts, voters, end and closed; our own polls from the Mastodon API go out as Questions; votes in are counted once per voter and never become replies; personas vote on other servers' polls with one Note per choice; counts refresh with an Update at most every three minutes; a poll closes on time and tells its voters and its author. GET /api/v1/polls/:id and POST /api/v1/polls/:id/votes. - An Update without a newer `updated` only refreshes poll, video, audio and event details and leaves no revision. Checked live against GoToSocial: each side's poll reaches the other as a poll and each side's vote is counted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012CzABvBkbcFqoHdmi8b9WB
55 lines
1.9 KiB
C#
55 lines
1.9 KiB
C#
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
using PrivaPub.Api.Mastodon.Infrastructure;
|
|
using PrivaPub.Api.Mastodon.Mappers;
|
|
using PrivaPub.Domain.Privacy;
|
|
using PrivaPub.Domain.Statuses;
|
|
using PrivaPub.Models.Post;
|
|
using PrivaPub.StaticServices;
|
|
|
|
using PostEntity = PrivaPub.Models.Post.Post;
|
|
|
|
namespace PrivaPub.Api.Mastodon.Controllers
|
|
{
|
|
public class PollsController : MastodonController
|
|
{
|
|
readonly DbEntities _dbEntities;
|
|
readonly MastodonMapper _mapper;
|
|
readonly IPollService _polls;
|
|
|
|
public PollsController(DbEntities dbEntities, MastodonMapper mapper, IPollService polls)
|
|
{
|
|
_dbEntities = dbEntities;
|
|
_mapper = mapper;
|
|
_polls = polls;
|
|
}
|
|
|
|
[HttpGet("/api/v1/polls/{id}"), Scope("read:statuses", requiresUser: false), AllowAnonymous]
|
|
public async Task<IActionResult> Get(string id, CancellationToken token)
|
|
{
|
|
var post = await Visible(id, token);
|
|
return post?.Poll == default ? NotFoundError() : Json(_mapper.Poll(post, MyId, await _polls.OwnVotes(post.ID, MyId, token)));
|
|
}
|
|
|
|
[HttpPost("/api/v1/polls/{id}/votes"), Scope("write:statuses")]
|
|
public async Task<IActionResult> Vote(string id, CancellationToken token)
|
|
{
|
|
var post = await Visible(id, token);
|
|
if (post?.Poll == default)
|
|
return NotFoundError();
|
|
var choices = Params.List("choices").Select(c => int.TryParse(c, out var index) ? index : -1).ToList();
|
|
var outcome = await _polls.Vote(Me, post, choices, token);
|
|
if (!outcome.Ok)
|
|
return Error(outcome.Status, outcome.Error);
|
|
return Json(_mapper.Poll(outcome.Post, MyId, await _polls.OwnVotes(post.ID, MyId, token)));
|
|
}
|
|
|
|
async Task<PostEntity> Visible(string id, CancellationToken token)
|
|
{
|
|
var post = await _dbEntities.Posts.Match(p => p.ID == id && !p.DeletedAt.HasValue).ExecuteFirstAsync(token);
|
|
return post != default && post.Visibility != PostVisibility.LocalGeo && await VisibilityPolicy.CanSee(post, MyId, token) ? post : default;
|
|
}
|
|
}
|
|
}
|