Protocol 2.0 + 2.1 — account provisioning, world-state streams, Town Cryer news #7
@@ -280,6 +280,22 @@ In modern ServUO the "mayor" of a town is the **City Loyalty Governor**. The set
|
|||||||
|
|
||||||
Render the current board from `GET /governors` (§6) on connect, then keep it live with these events.
|
Render the current board from `GET /governors` (§6) on connect, then keep it live with these events.
|
||||||
|
|
||||||
|
#### Presence (Protocol 2.0)
|
||||||
|
|
||||||
|
Who's online and where. A population snapshot is polled (`PresenceSweepSeconds`, default 30s) and emitted **only when it changes**; region transitions arrive in real time.
|
||||||
|
|
||||||
|
| kind | fields | notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| `presence.online` | `count`, `byFacet` `{map: n}`, `byRegion` `{region: n}` | The current online population. Emitted when the count or any breakdown changes. `GET /online` gives the latest; `GET /history?kind=presence.online` the time series. |
|
||||||
|
| `region.enter` | `from` (or null), `to` (or null), `map`, `who` (actor object) | A player crossed into a new named region. `from`/`to` are region names (`Wilderness` is unnamed). Cheap "who's where" feed. |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"kind":"presence.online","count":42,"byFacet":{"Felucca":12,"Trammel":30},
|
||||||
|
"byRegion":{"Britain":18,"Wilderness":9,"Despise":2},"t":1752489280000}
|
||||||
|
{"kind":"region.enter","from":"Britain","to":"Despise","map":"Felucca",
|
||||||
|
"who":{"serial":"0x1A2B","name":"Darrow","acct":"whitlocktech","player":true},"t":...}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. REST — read queries
|
## 5. REST — read queries
|
||||||
@@ -576,6 +592,16 @@ GET /governors
|
|||||||
|
|
||||||
Every city's latest governance snapshot — the live board, kept current by the `city.update` stream (§4). Empty if the shard does not run the City Loyalty system. Ordered by city.
|
Every city's latest governance snapshot — the live board, kept current by the `city.update` stream (§4). Empty if the shard does not run the City Loyalty system. Ordered by city.
|
||||||
|
|
||||||
|
### Online population (Protocol 2.0)
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /online
|
||||||
|
→ {"kind":"presence.online","count":42,"byFacet":{"Felucca":12,"Trammel":30},
|
||||||
|
"byRegion":{"Britain":18,"Wilderness":9},"t":1752489280000}
|
||||||
|
```
|
||||||
|
|
||||||
|
The current online population — total plus per-facet and per-region breakdowns. The latest `presence.online` snapshot (from SQLite, so it survives a sidecar restart); keep it live with the `presence.online` stream (§4). `count: 0` with empty maps if the shard hasn't reported yet. For the population time series, `GET /history?kind=presence.online`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Status codes
|
## 7. Status codes
|
||||||
|
|||||||
@@ -381,8 +381,8 @@ If the website mirrors rosters/links (it does — `store.record_link`), it must
|
|||||||
## 13. Part B phasing
|
## 13. Part B phasing
|
||||||
|
|
||||||
1. ~~**Guilds + governors.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeSocial.cs` (guild sweep + `JoinGuild` → `guild.update`/`guild.remove`/`guild.join`) and `BridgeGovernance.cs` (city sweep → `city.update`, gated on `CityLoyaltySystem.Enabled`), `GuildSweepSeconds` (60s) / `CitySweepSeconds` (300s) config, both wired into `[bridge reload|sweepnow|status`. Sidecar `guilds`/`governors` board tables + `GET /guilds`, `/governors` served from the store (the §12.2 snapshot rule). Shared `BridgeJson.Actor` writer (serial/name/acct/webId/player). **Deviation from the §10 sketch:** the wire uses full-state `guild.update`/`city.update` upserts (website derives "created"/"governor changed" from the board) rather than discrete `guild.created`/`city.governor` events — this avoids a reconnect re-emit looking like a storm of creations, matching the proven `champ.update` model. *Live end-to-end run still pending.*
|
1. ~~**Guilds + governors.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgeSocial.cs` (guild sweep + `JoinGuild` → `guild.update`/`guild.remove`/`guild.join`) and `BridgeGovernance.cs` (city sweep → `city.update`, gated on `CityLoyaltySystem.Enabled`), `GuildSweepSeconds` (60s) / `CitySweepSeconds` (300s) config, both wired into `[bridge reload|sweepnow|status`. Sidecar `guilds`/`governors` board tables + `GET /guilds`, `/governors` served from the store (the §12.2 snapshot rule). Shared `BridgeJson.Actor` writer (serial/name/acct/webId/player). **Deviation from the §10 sketch:** the wire uses full-state `guild.update`/`city.update` upserts (website derives "created"/"governor changed" from the board) rather than discrete `guild.created`/`city.governor` events — this avoids a reconnect re-emit looking like a storm of creations, matching the proven `champ.update` model. *Live end-to-end run still pending.*
|
||||||
2. **Presence.** Who's-online/population sweep + region presence (`OnEnterRegion`) → `GET /online`, population history in the store.
|
2. ~~**Presence.**~~ **Built (2026-07-17), compiles clean both sides.** `BridgePresence.cs`: a `presence.online` sweep (total + per-facet + per-region, emitted on change) and real-time `region.enter` (`EventSink.OnEnterRegion`, player-filtered). `PresenceSweepSeconds` (30s), wired into `[bridge`. `GET /online` serves the latest snapshot from the event store (population series via `/history?kind=presence.online`). *Live run pending.*
|
||||||
3. **Housing registry.** Extend the decay sweep to a full owner→houses list + houses-for-sale → `GET /houses`. (Selected from the §11 menu.)
|
3. **Housing registry.** Extend the decay sweep to a full owner→houses list → `GET /houses`. (Selected from the §11 menu. Note: stock ServUO has no "for sale" flag on houses, so the registry is owner→houses; for-sale is dropped.)
|
||||||
4. **Titles.** `char.profile` `titles` block (§10.3) — no new stream, folds into `BridgeProfile`.
|
4. **Titles.** `char.profile` `titles` block (§10.3) — no new stream, folds into `BridgeProfile`.
|
||||||
5. **Factions/VvV** — only after confirming which system the shard runs; stream just the enabled one.
|
5. **Factions/VvV** — only after confirming which system the shard runs; stream just the enabled one.
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,15 @@ GuildSweepSeconds=60
|
|||||||
# Idle (emits nothing) unless the City Loyalty system is enabled (CityLoyalty.Enabled).
|
# Idle (emits nothing) unless the City Loyalty system is enabled (CityLoyalty.Enabled).
|
||||||
CitySweepSeconds=300
|
CitySweepSeconds=300
|
||||||
|
|
||||||
|
# Presence poll. Online population (total, per-facet, per-region) is snapshotted on this
|
||||||
|
# interval and emitted as presence.online only when it changes. Region transitions come
|
||||||
|
# through separately in real time as region.enter (EventSink.OnEnterRegion).
|
||||||
|
PresenceSweepSeconds=30
|
||||||
|
|
||||||
|
# Housing registry poll. Every house is diffed on this interval to emit house.update /
|
||||||
|
# house.remove (owner, region, location, decay). Houses change slowly; a few minutes is fine.
|
||||||
|
HousingSweepSeconds=300
|
||||||
|
|
||||||
# Shown to a player when they run [link. The website page where they enter the code.
|
# Shown to a player when they run [link. The website page where they enter the code.
|
||||||
LinkUrl=https://yoursite/link
|
LinkUrl=https://yoursite/link
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ namespace Server.Custom.Bridge
|
|||||||
BridgeChamps.Rearm();
|
BridgeChamps.Rearm();
|
||||||
BridgeSocial.Rearm();
|
BridgeSocial.Rearm();
|
||||||
BridgeGovernance.Rearm();
|
BridgeGovernance.Rearm();
|
||||||
|
BridgePresence.Rearm();
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeConfig.Describe());
|
||||||
e.Mobile.SendMessage("Bridge: sweeps re-armed; endpoint changes take effect on reconnect.");
|
e.Mobile.SendMessage("Bridge: sweeps re-armed; endpoint changes take effect on reconnect.");
|
||||||
break;
|
break;
|
||||||
@@ -177,11 +178,13 @@ namespace Server.Custom.Bridge
|
|||||||
BridgeChamps.SweepOnce();
|
BridgeChamps.SweepOnce();
|
||||||
BridgeSocial.SweepOnce();
|
BridgeSocial.SweepOnce();
|
||||||
BridgeGovernance.SweepOnce();
|
BridgeGovernance.SweepOnce();
|
||||||
|
BridgePresence.SweepOnce();
|
||||||
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
|
e.Mobile.SendMessage("Bridge: ran one sweep of each stream.");
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeSweeps.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
|
||||||
|
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -194,6 +197,7 @@ namespace Server.Custom.Bridge
|
|||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeChamps.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeSocial.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgeGovernance.Status());
|
||||||
|
e.Mobile.SendMessage("Bridge: {0}", BridgePresence.Status());
|
||||||
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
|
e.Mobile.SendMessage("Bridge: {0}", BridgePages.Status());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ namespace Server.Custom.Bridge
|
|||||||
public static int ChampSweepSeconds { get; private set; }
|
public static int ChampSweepSeconds { get; private set; }
|
||||||
public static int GuildSweepSeconds { get; private set; }
|
public static int GuildSweepSeconds { get; private set; }
|
||||||
public static int CitySweepSeconds { get; private set; }
|
public static int CitySweepSeconds { get; private set; }
|
||||||
|
public static int PresenceSweepSeconds { get; private set; }
|
||||||
|
public static int HousingSweepSeconds { get; private set; }
|
||||||
|
|
||||||
public static string LinkUrl { get; private set; }
|
public static string LinkUrl { get; private set; }
|
||||||
|
|
||||||
@@ -91,6 +93,14 @@ namespace Server.Custom.Bridge
|
|||||||
if (CitySweepSeconds < 1)
|
if (CitySweepSeconds < 1)
|
||||||
CitySweepSeconds = 1;
|
CitySweepSeconds = 1;
|
||||||
|
|
||||||
|
PresenceSweepSeconds = Config.Get("Bridge.PresenceSweepSeconds", 30);
|
||||||
|
if (PresenceSweepSeconds < 1)
|
||||||
|
PresenceSweepSeconds = 1;
|
||||||
|
|
||||||
|
HousingSweepSeconds = Config.Get("Bridge.HousingSweepSeconds", 300);
|
||||||
|
if (HousingSweepSeconds < 1)
|
||||||
|
HousingSweepSeconds = 1;
|
||||||
|
|
||||||
LinkUrl = Config.Get("Bridge.LinkUrl", "https://yoursite/link");
|
LinkUrl = Config.Get("Bridge.LinkUrl", "https://yoursite/link");
|
||||||
|
|
||||||
TownCrierMaxLines = Config.Get("Bridge.TownCrierMaxLines", 6);
|
TownCrierMaxLines = Config.Get("Bridge.TownCrierMaxLines", 6);
|
||||||
|
|||||||
203
overlay/Scripts/Custom/Bridge/BridgePresence.cs
Normal file
203
overlay/Scripts/Custom/Bridge/BridgePresence.cs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using Server.Mobiles;
|
||||||
|
|
||||||
|
namespace Server.Custom.Bridge
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The presence stream (docs/PROTOCOL_2.md §11 #1/#2): who is online and where. Two parts:
|
||||||
|
///
|
||||||
|
/// presence.online - a periodic population snapshot (total, per-facet, per-region), emitted
|
||||||
|
/// on a sweep but only when it changes, so the site has a live "N online"
|
||||||
|
/// plus a change history without a firehose of identical frames.
|
||||||
|
/// region.enter - a real-time location transition from EventSink.OnEnterRegion, the cheap
|
||||||
|
/// per-player movement signal PLAN.md §5.6 recommends over Movement.
|
||||||
|
///
|
||||||
|
/// The snapshot is derived each sweep from the online PlayerMobiles (NetState != null), the
|
||||||
|
/// same population the vitals sweep already walks; counting them by map and region is a handful
|
||||||
|
/// of field reads. region.enter is filtered to players.
|
||||||
|
/// </summary>
|
||||||
|
public static class BridgePresence
|
||||||
|
{
|
||||||
|
private static Timer _timer;
|
||||||
|
|
||||||
|
// Signature of the last-emitted snapshot, so an unchanged population emits nothing.
|
||||||
|
private static string _lastSig;
|
||||||
|
|
||||||
|
private static long _sweeps, _emitted, _regionEnters;
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
if (!BridgeConfig.Enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
EventSink.OnEnterRegion += OnEnterRegion;
|
||||||
|
EventSink.ServerStarted += OnServerStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnServerStarted()
|
||||||
|
{
|
||||||
|
// Force the next sweep to emit after a (re)connect, so a sidecar that restarted gets the
|
||||||
|
// current population within one sweep.
|
||||||
|
BridgeLink.Connected_Core += OnConnected;
|
||||||
|
Rearm();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnConnected()
|
||||||
|
{
|
||||||
|
_lastSig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Stops and recreates the timer from current config. Called by `[bridge reload`.</summary>
|
||||||
|
public static void Rearm()
|
||||||
|
{
|
||||||
|
Stop();
|
||||||
|
|
||||||
|
_timer = Timer.DelayCall(
|
||||||
|
TimeSpan.FromSeconds(BridgeConfig.PresenceSweepSeconds),
|
||||||
|
TimeSpan.FromSeconds(BridgeConfig.PresenceSweepSeconds),
|
||||||
|
PresenceSweep);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Stop()
|
||||||
|
{
|
||||||
|
if (_timer != null) { _timer.Stop(); _timer = null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Status()
|
||||||
|
{
|
||||||
|
return String.Format("presence(sweeps={0} emitted={1} regionEnters={2})",
|
||||||
|
_sweeps, _emitted, _regionEnters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Runs one sweep now. Wired into `[bridge sweepnow`.</summary>
|
||||||
|
public static void SweepOnce()
|
||||||
|
{
|
||||||
|
PresenceSweep();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PresenceSweep()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_sweeps++;
|
||||||
|
|
||||||
|
if (!BridgeLink.Connected)
|
||||||
|
return; // nothing is listening; do not fill the queue with perishable snapshots
|
||||||
|
|
||||||
|
int total = 0;
|
||||||
|
var byFacet = new SortedDictionary<string, int>(StringComparer.Ordinal);
|
||||||
|
var byRegion = new SortedDictionary<string, int>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
foreach (var m in World.Mobiles.Values)
|
||||||
|
{
|
||||||
|
var pm = m as PlayerMobile;
|
||||||
|
|
||||||
|
if (pm == null || pm.NetState == null || pm.Deleted)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
total++;
|
||||||
|
|
||||||
|
var facet = pm.Map == null ? "Internal" : pm.Map.Name;
|
||||||
|
Bump(byFacet, facet);
|
||||||
|
|
||||||
|
var region = pm.Region;
|
||||||
|
var regionName = (region == null || String.IsNullOrEmpty(region.Name)) ? "Wilderness" : region.Name;
|
||||||
|
Bump(byRegion, regionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sig = Signature(total, byFacet, byRegion);
|
||||||
|
if (sig == _lastSig)
|
||||||
|
return; // population unchanged since last emit
|
||||||
|
|
||||||
|
_lastSig = sig;
|
||||||
|
BridgeLink.Emit(WriteOnline(total, byFacet, byRegion));
|
||||||
|
_emitted++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Bridge] presence sweep threw: {0}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Bump(IDictionary<string, int> map, string key)
|
||||||
|
{
|
||||||
|
int n;
|
||||||
|
map[key] = map.TryGetValue(key, out n) ? n + 1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Signature(int total, SortedDictionary<string, int> byFacet, SortedDictionary<string, int> byRegion)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
sb.Append(total);
|
||||||
|
foreach (var kv in byFacet) sb.Append('|').Append(kv.Key).Append(':').Append(kv.Value);
|
||||||
|
sb.Append('#');
|
||||||
|
foreach (var kv in byRegion) sb.Append('|').Append(kv.Key).Append(':').Append(kv.Value);
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string WriteOnline(int total, SortedDictionary<string, int> byFacet, SortedDictionary<string, int> byRegion)
|
||||||
|
{
|
||||||
|
var sb = BridgeJson.Begin("presence.online").Num("count", total);
|
||||||
|
|
||||||
|
WriteCounts(sb, "byFacet", byFacet);
|
||||||
|
WriteCounts(sb, "byRegion", byRegion);
|
||||||
|
|
||||||
|
return sb.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Writes a nested object of {name: count} pairs.</summary>
|
||||||
|
private static void WriteCounts(System.Text.StringBuilder sb, string field, SortedDictionary<string, int> counts)
|
||||||
|
{
|
||||||
|
sb.Append(",\"").Append(field).Append("\":{");
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
foreach (var kv in counts)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
sb.Append(',');
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
BridgeJson.Escape(sb, kv.Key);
|
||||||
|
sb.Append(':').Append(kv.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- real-time region transitions ----
|
||||||
|
|
||||||
|
private static void OnEnterRegion(OnEnterRegionEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (e == null || e.From == null || !e.From.Player)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var from = e.OldRegion;
|
||||||
|
var to = e.NewRegion;
|
||||||
|
|
||||||
|
// Only meaningful when the named region actually changed.
|
||||||
|
var fromName = from == null ? null : from.Name;
|
||||||
|
var toName = to == null ? null : to.Name;
|
||||||
|
if (String.Equals(fromName, toName, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var sb = BridgeJson.Begin("region.enter")
|
||||||
|
.Str("from", fromName)
|
||||||
|
.Str("to", toName)
|
||||||
|
.Str("map", e.From.Map == null ? null : e.From.Map.Name);
|
||||||
|
|
||||||
|
sb.Actor("who", e.From);
|
||||||
|
|
||||||
|
BridgeLink.Emit(sb.End());
|
||||||
|
_regionEnters++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine("[Bridge] region enter handler threw: {0}", ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ pub async fn serve(addr: &str, state: AppState) -> anyhow::Result<()> {
|
|||||||
// and survive an outage with the last-known snapshot (docs/PROTOCOL_2.md §12.2).
|
// and survive an outage with the last-known snapshot (docs/PROTOCOL_2.md §12.2).
|
||||||
.route("/guilds", get(guilds))
|
.route("/guilds", get(guilds))
|
||||||
.route("/governors", get(governors))
|
.route("/governors", get(governors))
|
||||||
|
.route("/online", get(online))
|
||||||
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
.route_layer(middleware::from_fn_with_state(state.clone(), gate));
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -729,6 +730,26 @@ async fn governors(State(st): State<AppState>) -> impl IntoResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The current online population: total plus per-facet and per-region counts. This is the most
|
||||||
|
/// recent `presence.online` snapshot from the event store (so it survives a sidecar restart); the
|
||||||
|
/// live `presence.online` stream keeps it current, and `GET /history?kind=presence.online` gives the
|
||||||
|
/// population time series. Returns `count: 0` if the shard has not reported one yet.
|
||||||
|
async fn online(State(st): State<AppState>) -> impl IntoResponse {
|
||||||
|
match st.store.recent(Some("presence.online"), 1).await {
|
||||||
|
Ok(mut events) => match events.pop() {
|
||||||
|
Some(latest) => (StatusCode::OK, Json(latest)),
|
||||||
|
None => (
|
||||||
|
StatusCode::OK,
|
||||||
|
Json(json!({"kind": "presence.online", "count": 0, "byFacet": {}, "byRegion": {}})),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Err(e) => (
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(json!({"error": e.to_string()})),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- websocket ----
|
// ---- websocket ----
|
||||||
|
|
||||||
async fn ws_upgrade(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse {
|
async fn ws_upgrade(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user