Skip to content

Commit ff07cb2

Browse files
committed
api: Add realmUsers and friends to InitialSnapshot; add User model type
1 parent 901a99e commit ff07cb2

File tree

6 files changed

+244
-0
lines changed

6 files changed

+244
-0
lines changed

lib/api/model/initial_snapshot.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,33 @@ class InitialSnapshot {
2323

2424
final int maxFileUploadSizeMib;
2525

26+
@JsonKey(readValue: _readUsersIsActiveFallbackTrue)
27+
final List<User> realmUsers;
28+
@JsonKey(readValue: _readUsersIsActiveFallbackFalse)
29+
final List<User> realmNonActiveUsers;
30+
@JsonKey(readValue: _readUsersIsActiveFallbackTrue)
31+
final List<User> crossRealmBots;
32+
2633
// TODO etc., etc.
2734

35+
// `is_active` is sometimes absent:
36+
// https://chat.zulip.org/#narrow/stream/412-api-documentation/topic/.60is_active.60.20in.20.60.2Fregister.60.20response/near/1371603
37+
// But for our model it's convenient to always have it; so, fill it in.
38+
static Object? _readUsersIsActiveFallbackTrue(Map json, String key) {
39+
final list = (json[key] as List<dynamic>);
40+
for (final Map<String, dynamic> user in list) {
41+
user.putIfAbsent('is_active', () => true);
42+
}
43+
return list;
44+
}
45+
static Object? _readUsersIsActiveFallbackFalse(Map json, String key) {
46+
final list = (json[key] as List<dynamic>);
47+
for (final Map<String, dynamic> user in list) {
48+
user.putIfAbsent('is_active', () => false);
49+
}
50+
return list;
51+
}
52+
2853
InitialSnapshot({
2954
this.queueId,
3055
required this.lastEventId,
@@ -35,6 +60,9 @@ class InitialSnapshot {
3560
required this.customProfileFields,
3661
required this.subscriptions,
3762
required this.maxFileUploadSizeMib,
63+
required this.realmUsers,
64+
required this.realmNonActiveUsers,
65+
required this.crossRealmBots,
3866
});
3967

4068
factory InitialSnapshot.fromJson(Map<String, dynamic> json) =>

lib/api/model/initial_snapshot.g.dart

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/api/model/model.dart

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,92 @@ class CustomProfileField {
3131
Map<String, dynamic> toJson() => _$CustomProfileFieldToJson(this);
3232
}
3333

34+
@JsonSerializable(fieldRename: FieldRename.snake)
35+
class ProfileFieldUserData {
36+
final String value;
37+
final String? renderedValue;
38+
39+
ProfileFieldUserData({required this.value, this.renderedValue});
40+
41+
factory ProfileFieldUserData.fromJson(Map<String, dynamic> json) =>
42+
_$ProfileFieldUserDataFromJson(json);
43+
44+
Map<String, dynamic> toJson() => _$ProfileFieldUserDataToJson(this);
45+
}
46+
47+
/// As in [InitialSnapshot.realmUsers], [InitialSnapshot.realmNonActiveUsers], and [InitialSnapshot.crossRealmBots].
48+
///
49+
/// In the Zulip API, the items in realm_users, realm_non_active_users, and
50+
/// cross_realm_bots are all extremely similar. They differ only in that
51+
/// cross_realm_bots has is_system_bot.
52+
///
53+
/// https://zulip.com/api/register-queue#response
54+
@JsonSerializable(fieldRename: FieldRename.snake)
55+
class User {
56+
final int userId;
57+
@JsonKey(name: 'delivery_email')
58+
String? deliveryEmailStaleDoNotUse; // TODO see [RealmUserUpdateEvent.deliveryEmail]
59+
String email;
60+
String fullName;
61+
String dateJoined;
62+
bool isActive; // Really sometimes absent in /register, but we normalize that away; see [InitialSnapshot.realmUsers].
63+
bool isOwner;
64+
bool isAdmin;
65+
bool isGuest;
66+
bool? isBillingAdmin; // TODO(server-5)
67+
bool isBot;
68+
int? botType; // TODO enum
69+
int? botOwnerId;
70+
int role; // TODO enum
71+
String timezone;
72+
String? avatarUrl; // TODO distinguish null from missing https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/flutter.3A.20omitted.20vs.2E.20null.20in.20JSON/near/1551759
73+
int avatarVersion;
74+
// null for bots, which don't have custom profile fields.
75+
// If null for a non-bot, equivalent to `{}` (null just written for efficiency.)
76+
@JsonKey(readValue: _readProfileData)
77+
Map<int, ProfileFieldUserData>? profileData;
78+
@JsonKey(readValue: _readIsSystemBot)
79+
bool? isSystemBot; // TODO(server-5)
80+
81+
static Map<String, dynamic>? _readProfileData(Map json, String key) {
82+
final value = (json[key] as Map<String, dynamic>?);
83+
// Represent `{}` as `null`, to avoid allocating a huge number
84+
// of LinkedHashMap data structures that we can do without.
85+
// A hash table is inevitably going to involve some overhead
86+
// (several words, at minimum), even when nothing's stored in it yet.
87+
return (value != null && value.isNotEmpty) ? value : null;
88+
}
89+
90+
static bool? _readIsSystemBot(Map json, String key) =>
91+
json[key] ?? json['is_cross_realm_bot'];
92+
93+
User({
94+
required this.userId,
95+
required this.deliveryEmailStaleDoNotUse,
96+
required this.email,
97+
required this.fullName,
98+
required this.dateJoined,
99+
required this.isActive,
100+
required this.isOwner,
101+
required this.isAdmin,
102+
required this.isGuest,
103+
required this.isBillingAdmin,
104+
required this.isBot,
105+
this.botType,
106+
this.botOwnerId,
107+
required this.role,
108+
required this.timezone,
109+
required this.avatarUrl,
110+
required this.avatarVersion,
111+
this.profileData,
112+
this.isSystemBot,
113+
});
114+
115+
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
116+
117+
Map<String, dynamic> toJson() => _$UserToJson(this);
118+
}
119+
34120
/// As in `subscriptions` in the initial snapshot.
35121
@JsonSerializable(fieldRename: FieldRename.snake)
36122
class Subscription {

lib/api/model/model.g.dart

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/api/model/model_test.dart

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:test/scaffolding.dart';
3+
import 'package:zulip/api/model/model.dart';
4+
5+
void main() {
6+
group('User', () {
7+
final Map<String, dynamic> baseJson = Map.unmodifiable({
8+
'user_id': 123,
9+
'delivery_email': '[email protected]',
10+
'email': '[email protected]',
11+
'full_name': 'A User',
12+
'date_joined': '2023-04-28',
13+
'is_active': true,
14+
'is_owner': false,
15+
'is_admin': false,
16+
'is_guest': false,
17+
'is_billing_admin': false,
18+
'is_bot': false,
19+
'role': 400,
20+
'timezone': 'UTC',
21+
'avatar_version': 0,
22+
'profile_data': <String, dynamic>{},
23+
});
24+
25+
User mkUser (Map<String, dynamic> specialJson) {
26+
return User.fromJson({ ...baseJson, ...specialJson });
27+
}
28+
29+
test('delivery_email', () {
30+
check(mkUser({'delivery_email': '[email protected]'}).deliveryEmailStaleDoNotUse)
31+
.equals('[email protected]');
32+
});
33+
34+
test('profile_data', () {
35+
check(mkUser({'profile_data': <String, dynamic>{}}).profileData).isNull();
36+
check(mkUser({'profile_data': null}).profileData).isNull();
37+
check(mkUser({'profile_data': {'1': {'value': 'foo'}}}).profileData)
38+
.isNotNull().deepEquals({1: it()});
39+
});
40+
41+
test('is_system_bot', () {
42+
check(mkUser({}).isSystemBot).isNull();
43+
check(mkUser({'is_cross_realm_bot': true}).isSystemBot).equals(true);
44+
check(mkUser({'is_system_bot': true}).isSystemBot).equals(true);
45+
});
46+
});
47+
}

test/example_data.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,7 @@ final InitialSnapshot initialSnapshot = InitialSnapshot(
8383
customProfileFields: [],
8484
subscriptions: [], // TODO add subscriptions to example initial snapshot
8585
maxFileUploadSizeMib: 25,
86+
realmUsers: [],
87+
realmNonActiveUsers: [],
88+
crossRealmBots: [],
8689
);

0 commit comments

Comments
 (0)