mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
Merge branch 'march-2023-03-21' of https://github.com/DorkSquadRR/DorkNet into march-2023-03-21
This commit is contained in:
@@ -1,16 +1,32 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using DorkNet.Server.Services;
|
||||
|
||||
namespace DorkNet.Server.Controllers.API.Activities;
|
||||
|
||||
/// <summary>
|
||||
/// api.{rec.net,localhost}/api/activities/* — per-activity content
|
||||
/// (charades words, party-game prompts, etc.). The 2020 watch
|
||||
/// fetches these as a static list at room-join time; we ship an
|
||||
/// empty list so the activity's bundled defaults kick in.
|
||||
/// (charades words, party-game prompts, etc.).
|
||||
///
|
||||
/// The March 2023 watch fetches a 3D Charades deck at card-box spawn from
|
||||
/// <c>api/activities/charades/v1/words/{source}</c>, where <c>{source}</c>
|
||||
/// is one of three baked <c>CardBox.cardSource</c> slots — <c>Charades</c>,
|
||||
/// <c>CharadesAprilFoolsDay</c>, or <c>Icebreakers</c> (verified in the
|
||||
/// 2023.03.21 il2cpp dump). We resolve the admin-bound word list for the
|
||||
/// requested slot via <see cref="CharadesWordListService"/> and return the
|
||||
/// wire array of <c>{ EN_US, Difficulty }</c>. The older December route
|
||||
/// (no <c>{source}</c>) maps to the default Charades slot.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public class ActivitiesController : ControllerBase
|
||||
public class ActivitiesController(CharadesWordListService charades) : ControllerBase
|
||||
{
|
||||
[HttpGet("api/activities/charades/v1/words/{source}")]
|
||||
public async Task<IActionResult> CharadesWords(string source)
|
||||
=> Ok(await charades.ResolveWireWordsAsync(source));
|
||||
|
||||
/// <summary>December 2020 shape — no source segment. Serves the default
|
||||
/// Charades slot so the older client still gets a populated deck.</summary>
|
||||
[HttpGet("api/activities/charades/v1/words")]
|
||||
public IActionResult CharadesWords() => Ok(Array.Empty<object>());
|
||||
public async Task<IActionResult> CharadesWordsDefault()
|
||||
=> Ok(await charades.ResolveWireWordsAsync(
|
||||
nameof(CharadesWordListService.CharadesSlot.Charades)));
|
||||
}
|
||||
|
||||
@@ -10,10 +10,29 @@ namespace DorkNet.Server.Controllers.API.Sanitize;
|
||||
/// invention name / chat message to be submitted. Delegates to
|
||||
/// <see cref="ProfanityFilter"/> for the actual scrubbing. Response
|
||||
/// shape: <c>{Text (purified), IsClean (bool)}</c>.
|
||||
///
|
||||
/// When the admin has flipped
|
||||
/// <see cref="Data.Entities.ServerSettingsEntity.ProfanityFilterDisabled"/>,
|
||||
/// every route short-circuits to "clean" — input is returned unmodified
|
||||
/// and purity checks always pass — so nothing gets censored.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
public class SanitizeController(ILogger<SanitizeController> logger) : ControllerBase
|
||||
public class SanitizeController(
|
||||
ILogger<SanitizeController> logger,
|
||||
ServerSettingsService settings) : ControllerBase
|
||||
{
|
||||
/// <summary>Purify unless the filter is admin-disabled, in which case
|
||||
/// the input is returned untouched.</summary>
|
||||
private async Task<string> PurifyAsync(string input)
|
||||
=> await settings.IsProfanityFilterDisabledAsync()
|
||||
? input
|
||||
: ProfanityFilter.Purify(input);
|
||||
|
||||
/// <summary>Clean-check unless the filter is admin-disabled, in which
|
||||
/// case everything is treated as clean.</summary>
|
||||
private async Task<bool> IsCleanAsync(string input)
|
||||
=> await settings.IsProfanityFilterDisabledAsync() || ProfanityFilter.IsClean(input);
|
||||
|
||||
[HttpPost("api/sanitize/v1")]
|
||||
[HttpPost("api/sanitize/v1/text")]
|
||||
public async Task<IActionResult> Sanitize()
|
||||
@@ -73,7 +92,7 @@ public class SanitizeController(ILogger<SanitizeController> logger) : Controller
|
||||
// ASP.NET Core's StringOutputFormatter doesn't serialize as
|
||||
// text/plain (raw string sans quotes), which the watch then sees
|
||||
// as an empty cleanVersion after LitJson decode.
|
||||
var purified = ProfanityFilter.Purify(text);
|
||||
var purified = await PurifyAsync(text);
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(purified);
|
||||
logger.LogInformation("[sanitize] input=\"{Input}\" purified=\"{Purified}\" json={Json}", text, purified, json);
|
||||
return Content(json, "application/json");
|
||||
@@ -95,7 +114,7 @@ public class SanitizeController(ILogger<SanitizeController> logger) : Controller
|
||||
/// <c>{Text}</c> with profanity replaced. Wire shape matches
|
||||
/// <c>StringSanitization.PurifyString</c>.</summary>
|
||||
[HttpPost("api/sanitize/v1/purifyString")]
|
||||
public IActionResult PurifyString(
|
||||
public async Task<IActionResult> PurifyString(
|
||||
[FromBody] SanitizeBody? body,
|
||||
[FromForm(Name = "Text")] string? textForm,
|
||||
[FromForm(Name = "Input")] string? inputForm,
|
||||
@@ -104,28 +123,28 @@ public class SanitizeController(ILogger<SanitizeController> logger) : Controller
|
||||
{
|
||||
var input = body?.Text ?? body?.Input ?? body?.Value
|
||||
?? textForm ?? inputForm ?? valueForm ?? textQuery ?? string.Empty;
|
||||
return Ok(new { Text = ProfanityFilter.Purify(input) });
|
||||
return Ok(new { Text = await PurifyAsync(input) });
|
||||
}
|
||||
|
||||
/// <summary>POST <c>api/sanitize/v1/requestIsStringPure</c> —
|
||||
/// returns a bare JSON bool. Watch's <c>ExpectPrimitiveResponse</c>
|
||||
/// rejects object bodies here.</summary>
|
||||
[HttpPost("api/sanitize/v1/requestIsStringPure")]
|
||||
public IActionResult IsStringPure(
|
||||
public async Task<IActionResult> IsStringPure(
|
||||
[FromBody] SanitizeBody? body,
|
||||
[FromForm(Name = "Text")] string? textForm,
|
||||
[FromQuery(Name = "text")] string? textQuery)
|
||||
{
|
||||
var input = body?.Text ?? body?.Input ?? body?.Value
|
||||
?? textForm ?? textQuery ?? string.Empty;
|
||||
return Ok(ProfanityFilter.IsClean(input));
|
||||
return Ok(await IsCleanAsync(input));
|
||||
}
|
||||
|
||||
/// <summary>POST <c>api/sanitize/v1/isPure</c> — 2023 route used
|
||||
/// by CV2 maker pen text such as port names. It imports an object
|
||||
/// field named <c>IsPure</c>.</summary>
|
||||
[HttpPost("api/sanitize/v1/isPure")]
|
||||
public IActionResult IsPureV1(
|
||||
public async Task<IActionResult> IsPureV1(
|
||||
[FromBody] SanitizeBody? body,
|
||||
[FromForm(Name = "Text")] string? textForm,
|
||||
[FromForm(Name = "Input")] string? inputForm,
|
||||
@@ -134,7 +153,7 @@ public class SanitizeController(ILogger<SanitizeController> logger) : Controller
|
||||
{
|
||||
var input = body?.Text ?? body?.Input ?? body?.Value
|
||||
?? textForm ?? inputForm ?? valueForm ?? textQuery ?? string.Empty;
|
||||
var isPure = ProfanityFilter.IsClean(input);
|
||||
var isPure = await IsCleanAsync(input);
|
||||
logger.LogInformation(
|
||||
"[sanitize] isPure context={Context} intent={Intent} ruleset={Ruleset} valueLength={ValueLength} isPure={IsPure}",
|
||||
body?.Context ?? string.Empty,
|
||||
@@ -151,13 +170,13 @@ public class SanitizeController(ILogger<SanitizeController> logger) : Controller
|
||||
/// used by room/settings forms. Its importer reads an <c>IsPure</c>
|
||||
/// object field, unlike the newer bare-bool route above.</summary>
|
||||
[HttpPost("api/sanitize/isPure")]
|
||||
public IActionResult IsPure(
|
||||
public async Task<IActionResult> IsPure(
|
||||
[FromBody] SanitizeBody? body,
|
||||
[FromForm(Name = "Text")] string? textForm,
|
||||
[FromQuery(Name = "text")] string? textQuery)
|
||||
{
|
||||
var input = body?.Text ?? body?.Input ?? body?.Value
|
||||
?? textForm ?? textQuery ?? string.Empty;
|
||||
return Ok(new { IsPure = ProfanityFilter.IsClean(input) });
|
||||
return Ok(new { IsPure = await IsCleanAsync(input) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ public class AdminController(
|
||||
PlayerPresenceService playerPresence,
|
||||
PlayerLogService playerLog,
|
||||
ServerSettingsService serverSettings,
|
||||
CharadesWordListService charades,
|
||||
SignupCodeService signupCodes,
|
||||
IObjectStorage storage,
|
||||
DomainConfig domain,
|
||||
@@ -1490,12 +1491,121 @@ public class AdminController(
|
||||
row.SignupsDisabled,
|
||||
row.GlobalFriendsEnabled,
|
||||
row.WeeklyChallengesCompletedRequired,
|
||||
row.ProfanityFilterDisabled,
|
||||
row.UpdatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
public sealed record SignupsToggleRequest(bool Disabled);
|
||||
public sealed record GlobalFriendsToggleRequest(bool Enabled);
|
||||
public sealed record ProfanityFilterToggleRequest(bool Disabled);
|
||||
|
||||
/// <summary>POST <c>api/admin/v1/settings/profanity</c> — flip the
|
||||
/// server-side profanity filter. While disabled, every
|
||||
/// <c>api/sanitize/*</c> route returns input unchanged and treats all
|
||||
/// text as clean, so room/invention names and chat are never censored.
|
||||
/// Takes effect immediately (checked per request).</summary>
|
||||
[HttpPost("settings/profanity")]
|
||||
public async Task<ActionResult> SetProfanityFilterDisabled([FromBody] ProfanityFilterToggleRequest body)
|
||||
{
|
||||
var row = await serverSettings.SetProfanityFilterDisabledAsync(body.Disabled);
|
||||
await LogAsync(body.Disabled ? "profanity_filter_disabled" : "profanity_filter_enabled", "system", 0, "");
|
||||
await db.SaveChangesAsync();
|
||||
return Ok(new
|
||||
{
|
||||
row.SignupsDisabled,
|
||||
row.GlobalFriendsEnabled,
|
||||
row.WeeklyChallengesCompletedRequired,
|
||||
row.ProfanityFilterDisabled,
|
||||
row.UpdatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// ── 3D Charades word lists ───────────────────────────────────────────
|
||||
|
||||
public sealed record CharadesWordDto(string Text, int Difficulty);
|
||||
public sealed record CharadesWordListWriteDto(string? Name, List<CharadesWordDto>? Words);
|
||||
public sealed record CharadesImportDto(string? Text, int DefaultDifficulty, bool Replace);
|
||||
public sealed record CharadesBindingsDto(long Charades, long CharadesAprilFoolsDay, long Icebreakers);
|
||||
|
||||
private static List<CharadesWordListService.CharadesWord>? MapWords(List<CharadesWordDto>? words)
|
||||
=> words?.Select(w => new CharadesWordListService.CharadesWord(w.Text, w.Difficulty)).ToList();
|
||||
|
||||
/// <summary>GET <c>api/admin/v1/charades/wordlists</c> — the whole word
|
||||
/// list library plus the live slot→list bindings, so the SPA can render
|
||||
/// the editor and the three slot pickers in one round trip.</summary>
|
||||
[HttpGet("charades/wordlists")]
|
||||
public async Task<ActionResult> ListCharadesWordLists()
|
||||
{
|
||||
var lists = await charades.GetAllAsync();
|
||||
var bindings = await charades.GetBindingsAsync();
|
||||
return Ok(new { lists, bindings });
|
||||
}
|
||||
|
||||
/// <summary>POST <c>api/admin/v1/charades/wordlists</c> — create a new
|
||||
/// word list (optionally pre-populated).</summary>
|
||||
[HttpPost("charades/wordlists")]
|
||||
public async Task<ActionResult> CreateCharadesWordList([FromBody] CharadesWordListWriteDto body)
|
||||
{
|
||||
var dto = await charades.CreateAsync(body.Name ?? string.Empty, MapWords(body.Words));
|
||||
await LogAsync("create_charades_list", "global", dto.Id, dto.Name);
|
||||
return Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>PUT <c>api/admin/v1/charades/wordlists/{id}</c> — rename
|
||||
/// and/or replace the words of a list. Null fields are left as-is.</summary>
|
||||
[HttpPut("charades/wordlists/{id:long}")]
|
||||
public async Task<ActionResult> UpdateCharadesWordList(long id, [FromBody] CharadesWordListWriteDto body)
|
||||
{
|
||||
var dto = await charades.UpdateAsync(id, body.Name, MapWords(body.Words));
|
||||
if (dto is null) return NotFound();
|
||||
await LogAsync("update_charades_list", "global", id, dto.Name);
|
||||
return Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>DELETE <c>api/admin/v1/charades/wordlists/{id}</c> — remove a
|
||||
/// list. Any slot pointing at it is unbound (falls back to its built-in).</summary>
|
||||
[HttpDelete("charades/wordlists/{id:long}")]
|
||||
public async Task<ActionResult> DeleteCharadesWordList(long id)
|
||||
{
|
||||
var ok = await charades.DeleteAsync(id);
|
||||
if (!ok) return NotFound();
|
||||
await LogAsync("delete_charades_list", "global", id, "");
|
||||
return Ok(new { deleted = id });
|
||||
}
|
||||
|
||||
/// <summary>POST <c>api/admin/v1/charades/wordlists/{id}/import</c> —
|
||||
/// bulk-add cards from pasted text (one phrase per line, optional
|
||||
/// <c>| difficulty</c> suffix). <c>Replace=true</c> overwrites the
|
||||
/// list; otherwise the cards are appended.</summary>
|
||||
[HttpPost("charades/wordlists/{id:long}/import")]
|
||||
public async Task<ActionResult> ImportCharadesWordList(long id, [FromBody] CharadesImportDto body)
|
||||
{
|
||||
var dto = await charades.ImportAsync(id, body.Text ?? string.Empty, body.DefaultDifficulty, body.Replace);
|
||||
if (dto is null) return NotFound();
|
||||
await LogAsync("import_charades_list", "global", id, dto.Name);
|
||||
return Ok(dto);
|
||||
}
|
||||
|
||||
/// <summary>PUT <c>api/admin/v1/charades/bindings</c> — set which list is
|
||||
/// live for each of the client's three card-source slots. A list id of
|
||||
/// 0 unbinds that slot (falls back to its built-in). This is the
|
||||
/// "switch between lists" control.</summary>
|
||||
[HttpPut("charades/bindings")]
|
||||
public async Task<ActionResult> SetCharadesBindings([FromBody] CharadesBindingsDto body)
|
||||
{
|
||||
foreach (var listId in new[] { body.Charades, body.CharadesAprilFoolsDay, body.Icebreakers })
|
||||
{
|
||||
if (listId != 0 && !await db.CharadesWordLists.AnyAsync(l => l.Id == listId))
|
||||
return BadRequest(new { error = "unknown_list", id = listId });
|
||||
}
|
||||
|
||||
var bindings = await serverSettings.SetCharadesSlotBindingsAsync(
|
||||
new CharadesSlotBindings(body.Charades, body.CharadesAprilFoolsDay, body.Icebreakers));
|
||||
await LogAsync("set_charades_bindings", "system", 0,
|
||||
$"{bindings.Charades}/{bindings.CharadesAprilFoolsDay}/{bindings.Icebreakers}");
|
||||
return Ok(bindings);
|
||||
}
|
||||
|
||||
/// <summary>POST <c>api/admin/v1/settings/global-friends</c> — flip the
|
||||
/// "everyone is friends" toggle. No relationship rows are written; the
|
||||
|
||||
@@ -410,14 +410,32 @@ public class GoToController(
|
||||
};
|
||||
|
||||
[HttpPost("/goto/event/{eventId:long}")]
|
||||
// 2023 rename goto/event → matchmake/event (RecNet.Matchmaking HHDMGIMLBKF).
|
||||
[HttpPost("/matchmake/event/{eventId:long}")]
|
||||
public async Task<ActionResult<MatchmakingResponseDto>> GoToEvent(long eventId)
|
||||
{
|
||||
var resp = await BuildResponseAsync("DormRoom");
|
||||
// Matchmake the caller into the event's host room. Falls back to
|
||||
// the dorm if the event is unknown or has no room set.
|
||||
var roomId = await db.PlayerEvents.AsNoTracking()
|
||||
.Where(e => e.Id == eventId)
|
||||
.Select(e => (long?)e.RoomId)
|
||||
.FirstOrDefaultAsync();
|
||||
string? roomName = null;
|
||||
if (roomId is long rid && rid > 0)
|
||||
roomName = await db.Rooms.AsNoTracking()
|
||||
.Where(r => r.Id == rid)
|
||||
.Select(r => r.Name)
|
||||
.FirstOrDefaultAsync();
|
||||
|
||||
var resp = await BuildResponseAsync(
|
||||
string.IsNullOrWhiteSpace(roomName) ? "DormRoom" : roomName);
|
||||
await RecordResponseAsync(resp);
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
[HttpPost("/goto/club/{clubId:long}")]
|
||||
// 2023 rename goto/club → matchmake/club (RecNet.Matchmaking KPAIGCCHIBN).
|
||||
[HttpPost("/matchmake/club/{clubId:long}")]
|
||||
public async Task<ActionResult<MatchmakingResponseDto>> GoToClub(long clubId)
|
||||
{
|
||||
var club = await db.Clubs.AsNoTracking().FirstOrDefaultAsync(c => c.Id == clubId);
|
||||
@@ -430,7 +448,20 @@ public class GoToController(
|
||||
});
|
||||
}
|
||||
|
||||
var resp = await BuildResponseAsync("RecCenter");
|
||||
// Land in the club's clubhouse room when it has one set; otherwise
|
||||
// fall back to the Rec Center. Either way the response carries the
|
||||
// ClubId so the watch's club UI knows which club it matchmade for.
|
||||
string roomName = "RecCenter";
|
||||
if (club.ClubhouseRoomId is long clubhouseId && clubhouseId > 0)
|
||||
{
|
||||
var name = await db.Rooms.AsNoTracking()
|
||||
.Where(r => r.Id == clubhouseId)
|
||||
.Select(r => r.Name)
|
||||
.FirstOrDefaultAsync();
|
||||
if (!string.IsNullOrWhiteSpace(name)) roomName = name;
|
||||
}
|
||||
|
||||
var resp = await BuildResponseAsync(roomName);
|
||||
resp.RoomInstance!.ClubId = club.Id;
|
||||
await RecordResponseAsync(resp);
|
||||
return Ok(resp);
|
||||
@@ -438,7 +469,12 @@ public class GoToController(
|
||||
|
||||
[HttpPost("/goto/code")]
|
||||
[HttpPost("/goto/code/{code}")]
|
||||
public async Task<ActionResult<MatchmakingResponseDto>> GoToCode(string? code = null)
|
||||
// 2023 sends the room code as the second segment behind the room id
|
||||
// (RecNet.Matchmaking CPINEGCODMD → "matchmake/code/{roomId}/{code}").
|
||||
// The code alone resolves the private instance, so the roomId is just
|
||||
// routing context we don't need — resolve by code either way.
|
||||
[HttpPost("/matchmake/code/{roomId:long}/{code}")]
|
||||
public async Task<ActionResult<MatchmakingResponseDto>> GoToCode(string? code = null, long roomId = 0)
|
||||
{
|
||||
code ??= Request.HasFormContentType
|
||||
? (await Request.ReadFormAsync())["code"].FirstOrDefault()
|
||||
@@ -505,6 +541,11 @@ public class GoToController(
|
||||
/// dorm if the instance was forgotten (server restart, owner
|
||||
/// already left and nobody re-created it).</summary>
|
||||
[HttpPost("/goto/instance/{roomInstanceId:long}")]
|
||||
// The 2023 client renamed the whole matchmaking surface from goto/* to
|
||||
// matchmake/* (verified 2023.03.21 il2cpp dump — RecNet.Matchmaking
|
||||
// ANOAAIHGHMG builds "matchmake/instance/{0}"). Same handler, both
|
||||
// mounts, so December and 2023 clients each hit the path they build.
|
||||
[HttpPost("/matchmake/instance/{roomInstanceId:long}")]
|
||||
public async Task<ActionResult<MatchmakingResponseDto>> GoToInstance(long roomInstanceId)
|
||||
{
|
||||
var resp = await BuildPrivateInstanceJoinResponseAsync(roomInstanceId);
|
||||
@@ -512,7 +553,51 @@ public class GoToController(
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
/// <summary>POST <c>matchmake/chatinvite/{accountId}/{roomInstanceId}</c>
|
||||
/// — the 2023 "pull into my match" path. When a party owner invites
|
||||
/// someone over chat (PlayerParty.AcceptChatPartyInvite →
|
||||
/// RecNet.Matchmaking chatinvite, verified in the 2023.03.21 il2cpp
|
||||
/// dump), the invitee's watch posts here to matchmake into the owner's
|
||||
/// instance. The two path params are the inviter's account id and the
|
||||
/// room instance id in an order the client doesn't document; a private
|
||||
/// instance id is always ≥ 2^24 while account ids are small sequential,
|
||||
/// so we take the param that resolves to a registered instance (falling
|
||||
/// back to the larger value). Unlike the message-based
|
||||
/// <c>POST /invite</c> flow, a chat party invite writes no invitee row,
|
||||
/// so we add the caller to the allow-list here before resolving the
|
||||
/// join — the owner explicitly pulled them in.</summary>
|
||||
[HttpPost("/matchmake/chatinvite/{p0:long}/{p1:long}")]
|
||||
public async Task<ActionResult<MatchmakingResponseDto>> MatchmakeChatInvite(long p0, long p1)
|
||||
{
|
||||
var roomInstanceId = await ResolveChatInviteInstanceAsync(p0, p1);
|
||||
|
||||
if (this.CurrentPlayerId() is long callerId && roomInstanceId > 0)
|
||||
await privateInstances.InviteAsync(roomInstanceId, callerId);
|
||||
|
||||
var resp = await BuildPrivateInstanceJoinResponseAsync(roomInstanceId);
|
||||
await RecordResponseAsync(resp);
|
||||
logger.LogInformation(
|
||||
"[matchmake-chatinvite] caller={Caller} p0={P0} p1={P1} → instance={Instance} error={Error}",
|
||||
this.CurrentPlayerId(), p0, p1, roomInstanceId, resp.ErrorCode);
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
/// <summary>Pick the room-instance param out of a chatinvite's two path
|
||||
/// segments. Prefers whichever resolves to a registered private
|
||||
/// instance; if neither does (e.g. a public-room party pull whose id is
|
||||
/// synthetic), falls back to the larger value since instance ids dwarf
|
||||
/// account ids.</summary>
|
||||
private async Task<long> ResolveChatInviteInstanceAsync(long p0, long p1)
|
||||
{
|
||||
if (await privateInstances.GetAsync(p0) is not null) return p0;
|
||||
if (await privateInstances.GetAsync(p1) is not null) return p1;
|
||||
return Math.Max(p0, p1);
|
||||
}
|
||||
|
||||
[HttpPost("/goto/invite/{inviteId:long}")]
|
||||
// 2023 rename of goto/invite → matchmake/invite (RecNet.Matchmaking
|
||||
// OIEDPBLNMOP). {inviteId} is the invite message id, same as December.
|
||||
[HttpPost("/matchmake/invite/{inviteId:long}")]
|
||||
public async Task<ActionResult<MatchmakingResponseDto>> GoToInvite(long inviteId)
|
||||
{
|
||||
var callerId = this.CurrentPlayerId();
|
||||
@@ -758,6 +843,9 @@ public class GoToController(
|
||||
/// or on the invite list per <see cref="PrivateInstanceService.CanJoin"/>;
|
||||
/// otherwise we refuse with errorCode=9 (RoomNotJoinable).</summary>
|
||||
[HttpPost("/goto/player/{playerId:long}")]
|
||||
// 2023 rename goto/player → matchmake/player (RecNet.Matchmaking
|
||||
// FJAFOJHHKPO). Follow a friend into their current room/instance.
|
||||
[HttpPost("/matchmake/player/{playerId:long}")]
|
||||
public async Task<ActionResult<MatchmakingResponseDto>> GoToPlayer(long playerId)
|
||||
{
|
||||
var theirRoom = presence.GetRoom(playerId);
|
||||
|
||||
Reference in New Issue
Block a user