createMessage method

({JsonMap message, bool replay}) createMessage(
  1. String authorId,
  2. String threadId,
  3. JsonMap body, {
  4. String origin = 'human',
})

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);
}