-
-
Notifications
You must be signed in to change notification settings - Fork 671
api: Add getSingleMessage binding for GET messages/{message_id} #5465
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
api: Add getSingleMessage binding for GET messages/{message_id} #5465
Conversation
4fee291
to
8c17242
Compare
src/api/messages/getSingleMessage.js
Outdated
export type ServerReaction = {| | ||
+emoji_name: string, | ||
+emoji_code: string, | ||
+reaction_type: ReactionType, | ||
|
||
// Our binding assumes FL 120 (see jsdoc), so we don't have to handle FLs | ||
// <2 that don't provide `user_id`. | ||
+user_id: UserId, | ||
|
||
// Deprecated and "to be removed in a future release": | ||
// https://zulip.com/api/get-message | ||
-user: $ReadOnly<{| | ||
id: UserId, | ||
email: string, | ||
full_name: string, | ||
|
||
// Not observed, so marked optional (doc says required) | ||
is_mirror_dummy?: boolean, | ||
|}>, | ||
|}; | ||
|
||
/** | ||
* The elements of Message.edit_history found in the actual server response. | ||
*/ | ||
export type ServerMessageEdit = {| | ||
// Our binding assumes FL 120 (see jsdoc), so we don't have to support | ||
// the pre-FL 118 shape. This type is written accordingly. | ||
|
||
+prev_content?: string, | ||
+prev_rendered_content?: string, | ||
+prev_rendered_content_version?: number, | ||
+prev_stream?: number, | ||
+prev_topic?: string, | ||
+stream?: number, | ||
+timestamp: number, | ||
+topic?: string, | ||
+user_id: UserId | null, | ||
|}; | ||
|
||
// How `ServerMessage` relates to `Message`, in a way that applies | ||
// uniformly to `Message`'s subtypes. | ||
type ServerMessageOf<M: Message> = {| | ||
...$Exact<M>, | ||
|
||
// Can be null. The server hard-codes client_gravatar: | ||
// https://chat.zulip.org/#narrow/stream/378-api-design/topic/.60client_gravatar.60.20in.20.60messages.2F.7Bmessage_id.7D.60/near/1418337 | ||
+avatar_url: string | null, | ||
|
||
+reactions: $ReadOnlyArray<ServerReaction>, | ||
|
||
// Unlike Message['edit_history'], this can't be `null`. | ||
+edit_history?: $ReadOnlyArray<ServerMessageEdit>, | ||
|}; | ||
|
||
export type ServerMessage = ServerMessageOf<PmMessage> | ServerMessageOf<StreamMessage>; | ||
|
||
// The actual response from the server. We convert the message to a proper | ||
// Message before returning it to application code. | ||
type ServerApiResponseSingleMessage = {| | ||
...$Exact<ApiResponseSuccess>, | ||
-raw_content: string, // deprecated | ||
+message: ServerMessage, | ||
|}; | ||
|
||
/** Exported for tests only. */ | ||
export const migrateMessage = ( | ||
message: ServerMessage, | ||
identity: Identity, | ||
allowEditHistory: boolean, | ||
): Message => ({ | ||
...message, | ||
avatar_url: AvatarURL.fromUserOrBotData({ | ||
rawAvatarUrl: message.avatar_url, | ||
email: message.sender_email, | ||
userId: message.sender_id, | ||
realm: identity.realm, | ||
}), | ||
reactions: message.reactions.map(reaction => { | ||
const { user_id, emoji_name, reaction_type, emoji_code } = reaction; | ||
return { user_id, emoji_name, reaction_type, emoji_code }; | ||
}), | ||
|
||
// Why condition on allowEditHistory? See MessageBase['edit_history']. | ||
edit_history: allowEditHistory | ||
? (message.edit_history: $ReadOnlyArray<MessageEdit> | void) | ||
: null, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A lot of this seems to be in common with getMessages
. What would it look like if we reused more of that, rather than duplicating it?
The main difference I see is that this endpoint only relevantly exists at FL ≥120, and so these types have some simplifications that reflect that. But as far as those are concerned, I think I'd rather have a single implementation that handles both old and new formats, rather than one that does that and a second one that's simplified by only handling the new.
Could well be another difference I'm missing, though. One reason this sort of duplication is costly is that it makes it harder to spot other differences 🙂
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For how we type the data we expect from the server—ServerMessage
—I think we first want to make it match the server API doc, and second want to make it easy to verify that it matches the server API doc. When some reused type feeds into ServerMessage
here and ServerMessage
in getMessages
, there's some risk to both of those goals:
- Maybe the reused type is correct for both places now, but the server isn't bound to keep that true forever. Also, looking closer at each of the two
ServerMessage
types against its respective API doc: even where a reused component is accurate, it's sometimes silly how imprecise it is, because we've reused it in even more places. SeeServerMessage['flags']
in either file. - For that
ServerMessage['flags']
example, note how much effort it takes to actually decipher what precise type is intended, to compare it with the API doc. Command-clicking through in the IDE, and then reading a pretty old comment… In fact, it isn't even explicit: in that comment, we just assume that theMESSAGE_FETCH_COMPLETE
case applies, based on our background knowledge.
On the other hand, I do know that message objects are complex to type, hmm…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's true there are potential trade-offs in trying to deduplicate this code.
I am interested in understanding what the differences are, though. That's partly because for each difference, there's a good chance that one or the other side is a bug. (Or potentially a problem in the API itself.) Do you know whether there are other differences?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense; I'll list the differences below. 🙂
Thinking about why I've been cautious about abstracting too far away from literal details of what we expect from the API (as in an API doc), I'm reminded of a time I thought I'd found a model that applied equally to two things from the server, but then we studied the API more closely and found it was wrong. In particular, a user status in the initial data, and a user status in the update-user-status event. In fact, I think I had one wrong model after another:
- First wrong model: they're both a complete user status object.
- Second wrong model: they're both an update from the then-current user status.
Rather, the event holds an update from the then-current user status, and the initial data holds an update from the "zero" value. See discussion where you found a nice way to model that accurately.
And see an instance where I think we might again be tempted to reach for something reusable, UserTopic
, but it turns out to be wrong (as in my first wrong model, above).
Anyway, in this case it's simpler because these are both "fetch data" endpoints that are outside the event system. I acknowledge that it really would be surprising for a server to send differently shaped messages between GET /messages
and GET /messages/{message_id}
, and I don't think that's likely to change after studying the API more closely.
Anyway, I think these are the differences between ServerMessage
in getMessage
and my new getSingleMessage
file, excluding the FL >=120 simplifications:
- The use of individual
+
s vs.$ReadOnly<…>
, causingtypesEquivalent
to complain of one property being writable while its counterpart isn't. I thinktypesEquivalent
might be wrong in these instances, but I don't know, and it's probably worth erasing these differences anyway. - I marked
ServerReaction['user']
as write-only ingetSingleMessage
, so we don't try to read that deprecated property. This should apply to bothServerReaction
s, I'm sure. - I added
ServerReaction['user']['is_mirror_dummy']
ingetSingleMessage
, because I saw it in the relevant doc. Seeing now thatis_mirror_dummy
is also documented inGET /messages
, we should probably add it ingetMessages
.- I made this optional in
getSingleMessage
, despite the doc saying it's required, because I observed it being absent. We could also mark it optional in the otherServerReaction
, if we expect it to be optional there too, or if we test and find that it's sometimes absent there too, or just because optional would still be accurate, if imprecise, if it turns out to be required there.
- I made this optional in
- In both files,
ServerMessage['avatar_url']
isstring | null
.- In
getMessages
, IIUC, the| null
is because we sendclient_gravatar: true
to servers FL <92, and because we don't sendclient_gravatar: false
for servers FL >=92. - In
getSingleMessage
, the server, which we know is FL >=120 since we're receiving this data, acts like it has acted forgetMessages
since FL 92—except that it doesn't actually accept aclient_gravatar
param. So the presence of| null
there isn't conditional on anything. See discussion.
- In
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for those details!
Yeah, it sounds like it'd be good to deduplicate these. We just discussed in the office, and a file like src/api/rawModelTypes.js
would be a good home for ServerMessage
and the associated types and conversions.
8c17242
to
d4fb353
Compare
…e, etc. getMessages is a binding for the GET /messages endpoint. We're about to add a binding for the GET /message/{message_id} endpoint, which since zulip/zulip@82837304e (FL 120) has given us a full message object in the same shape. For both bindings, we want: (a) a type to describe the data given by the server to represent a message (ServerMessage before this commit), and (b) a function to transform that data into our Message type (migrateMessage before this commit). And it's good to deduplicate both (a) and (b), in part because any differences would be surprising and warrant investigation; see discussion: zulip#5465 (comment) The old code was already clear that it was about fetched data, not data from events, because it was in the file that defined api.getMessages. Keep that meaning intact by making the names more specific: ServerMessage → FetchedMessage (the "server" part is clear from the new file's name) migrateMessage → transformFetchedMessage (the orthogonal change from "migrate" to "transform" should help prevent confusion with src/storage/migrations.js, which has nothing to do with what this function does.) It's useful to remember that this code was developed specifically for these fetch-message(s) endpoints, e.g., for a future investigation aiming to deduplicate most of the corresponding 'message'-event code with this code. (That last bit reminds me that we don't even have a deliberate type for how we expect servers to represent a message in 'message' events. I'll write a TODO for that in the next commit.)
d4fb353
to
598ec8d
Compare
Thanks for the review! Revision pushed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the revision!
This looks good -- just small comments below. Then please merge at will.
edit_history?: $ReadOnlyArray<FetchedMessageEdit>, | ||
|}>; | ||
|
||
export type FetchedMessage = FetchedMessageOf<PmMessage> | FetchedMessageOf<StreamMessage>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old code was already clear that it was about fetched data, not
data from events, because it was in the file that defined
api.getMessages. Keep that meaning intact by making the names more
specific:
ServerMessage → FetchedMessage
(the "server" part is clear from the new file's name)
I like this naming idea!
src/api/messages/getSingleMessage.js
Outdated
-raw_content: string, // deprecated | ||
|
||
// Until we narrow FetchedMessage into its FL 120+ form, FetchedMessage | ||
// will be a bit less precise that we could be here. That's because we |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: s/that we/than we/
src/api/messages/getSingleMessage.js
Outdated
* Gives undefined if the `message` field is missing, which it will be for | ||
* FL <120. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: no extra indentation; this is just a normal paragraph
src/api/messages/getSingleMessage.js
Outdated
auth: Auth, | ||
args: {| | ||
+message_id: number, | ||
+apply_markdown: boolean, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We just hardcode this to true in the actual request, right? So we can drop it from the function's parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yeah, right.
Also, I noticed we were accidentally passing client_gravatar: true
as a param to apiGet
, which this endpoint doesn't accept, as mentioned: #5465 (comment) so I stopped doing that.
This could be easy to miss when we address the TODO(server-5.0) on ServerMessageEdit.
…e, etc. getMessages is a binding for the GET /messages endpoint. We're about to add a binding for the GET /message/{message_id} endpoint, which since zulip/zulip@82837304e (FL 120) has given us a full message object in the same shape. For both bindings, we want: (a) a type to describe the data given by the server to represent a message (ServerMessage before this commit), and (b) a function to transform that data into our Message type (migrateMessage before this commit). And it's good to deduplicate both (a) and (b), in part because any differences would be surprising and warrant investigation; see discussion: zulip#5465 (comment) The old code was already clear that it was about fetched data, not data from events, because it was in the file that defined api.getMessages. Keep that meaning intact by making the names more specific: ServerMessage → FetchedMessage (the "server" part is clear from the new file's name) migrateMessage → transformFetchedMessage (the orthogonal change from "migrate" to "transform" should help prevent confusion with src/storage/migrations.js, which has nothing to do with what this function does.) It's useful to remember that this code was developed specifically for these fetch-message(s) endpoints, e.g., for a future investigation aiming to deduplicate most of the corresponding 'message'-event code with this code. (That last bit reminds me that we don't even have a deliberate type for how we expect servers to represent a message in 'message' events. I'll write a TODO for that in the next commit.)
We'll use this for zulip#5306; see the plan in discussion: https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/.23M5306.20Follow.20.2Fnear.2F.20links.20through.20topic.20moves.2Frenames/near/1407930 In particular, we want the stream and topic for a stream message that might not be in our data structures. We'll use this endpoint to fetch that information.
598ec8d
to
c35b883
Compare
Thanks for the review! Done. |
We'll use this for #5306; see the plan in discussion:
https://chat.zulip.org/#narrow/stream/243-mobile-team/topic/.23M5306.20Follow.20.2Fnear.2F.20links.20through.20topic.20moves.2Frenames/near/1407930
In particular, we want the stream and topic for a stream message
that we fetch using this endpoint (which we'd do as a fallback for
when we don't already have the message in our data structures.)