createMessage method
Creates a message of authorId in threadId from a MessageCreate body (04 5.1):
returns the message and whether it was a replay of the same client_msg_id.
Throws FakeProblem: 404 for a thread the author does not see, 403 forbidden below
write, 403 channel_read_only, 400 validation_failed for a client_msg_id that is
not a UUIDv7, 422 idempotency_key_reuse for the same ID with a different body.
Implementation
({JsonMap message, bool replay}) createMessage(
String authorId,
String threadId,
JsonMap body, {
String origin = 'human',
}) {
final thread = visibleThread(authorId, threadId);
final channelId = thread['channel_id']! as String;
final channel = channels[channelId]!;
final level = channel.access[authorId];
if (level == 'read') throw const FakeProblem(403, 'forbidden');
if (channel.json['read_only'] == true) {
throw const FakeProblem(403, 'channel_read_only');
}
final cmid = body['client_msg_id']! as String;
if (!_uuid7.hasMatch(cmid)) {
throw const FakeProblem(
400,
'validation_failed',
errors: [
{'pointer': '/client_msg_id', 'code': 'invalid_format'},
],
);
}
// The canonical request: thread, content, reply reference, attachments (04 5.1 item 3).
final hash = sha256
.convert(
utf8.encode(
jsonEncode([
threadId,
body['content_md'] ?? '',
body['reply_to_message_id'] ?? '',
body['attachment_file_ids'] ?? const <Object?>[],
]),
),
)
.toString();
final known = _clientMsgIds['$authorId|$cmid'];
if (known != null) {
if (known.hash != hash) {
throw const FakeProblem(422, 'idempotency_key_reuse');
}
return (message: messages[known.id]!, replay: true);
}
final author = people[authorId]!;
if (thread['state'] == 'closed') {
_reopen(threadId, authorId);
_systemLine(threadId, 'thread.reopened', authorId);
}
final seq = (thread['last_message_seq']! as int) + 1;
final id = nextId();
final mentions = [
...?(body['mention_ids'] as List<Object?>?)?.cast<String>(),
// 03 content format item 4: the server resolves `@handle` tokens of principals that
// see the channel; the fake does it only when a test asks (M4-21).
if (resolveMentions && body['mention_ids'] == null)
..._handlesIn(body['content_md'] as String? ?? '', channelId),
];
final attachments = _attachable(authorId, channelId, [
...?(body['attachment_file_ids'] as List<Object?>?)?.cast<String>(),
], kind: body['kind'] as String? ?? 'text');
final message = <String, Object?>{
'id': id,
'thread_id': threadId,
'channel_id': channelId,
'thread_seq': seq,
'author': author.ref,
'client_msg_id': cmid,
'origin': origin,
'kind': body['kind'] ?? 'text',
'content_md': body['content_md'] ?? '',
'content_format': 1,
'reply_to_message_id': ?body['reply_to_message_id'],
'silent': body['silent'] ?? false,
'attachments': attachments,
'mention_ids': mentions,
'reactions': <Object?>[],
'pinned': false,
'components': <Object?>[],
'version': 1,
'created_at': stamp(),
};
messages[id] = message;
_attach(id, attachments);
_threadMessages[threadId]!.add(id);
_clientMsgIds['$authorId|$cmid'] = (id: id, hash: hash);
thread
..['last_message_seq'] = seq
..['message_count'] = (thread['message_count']! as int) + 1
..['last_activity_at'] = message['created_at'];
for (final check in List.of(triggerChecks)) {
final state = check(message, author);
if (state == null) continue;
message
..['trigger_state'] = state
..['trigger_state_at'] = message['created_at'];
break;
}
append(
'message.created',
resource: {'kind': 'message', 'id': id, 'version': message['version']},
scope: _scopeOf(threadId),
payload: {'message': message},
actor: authorId,
clientMsgId: cmid,
);
for (final hook in List.of(messageHooks)) {
hook(message, author);
}
// 13 3.2: a person who writes in a thread (not the main thread) follows it.
if (origin == 'human') _follow(authorId, threadId);
if (notifyOnMessages) notify(id);
if (body['clear_draft'] == true) deleteDraft(authorId, threadId);
return (message: message, replay: false);
}