Bump protocol to v9 and carry talk-circle ids on the presence frame.
Names stay off the wire; a person not in a circle writes an empty member list and topic. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+13
-2
@@ -1,4 +1,4 @@
|
|||||||
# Wire protocol v8
|
# Wire protocol v9
|
||||||
|
|
||||||
The client talks to the server two ways:
|
The client talks to the server two ways:
|
||||||
|
|
||||||
@@ -308,6 +308,9 @@ grid fetches `GET .../timetable?classId=` with it.
|
|||||||
|
|
||||||
`activity` is the ActionDef name currently in progress, or `null` when idle. `activityLabel`
|
`activity` is the ActionDef name currently in progress, or `null` when idle. `activityLabel`
|
||||||
is that def in the request locale. HTTP JSON is additive — no protocol version bump.
|
is that def in the request locale. HTTP JSON is additive — no protocol version bump.
|
||||||
|
`talkCircleMemberIds` is the live circle (including self, sorted) or `[]`; `talkTopicId` is the
|
||||||
|
topic def id or `null` when not talking. Same ids as the presence frame; names are not repeated
|
||||||
|
here either — the Now tab uses the directory and locale, like the location panel.
|
||||||
|
|
||||||
`skills` lists only keys the person has, not every `SkillDef` in the catalog. A first-year has
|
`skills` lists only keys the person has, not every `SkillDef` in the catalog. A first-year has
|
||||||
no Chemistry; a related tongue from the name set may sit beside the native at a low value.
|
no Chemistry; a related tongue from the name set may sit beside the native at a low value.
|
||||||
@@ -347,6 +350,8 @@ The client does not compute thresholds. There is no school-wide opinions endpoin
|
|||||||
"needs": [{ "id": "Sleep", "label": "Сон", "value": 1 }],
|
"needs": [{ "id": "Sleep", "label": "Сон", "value": 1 }],
|
||||||
"activity": null,
|
"activity": null,
|
||||||
"activityLabel": null,
|
"activityLabel": null,
|
||||||
|
"talkCircleMemberIds": [],
|
||||||
|
"talkTopicId": null,
|
||||||
"family": {
|
"family": {
|
||||||
"parents": [{ "id": "f0.p1", "fullName": "Иванова Ольга Михайловна", "female": true }],
|
"parents": [{ "id": "f0.p1", "fullName": "Иванова Ольга Михайловна", "female": true }],
|
||||||
"children": [],
|
"children": [],
|
||||||
@@ -972,6 +977,12 @@ Each person:
|
|||||||
| string | person id |
|
| string | person id |
|
||||||
| string | node id they occupy |
|
| string | node id they occupy |
|
||||||
| `u8` | `1` here, `2` walking |
|
| `u8` | `1` here, `2` walking |
|
||||||
|
| `u8` | talk-circle member count, then that many person-id strings |
|
||||||
|
| string | topic id (empty when not in a circle) |
|
||||||
|
|
||||||
|
Member ids are the live circle, including self, sorted by id. Count `0` and an empty topic mean
|
||||||
|
the person is not talking — the same id/node/state as before the circle fields. Names are not
|
||||||
|
on this frame; the client builds «говорит с Машей о футболе» from the HTTP directory and locale.
|
||||||
|
|
||||||
## Guarantees and limits
|
## Guarantees and limits
|
||||||
|
|
||||||
@@ -986,7 +997,7 @@ Each person:
|
|||||||
the oldest, because a stale clock is worthless once a newer one exists.
|
the oldest, because a stale clock is worthless once a newer one exists.
|
||||||
- The map snapshot and presence use a separate reliable queue so ticks cannot crowd them out.
|
- The map snapshot and presence use a separate reliable queue so ticks cannot crowd them out.
|
||||||
|
|
||||||
## Not in v8 yet
|
## Not in v9 yet
|
||||||
|
|
||||||
Authentication, Sit orders, an event log, walk animation, and `OpenLocation` on the server —
|
Authentication, Sit orders, an event log, walk animation, and `OpenLocation` on the server —
|
||||||
the tree and the location panel are filtered on the client from the snapshot plus presence.
|
the tree and the location panel are filtered on the client from the snapshot plus presence.
|
||||||
|
|||||||
@@ -262,7 +262,9 @@ describe('decodeServerMessage', () => {
|
|||||||
+ 2
|
+ 2
|
||||||
+ 2 + personId.length
|
+ 2 + personId.length
|
||||||
+ 2 + personNode.length
|
+ 2 + personNode.length
|
||||||
+ 1,
|
+ 1
|
||||||
|
+ 1
|
||||||
|
+ 2,
|
||||||
);
|
);
|
||||||
const view = new DataView(buffer);
|
const view = new DataView(buffer);
|
||||||
view.setUint8(0, MessageType.ServerPresence);
|
view.setUint8(0, MessageType.ServerPresence);
|
||||||
@@ -296,6 +298,10 @@ describe('decodeServerMessage', () => {
|
|||||||
new Uint8Array(buffer).set(personNode, offset);
|
new Uint8Array(buffer).set(personNode, offset);
|
||||||
offset += personNode.length;
|
offset += personNode.length;
|
||||||
view.setUint8(offset, PresenceState.Here);
|
view.setUint8(offset, PresenceState.Here);
|
||||||
|
offset += 1;
|
||||||
|
view.setUint8(offset, 0);
|
||||||
|
offset += 1;
|
||||||
|
view.setUint16(offset, 0, true);
|
||||||
|
|
||||||
expect(decodeServerMessage(buffer)).toEqual({
|
expect(decodeServerMessage(buffer)).toEqual({
|
||||||
type: 'presence',
|
type: 'presence',
|
||||||
@@ -308,7 +314,123 @@ describe('decodeServerMessage', () => {
|
|||||||
activityClass: '5А',
|
activityClass: '5А',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
people: [{ id: 'f0.c0', nodeId: 'classroom-101', state: PresenceState.Here }],
|
people: [
|
||||||
|
{
|
||||||
|
id: 'f0.c0',
|
||||||
|
nodeId: 'classroom-101',
|
||||||
|
state: PresenceState.Here,
|
||||||
|
talkMemberIds: [],
|
||||||
|
talkTopicId: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reads a presence person with talk-circle member ids and topic id', () => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const putString = (target: DataView, at: number, text: Uint8Array): number => {
|
||||||
|
target.setUint16(at, text.length, true);
|
||||||
|
new Uint8Array(target.buffer).set(text, at + 2);
|
||||||
|
return at + 2 + text.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const corridor = encoder.encode('corridor-1');
|
||||||
|
const selfId = encoder.encode('f0.c0');
|
||||||
|
const partnerId = encoder.encode('f0.c1');
|
||||||
|
const topic = encoder.encode('TopicSport');
|
||||||
|
const buffer = new ArrayBuffer(
|
||||||
|
7
|
||||||
|
+ 2
|
||||||
|
+ 2
|
||||||
|
+ 2 + selfId.length
|
||||||
|
+ 2 + corridor.length
|
||||||
|
+ 1
|
||||||
|
+ 1
|
||||||
|
+ 2 + selfId.length
|
||||||
|
+ 2 + partnerId.length
|
||||||
|
+ 2 + topic.length,
|
||||||
|
);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint8(0, MessageType.ServerPresence);
|
||||||
|
view.setInt32(1, 1, true);
|
||||||
|
view.setUint16(5, 0, true);
|
||||||
|
view.setUint16(7, 1, true);
|
||||||
|
let offset = 9;
|
||||||
|
offset = putString(view, offset, selfId);
|
||||||
|
offset = putString(view, offset, corridor);
|
||||||
|
view.setUint8(offset, PresenceState.Here);
|
||||||
|
offset += 1;
|
||||||
|
view.setUint8(offset, 2);
|
||||||
|
offset += 1;
|
||||||
|
offset = putString(view, offset, selfId);
|
||||||
|
offset = putString(view, offset, partnerId);
|
||||||
|
putString(view, offset, topic);
|
||||||
|
|
||||||
|
expect(decodeServerMessage(buffer)).toEqual({
|
||||||
|
type: 'presence',
|
||||||
|
schoolId: 1,
|
||||||
|
nodes: [],
|
||||||
|
people: [
|
||||||
|
{
|
||||||
|
id: 'f0.c0',
|
||||||
|
nodeId: 'corridor-1',
|
||||||
|
state: PresenceState.Here,
|
||||||
|
talkMemberIds: ['f0.c0', 'f0.c1'],
|
||||||
|
talkTopicId: 'TopicSport',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not carry a person display name in a presence frame', () => {
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const putString = (target: DataView, at: number, text: Uint8Array): number => {
|
||||||
|
target.setUint16(at, text.length, true);
|
||||||
|
new Uint8Array(target.buffer).set(text, at + 2);
|
||||||
|
return at + 2 + text.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
const corridor = encoder.encode('corridor-1');
|
||||||
|
const selfId = encoder.encode('f0.c0');
|
||||||
|
const topic = encoder.encode('TopicSport');
|
||||||
|
const buffer = new ArrayBuffer(
|
||||||
|
7 + 2 + 2 + selfId.length + 2 + corridor.length + 1 + 1 + 2 + selfId.length + 2 + topic.length,
|
||||||
|
);
|
||||||
|
const view = new DataView(buffer);
|
||||||
|
view.setUint8(0, MessageType.ServerPresence);
|
||||||
|
view.setInt32(1, 1, true);
|
||||||
|
view.setUint16(5, 0, true);
|
||||||
|
view.setUint16(7, 1, true);
|
||||||
|
let offset = 9;
|
||||||
|
offset = putString(view, offset, selfId);
|
||||||
|
offset = putString(view, offset, corridor);
|
||||||
|
view.setUint8(offset, PresenceState.Here);
|
||||||
|
offset += 1;
|
||||||
|
view.setUint8(offset, 1);
|
||||||
|
offset += 1;
|
||||||
|
offset = putString(view, offset, selfId);
|
||||||
|
putString(view, offset, topic);
|
||||||
|
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
const text = new TextDecoder().decode(bytes);
|
||||||
|
expect(text).not.toContain('Мария');
|
||||||
|
expect(text).not.toContain('Иванова');
|
||||||
|
expect(text).not.toContain('Маша');
|
||||||
|
expect(text).toContain('f0.c0');
|
||||||
|
expect(text).toContain('TopicSport');
|
||||||
|
expect(decodeServerMessage(buffer)).toEqual({
|
||||||
|
type: 'presence',
|
||||||
|
schoolId: 1,
|
||||||
|
nodes: [],
|
||||||
|
people: [
|
||||||
|
{
|
||||||
|
id: 'f0.c0',
|
||||||
|
nodeId: 'corridor-1',
|
||||||
|
state: PresenceState.Here,
|
||||||
|
talkMemberIds: ['f0.c0'],
|
||||||
|
talkTopicId: 'TopicSport',
|
||||||
|
},
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
* changed together and documented in `docs/protocol.md`. All numbers are little-endian.
|
* changed together and documented in `docs/protocol.md`. All numbers are little-endian.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const PROTOCOL_VERSION = 8;
|
export const PROTOCOL_VERSION = 9;
|
||||||
|
|
||||||
export const MessageType = {
|
export const MessageType = {
|
||||||
ClientHello: 0x01,
|
ClientHello: 0x01,
|
||||||
@@ -122,6 +122,8 @@ export interface PresencePerson {
|
|||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly nodeId: string;
|
readonly nodeId: string;
|
||||||
readonly state: number;
|
readonly state: number;
|
||||||
|
readonly talkMemberIds: readonly string[];
|
||||||
|
readonly talkTopicId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PresenceMessage {
|
export interface PresenceMessage {
|
||||||
@@ -375,7 +377,24 @@ function decodePresence(view: DataView): PresenceMessage {
|
|||||||
offset = nodeId.next;
|
offset = nodeId.next;
|
||||||
const state = readU8(view, offset);
|
const state = readU8(view, offset);
|
||||||
offset += 1;
|
offset += 1;
|
||||||
people.push({ id: id.text, nodeId: nodeId.text, state });
|
const memberCount = readU8(view, offset);
|
||||||
|
offset += 1;
|
||||||
|
const talkMemberIds: string[] = [];
|
||||||
|
for (let member = 0; member < memberCount; member++) {
|
||||||
|
const memberId = readString(view, offset);
|
||||||
|
offset = memberId.next;
|
||||||
|
talkMemberIds.push(memberId.text);
|
||||||
|
}
|
||||||
|
|
||||||
|
const topic = readString(view, offset);
|
||||||
|
offset = topic.next;
|
||||||
|
people.push({
|
||||||
|
id: id.text,
|
||||||
|
nodeId: nodeId.text,
|
||||||
|
state,
|
||||||
|
talkMemberIds,
|
||||||
|
talkTopicId: topic.text,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { type: 'presence', schoolId, nodes, people };
|
return { type: 'presence', schoolId, nodes, people };
|
||||||
|
|||||||
@@ -98,8 +98,22 @@ public sealed record PresenceNode(
|
|||||||
string ActivitySubject = "",
|
string ActivitySubject = "",
|
||||||
string ActivityClass = "");
|
string ActivityClass = "");
|
||||||
|
|
||||||
/// <summary>One on-campus person. Names are resolved over HTTP, not on this frame.</summary>
|
/// <summary>
|
||||||
public sealed record PresencePerson(string Id, string NodeId, byte State);
|
/// One on-campus person. Names are resolved over HTTP, not on this frame. Talk members and
|
||||||
|
/// topic are ids; an empty list means the person is not in a circle.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record PresencePerson(
|
||||||
|
string Id,
|
||||||
|
string NodeId,
|
||||||
|
byte State,
|
||||||
|
IReadOnlyList<string> TalkMemberIds,
|
||||||
|
string TalkTopicId)
|
||||||
|
{
|
||||||
|
public PresencePerson(string id, string nodeId, byte state)
|
||||||
|
: this(id, nodeId, state, [], "")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Live occupancy of an open school, about twice a second. Counts and people cover the whole
|
/// Live occupancy of an open school, about twice a second. Counts and people cover the whole
|
||||||
|
|||||||
@@ -202,6 +202,14 @@ public static class ProtocolCodec
|
|||||||
foreach (var person in message.People)
|
foreach (var person in message.People)
|
||||||
{
|
{
|
||||||
size += StringSize(person.Id) + StringSize(person.NodeId) + sizeof(byte);
|
size += StringSize(person.Id) + StringSize(person.NodeId) + sizeof(byte);
|
||||||
|
var members = person.TalkMemberIds ?? [];
|
||||||
|
size += sizeof(byte);
|
||||||
|
foreach (var memberId in members)
|
||||||
|
{
|
||||||
|
size += StringSize(memberId);
|
||||||
|
}
|
||||||
|
|
||||||
|
size += StringSize(person.TalkTopicId ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return size;
|
return size;
|
||||||
@@ -242,9 +250,22 @@ public static class ProtocolCodec
|
|||||||
writer.WriteUInt16((ushort)message.People.Count);
|
writer.WriteUInt16((ushort)message.People.Count);
|
||||||
foreach (var person in message.People)
|
foreach (var person in message.People)
|
||||||
{
|
{
|
||||||
|
var members = person.TalkMemberIds ?? [];
|
||||||
|
if (members.Count > byte.MaxValue)
|
||||||
|
{
|
||||||
|
throw new ProtocolException($"Presence person {person.Id} has {members.Count} talk members; u8 count cannot hold it.");
|
||||||
|
}
|
||||||
|
|
||||||
writer.WriteString(person.Id);
|
writer.WriteString(person.Id);
|
||||||
writer.WriteString(person.NodeId);
|
writer.WriteString(person.NodeId);
|
||||||
writer.WriteByte(person.State);
|
writer.WriteByte(person.State);
|
||||||
|
writer.WriteByte((byte)members.Count);
|
||||||
|
foreach (var memberId in members)
|
||||||
|
{
|
||||||
|
writer.WriteString(memberId);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteString(person.TalkTopicId ?? "");
|
||||||
}
|
}
|
||||||
|
|
||||||
return writer.Position;
|
return writer.Position;
|
||||||
@@ -403,7 +424,18 @@ public static class ProtocolCodec
|
|||||||
var people = new PresencePerson[personCount];
|
var people = new PresencePerson[personCount];
|
||||||
for (var i = 0; i < personCount; i++)
|
for (var i = 0; i < personCount; i++)
|
||||||
{
|
{
|
||||||
people[i] = new PresencePerson(reader.ReadString(), reader.ReadString(), reader.ReadByte());
|
var id = reader.ReadString();
|
||||||
|
var nodeId = reader.ReadString();
|
||||||
|
var state = reader.ReadByte();
|
||||||
|
var memberCount = reader.ReadByte();
|
||||||
|
var members = new string[memberCount];
|
||||||
|
for (var member = 0; member < memberCount; member++)
|
||||||
|
{
|
||||||
|
members[member] = reader.ReadString();
|
||||||
|
}
|
||||||
|
|
||||||
|
var topicId = reader.ReadString();
|
||||||
|
people[i] = new PresencePerson(id, nodeId, state, members, topicId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ServerPresenceMessage(schoolId, nodes, people);
|
return new ServerPresenceMessage(schoolId, nodes, people);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace HSchool.Protocol;
|
|||||||
public static class ProtocolConstants
|
public static class ProtocolConstants
|
||||||
{
|
{
|
||||||
/// <summary>Bumped on every breaking change to the binary layout.</summary>
|
/// <summary>Bumped on every breaking change to the binary layout.</summary>
|
||||||
public const byte Version = 8;
|
public const byte Version = 9;
|
||||||
|
|
||||||
/// <summary>Upper bound for a single WebSocket frame accepted by the server.</summary>
|
/// <summary>Upper bound for a single WebSocket frame accepted by the server.</summary>
|
||||||
public const int MaxMessageSize = 8 * 1024;
|
public const int MaxMessageSize = 8 * 1024;
|
||||||
|
|||||||
@@ -246,7 +246,120 @@ public class ProtocolCodecTests
|
|||||||
Assert.Equal("f0.c0", read.People[0].Id);
|
Assert.Equal("f0.c0", read.People[0].Id);
|
||||||
Assert.Equal("classroom-101", read.People[0].NodeId);
|
Assert.Equal("classroom-101", read.People[0].NodeId);
|
||||||
Assert.Equal(PresenceState.Here, read.People[0].State);
|
Assert.Equal(PresenceState.Here, read.People[0].State);
|
||||||
|
Assert.Empty(read.People[0].TalkMemberIds);
|
||||||
|
Assert.Equal("", read.People[0].TalkTopicId);
|
||||||
Assert.Equal(PresenceState.Walking, read.People[1].State);
|
Assert.Equal(PresenceState.Walking, read.People[1].State);
|
||||||
|
Assert.Empty(read.People[1].TalkMemberIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Presence_PersonWithoutCircle_WritesEmptyMemberListAndEmptyTopic()
|
||||||
|
{
|
||||||
|
var message = new ServerPresenceMessage(
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
[new PresencePerson("f0.c0", "corridor-1", PresenceState.Here)]);
|
||||||
|
var buffer = new byte[ProtocolCodec.PresenceSize(message)];
|
||||||
|
|
||||||
|
var length = ProtocolCodec.WritePresence(buffer, message);
|
||||||
|
|
||||||
|
Assert.Equal((byte)MessageType.ServerPresence, buffer[0]);
|
||||||
|
Assert.Equal(1, BitConverter.ToInt32(buffer.AsSpan(1, 4)));
|
||||||
|
Assert.Equal((ushort)0, BitConverter.ToUInt16(buffer.AsSpan(5, 2)));
|
||||||
|
Assert.Equal((ushort)1, BitConverter.ToUInt16(buffer.AsSpan(7, 2)));
|
||||||
|
|
||||||
|
var offset = 9;
|
||||||
|
offset = AssertWireString(buffer, offset, "f0.c0");
|
||||||
|
offset = AssertWireString(buffer, offset, "corridor-1");
|
||||||
|
Assert.Equal(PresenceState.Here, buffer[offset]);
|
||||||
|
offset += 1;
|
||||||
|
Assert.Equal(0, buffer[offset]);
|
||||||
|
offset += 1;
|
||||||
|
offset = AssertWireString(buffer, offset, "");
|
||||||
|
Assert.Equal(length, offset);
|
||||||
|
Assert.Equal(length, ProtocolCodec.PresenceSize(message));
|
||||||
|
|
||||||
|
var read = ProtocolCodec.ReadPresence(buffer.AsSpan(0, length));
|
||||||
|
Assert.Empty(read.People[0].TalkMemberIds);
|
||||||
|
Assert.Equal("", read.People[0].TalkTopicId);
|
||||||
|
Assert.Equal(message.People[0].Id, read.People[0].Id);
|
||||||
|
Assert.Equal(message.People[0].NodeId, read.People[0].NodeId);
|
||||||
|
Assert.Equal(message.People[0].State, read.People[0].State);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Presence_PersonWithCircle_RoundTripsMemberIdsAndTopic()
|
||||||
|
{
|
||||||
|
var members = new[] { "f0.c0", "f0.c1" };
|
||||||
|
var message = new ServerPresenceMessage(
|
||||||
|
1,
|
||||||
|
[new PresenceNode("corridor-1", 2)],
|
||||||
|
[new PresencePerson("f0.c0", "corridor-1", PresenceState.Here, members, "TopicSport")]);
|
||||||
|
var buffer = new byte[ProtocolCodec.PresenceSize(message)];
|
||||||
|
|
||||||
|
var length = ProtocolCodec.WritePresence(buffer, message);
|
||||||
|
|
||||||
|
Assert.Equal((byte)MessageType.ServerPresence, buffer[0]);
|
||||||
|
Assert.Equal(1, BitConverter.ToInt32(buffer.AsSpan(1, 4)));
|
||||||
|
Assert.Equal((ushort)1, BitConverter.ToUInt16(buffer.AsSpan(5, 2)));
|
||||||
|
|
||||||
|
var offset = 7;
|
||||||
|
offset = AssertWireString(buffer, offset, "corridor-1");
|
||||||
|
Assert.Equal((ushort)2, BitConverter.ToUInt16(buffer.AsSpan(offset, 2)));
|
||||||
|
offset += 2;
|
||||||
|
Assert.Equal(0, buffer[offset]);
|
||||||
|
offset += 1;
|
||||||
|
Assert.Equal((ushort)1, BitConverter.ToUInt16(buffer.AsSpan(offset, 2)));
|
||||||
|
offset += 2;
|
||||||
|
offset = AssertWireString(buffer, offset, "f0.c0");
|
||||||
|
offset = AssertWireString(buffer, offset, "corridor-1");
|
||||||
|
Assert.Equal(PresenceState.Here, buffer[offset]);
|
||||||
|
offset += 1;
|
||||||
|
Assert.Equal(2, buffer[offset]);
|
||||||
|
offset += 1;
|
||||||
|
offset = AssertWireString(buffer, offset, "f0.c0");
|
||||||
|
offset = AssertWireString(buffer, offset, "f0.c1");
|
||||||
|
offset = AssertWireString(buffer, offset, "TopicSport");
|
||||||
|
Assert.Equal(length, offset);
|
||||||
|
Assert.Equal(length, ProtocolCodec.PresenceSize(message));
|
||||||
|
|
||||||
|
var read = ProtocolCodec.ReadPresence(buffer.AsSpan(0, length));
|
||||||
|
Assert.Equal(["f0.c0", "f0.c1"], read.People[0].TalkMemberIds);
|
||||||
|
Assert.Equal("TopicSport", read.People[0].TalkTopicId);
|
||||||
|
Assert.Equal(message.SchoolId, read.SchoolId);
|
||||||
|
Assert.Equal(message.People[0].Id, read.People[0].Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Presence_DoesNotContainPersonDisplayName()
|
||||||
|
{
|
||||||
|
var message = new ServerPresenceMessage(
|
||||||
|
1,
|
||||||
|
[],
|
||||||
|
[new PresencePerson(
|
||||||
|
"f0.c0",
|
||||||
|
"corridor-1",
|
||||||
|
PresenceState.Here,
|
||||||
|
["f0.c0", "f0.c1"],
|
||||||
|
"TopicSport")]);
|
||||||
|
var buffer = new byte[ProtocolCodec.PresenceSize(message)];
|
||||||
|
|
||||||
|
ProtocolCodec.WritePresence(buffer, message);
|
||||||
|
var text = System.Text.Encoding.UTF8.GetString(buffer);
|
||||||
|
|
||||||
|
Assert.DoesNotContain("Мария", text);
|
||||||
|
Assert.DoesNotContain("Иванова", text);
|
||||||
|
Assert.DoesNotContain("Маша", text);
|
||||||
|
Assert.Contains("f0.c0", text);
|
||||||
|
Assert.Contains("TopicSport", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int AssertWireString(byte[] buffer, int offset, string expected)
|
||||||
|
{
|
||||||
|
var bytes = System.Text.Encoding.UTF8.GetBytes(expected);
|
||||||
|
Assert.Equal((ushort)bytes.Length, BitConverter.ToUInt16(buffer.AsSpan(offset, 2)));
|
||||||
|
Assert.Equal(bytes, buffer.AsSpan(offset + 2, bytes.Length).ToArray());
|
||||||
|
return offset + 2 + bytes.Length;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
Reference in New Issue
Block a user