///|
/// Fetch a channel by snowflake.
pub async fn Client::get_channel(
  self : Client,
  channel_id : @model.ChannelId,
) -> @model.Channel raise DiscordHttpError {
  decode(self.request(GetChannel(channel_id~)))
}

///|
/// Modify common channel settings. `archived`, `auto_archive_duration`,
/// `locked`, `invitable`, and `applied_tags` only apply when the channel is
/// a thread; `flags` covers both threads (`PINNED`) and forum/media channels
/// (`REQUIRE_TAG`). `typ` converts between text and announcement channels;
/// `bitrate`, `user_limit`, `rtc_region` (null means automatic), and
/// `video_quality_mode` apply to voice and stage channels; the
/// `available_tags`/`default_*` family applies to forum and media channels;
/// `icon` (base64 image data) renames a group DM.
pub async fn Client::modify_channel(
  self : Client,
  channel_id : @model.ChannelId,
  name? : String,
  typ? : @model.ChannelType,
  topic? : String,
  clear_topic? : Bool = false,
  nsfw? : Bool,
  parent_id? : @model.ChannelId,
  clear_parent_id? : Bool = false,
  rate_limit_per_user? : Int,
  position? : Int,
  bitrate? : Int,
  user_limit? : Int,
  permission_overwrites? : Array[@model.Overwrite],
  rtc_region? : String,
  clear_rtc_region? : Bool = false,
  video_quality_mode? : @model.VideoQualityMode,
  clear_video_quality_mode? : Bool = false,
  default_auto_archive_duration? : @model.ThreadAutoArchiveDuration,
  available_tags? : Array[@model.ForumTagRequest],
  default_reaction_emoji? : @model.DefaultReaction,
  clear_default_reaction_emoji? : Bool = false,
  default_thread_rate_limit_per_user? : Int,
  default_sort_order? : @model.SortOrderType,
  clear_default_sort_order? : Bool = false,
  default_forum_layout? : @model.ForumLayoutType,
  icon? : String,
  flags? : @model.ChannelFlags,
  archived? : Bool,
  auto_archive_duration? : @model.ThreadAutoArchiveDuration,
  locked? : Bool,
  invitable? : Bool,
  applied_tags? : Array[@model.GenericId],
  audit_reason? : String,
) -> @model.Channel raise DiscordHttpError {
  validate_length("channel name", name, min=1, max=100)
  validate_length("channel topic", topic, max=4096)
  validate_range("rate_limit_per_user", rate_limit_per_user, min=0, max=21600)
  let body = @model.ObjBuilder()
    .opt("name", name)
    .opt("type", typ)
    .und("topic", patch_nullable("topic", topic, clear_topic))
    .opt("nsfw", nsfw)
    .und("parent_id", patch_nullable("parent_id", parent_id, clear_parent_id))
    .opt("rate_limit_per_user", rate_limit_per_user)
    .opt("position", position)
    .opt("bitrate", bitrate)
    .opt("user_limit", user_limit)
    .opt("permission_overwrites", permission_overwrites)
    .und(
      "rtc_region",
      patch_nullable("rtc_region", rtc_region, clear_rtc_region),
    )
    .und(
      "video_quality_mode",
      patch_nullable(
        "video_quality_mode", video_quality_mode, clear_video_quality_mode,
      ),
    )
    .opt("default_auto_archive_duration", default_auto_archive_duration)
    .opt("available_tags", available_tags)
    .und(
      "default_reaction_emoji",
      patch_nullable(
        "default_reaction_emoji", default_reaction_emoji, clear_default_reaction_emoji,
      ),
    )
    .opt(
      "default_thread_rate_limit_per_user", default_thread_rate_limit_per_user,
    )
    .und(
      "default_sort_order",
      patch_nullable(
        "default_sort_order", default_sort_order, clear_default_sort_order,
      ),
    )
    .opt("default_forum_layout", default_forum_layout)
    .opt("icon", icon)
    .opt("flags", flags)
    .opt("archived", archived)
    .opt("auto_archive_duration", auto_archive_duration)
    .opt("locked", locked)
    .opt("invitable", invitable)
    .opt("applied_tags", applied_tags)
    .build()
  decode(self.request(ModifyChannel(channel_id~), body~, audit_reason?))
}

///|
/// Delete or close a channel.
pub async fn Client::delete_channel(
  self : Client,
  channel_id : @model.ChannelId,
  audit_reason? : String,
) -> @model.Channel raise DiscordHttpError {
  decode(self.request(DeleteChannel(channel_id~), audit_reason?))
}

///|
/// Trigger the typing indicator for a channel.
pub async fn Client::trigger_typing(
  self : Client,
  channel_id : @model.ChannelId,
) -> Unit raise DiscordHttpError {
  self.request(TriggerTypingIndicator(channel_id~)) |> ignore
}

///|
/// Pin a message in a channel.
pub async fn Client::pin_message(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  audit_reason? : String,
) -> Unit raise DiscordHttpError {
  self.request(PinMessage(channel_id~, message_id~), audit_reason?) |> ignore
}

///|
/// Remove a message pin from a channel.
pub async fn Client::unpin_message(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  audit_reason? : String,
) -> Unit raise DiscordHttpError {
  self.request(UnpinMessage(channel_id~, message_id~), audit_reason?) |> ignore
}

///|
/// List every pinned message in a channel.
pub async fn Client::get_pinned_messages(
  self : Client,
  channel_id : @model.ChannelId,
) -> Array[@model.Message] raise DiscordHttpError {
  decode(self.request(GetPinnedMessages(channel_id~)))
}

///|
/// Fetch one page of channel pins, newest first. Use the final entry's
/// `pinned_at` as `before` while `has_more` is true.
pub async fn Client::get_channel_pins(
  self : Client,
  channel_id : @model.ChannelId,
  before? : @model.Timestamp,
  limit? : Int,
) -> @model.ChannelPins raise DiscordHttpError {
  if limit is Some(value) && (value < 1 || value > 50) {
    raise DiscordHttpError::Validation(
      message="channel pin limit must be between 1 and 50",
    )
  }
  let params : Array[String] = []
  if before is Some(value) {
    params.push("before=\{percent_encode_query_text(value.to_string())}")
  }
  if limit is Some(value) {
    params.push("limit=\{value}")
  }
  let query = if params.is_empty() { "" } else { "?" + params.join("&") }
  decode(self.request(GetChannelPins(channel_id~, query~)))
}

///|
/// Create or replace a channel permission overwrite. Omitted `allow`/`deny`
/// default to no permissions on the Discord side.
pub async fn Client::edit_channel_permissions(
  self : Client,
  channel_id : @model.ChannelId,
  overwrite_id : @model.GenericId,
  typ : @model.OverwriteType,
  allow? : @model.Permissions,
  deny? : @model.Permissions,
  audit_reason? : String,
) -> Unit raise DiscordHttpError {
  let body = @model.ObjBuilder()
    .opt("allow", allow)
    .opt("deny", deny)
    .field("type", typ)
    .build()
  self.request(
    EditChannelPermissions(channel_id~, overwrite_id~),
    body~,
    audit_reason?,
  )
  |> ignore
}

///|
/// Delete a channel permission overwrite.
pub async fn Client::delete_channel_permission(
  self : Client,
  channel_id : @model.ChannelId,
  overwrite_id : @model.GenericId,
  audit_reason? : String,
) -> Unit raise DiscordHttpError {
  self.request(
    DeleteChannelPermission(channel_id~, overwrite_id~),
    audit_reason?,
  )
  |> ignore
}

///|
/// List invites created for a channel.
pub async fn Client::get_channel_invites(
  self : Client,
  channel_id : @model.ChannelId,
) -> Array[@model.Invite] raise DiscordHttpError {
  decode(self.request(GetChannelInvites(channel_id~)))
}

///|
/// Create an invite for a channel. `target_type` makes a voice-channel
/// invite open a stream (`Stream`, requires `target_user_id`) or an embedded
/// application (`EmbeddedApplication`, requires `target_application_id`).
/// `role_ids` grants roles to accepting users (requires `MANAGE_ROLES`);
/// `target_users_file` restricts who may accept it (a CSV of user ids, sent
/// as multipart form data).
pub async fn Client::create_channel_invite(
  self : Client,
  channel_id : @model.ChannelId,
  max_age? : Int,
  max_uses? : Int,
  temporary? : Bool,
  unique? : Bool,
  target_type? : @model.InviteTargetType,
  target_user_id? : @model.UserId,
  target_application_id? : @model.ApplicationId,
  role_ids? : Array[@model.RoleId],
  target_users_file? : FileUpload,
  audit_reason? : String,
) -> @model.Invite raise DiscordHttpError {
  if target_type is Some(Stream) && target_user_id is None {
    raise Validation(message="target_type Stream requires target_user_id")
  }
  if target_type is Some(EmbeddedApplication) && target_application_id is None {
    raise Validation(
      message="target_type EmbeddedApplication requires target_application_id",
    )
  }
  let body = @model.ObjBuilder()
    .opt("max_age", max_age)
    .opt("max_uses", max_uses)
    .opt("temporary", temporary)
    .opt("unique", unique)
    .opt("target_type", target_type)
    .opt("target_user_id", target_user_id)
    .opt("target_application_id", target_application_id)
    .opt("role_ids", role_ids)
    .build()
  let files = target_users_file.map(file => [file])
  decode(
    self.request(CreateChannelInvite(channel_id~), body~, files?, audit_reason?),
  )
}

///|
fn follow_announcement_channel_body(
  webhook_channel_id : @model.ChannelId,
) -> Json {
  @model.ObjBuilder().field("webhook_channel_id", webhook_channel_id).build()
}

///|
/// Follow an announcement channel, forwarding its messages to another
/// channel through the returned webhook.
pub async fn Client::follow_announcement_channel(
  self : Client,
  channel_id : @model.ChannelId,
  webhook_channel_id : @model.ChannelId,
  audit_reason? : String,
) -> @model.FollowedChannel raise DiscordHttpError {
  let body = follow_announcement_channel_body(webhook_channel_id)
  decode(
    self.request(FollowAnnouncementChannel(channel_id~), body~, audit_reason?),
  )
}

///|
fn set_voice_channel_status_body(
  status : String?,
) -> Json raise DiscordHttpError {
  validate_length("voice channel status", status, max=500)
  @model.ObjBuilder()
  .field("status", @model.Nullable::from_option(status))
  .build()
}

///|
/// Set a voice channel's status, or clear it by omitting `status`.
pub async fn Client::set_voice_channel_status(
  self : Client,
  channel_id : @model.ChannelId,
  status? : String,
  audit_reason? : String,
) -> Unit raise DiscordHttpError {
  let body = set_voice_channel_status_body(status)
  self.request(SetVoiceChannelStatus(channel_id~), body~, audit_reason?)
  |> ignore
}

///|
fn group_dm_add_recipient_body(access_token : String, nick : String?) -> Json {
  @model.ObjBuilder()
  .field("access_token", access_token)
  .opt("nick", nick)
  .build()
}

///|
/// Add a user to a group DM. `access_token` is the recipient's OAuth2 token
/// with the `gdm.join` scope and is sent as a JSON body field.
pub async fn Client::group_dm_add_recipient(
  self : Client,
  channel_id : @model.ChannelId,
  user_id : @model.UserId,
  access_token : String,
  nick? : String,
) -> Unit raise DiscordHttpError {
  let body = group_dm_add_recipient_body(access_token, nick)
  self.request(GroupDmAddRecipient(channel_id~, user_id~), body~) |> ignore
}

///|
/// Remove a user from a group DM.
pub async fn Client::group_dm_remove_recipient(
  self : Client,
  channel_id : @model.ChannelId,
  user_id : @model.UserId,
) -> Unit raise DiscordHttpError {
  self.request(GroupDmRemoveRecipient(channel_id~, user_id~)) |> ignore
}

///|
/// Add a reaction. Pass the emoji in raw form: a unicode emoji such as
/// "\u{1F525}", or name:id for custom emoji. It is percent-encoded here.
pub async fn Client::create_reaction(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  emoji : String,
) -> Unit raise DiscordHttpError {
  let emoji = percent_encode_query_text(emoji)
  self.request(CreateReaction(channel_id~, message_id~, emoji~)) |> ignore
}

///|
/// Delete the bot user's reaction. The emoji is passed raw (unicode or
/// name:id) and percent-encoded here.
pub async fn Client::delete_own_reaction(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  emoji : String,
) -> Unit raise DiscordHttpError {
  let emoji = percent_encode_query_text(emoji)
  self.request(DeleteOwnReaction(channel_id~, message_id~, emoji~)) |> ignore
}

///|
/// Delete another user's reaction. The emoji is passed raw (unicode or
/// name:id) and percent-encoded here.
pub async fn Client::delete_user_reaction(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  emoji : String,
  user_id : @model.UserId,
) -> Unit raise DiscordHttpError {
  let emoji = percent_encode_query_text(emoji)
  self.request(DeleteUserReaction(channel_id~, message_id~, emoji~, user_id~))
  |> ignore
}

///|
/// Delete every reaction from a message.
pub async fn Client::delete_all_reactions(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
) -> Unit raise DiscordHttpError {
  self.request(DeleteAllReactions(channel_id~, message_id~)) |> ignore
}

///|
/// Delete every reaction matching an emoji. The emoji is passed raw (unicode
/// or name:id) and percent-encoded here.
pub async fn Client::delete_all_reactions_for_emoji(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  emoji : String,
) -> Unit raise DiscordHttpError {
  let emoji = percent_encode_query_text(emoji)
  self.request(DeleteAllReactionsForEmoji(channel_id~, message_id~, emoji~))
  |> ignore
}

///|
/// List users who reacted with an emoji. The emoji is passed raw (unicode or
/// name:id) and percent-encoded here. `typ` selects normal or burst (super)
/// reactions.
pub async fn Client::get_reactions(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  emoji : String,
  typ? : @model.ReactionType,
  limit? : Int,
  after? : @model.UserId,
) -> Array[@model.User] raise DiscordHttpError {
  let emoji = percent_encode_query_text(emoji)
  validate_range("reaction limit", limit, min=1, max=100)
  let params : Array[String] = []
  if typ is Some(value) {
    params.push("type=\{value.to_int()}")
  }
  if limit is Some(value) {
    params.push("limit=\{value}")
  }
  if after is Some(value) {
    params.push("after=\{value}")
  }
  let query = if params.length() == 0 { "" } else { "?" + params.join("&") }
  decode(self.request(GetReactions(channel_id~, message_id~, emoji~, query~)))
}

///|
/// Start a thread whose initial message is an existing channel message.
pub async fn Client::start_thread_from_message(
  self : Client,
  channel_id : @model.ChannelId,
  message_id : @model.MessageId,
  name : String,
  auto_archive_duration? : @model.ThreadAutoArchiveDuration,
  rate_limit_per_user? : Int,
  audit_reason? : String,
) -> @model.Channel raise DiscordHttpError {
  validate_length("thread name", Some(name), min=1, max=100)
  validate_range("rate_limit_per_user", rate_limit_per_user, min=0, max=21600)
  let body = @model.ObjBuilder()
    .field("name", name)
    .opt("auto_archive_duration", auto_archive_duration)
    .opt("rate_limit_per_user", rate_limit_per_user)
    .build()
  decode(
    self.request(
      StartThreadFromMessage(channel_id~, message_id~),
      body~,
      audit_reason?,
    ),
  )
}

///|
/// Start a thread that is not attached to an existing message.
///
/// Discord currently defaults `typ` to a private thread and plans to make the
/// field required in a future API version, so pass it explicitly.
pub async fn Client::start_thread_without_message(
  self : Client,
  channel_id : @model.ChannelId,
  name : String,
  typ? : @model.ChannelType,
  auto_archive_duration? : @model.ThreadAutoArchiveDuration,
  invitable? : Bool,
  rate_limit_per_user? : Int,
  audit_reason? : String,
) -> @model.Channel raise DiscordHttpError {
  validate_length("thread name", Some(name), min=1, max=100)
  validate_range("rate_limit_per_user", rate_limit_per_user, min=0, max=21600)
  let body = @model.ObjBuilder()
    .field("name", name)
    .opt("type", typ)
    .opt("auto_archive_duration", auto_archive_duration)
    .opt("invitable", invitable)
    .opt("rate_limit_per_user", rate_limit_per_user)
    .build()
  decode(
    self.request(StartThreadWithoutMessage(channel_id~), body~, audit_reason?),
  )
}

///|
/// Start a thread in a forum or media channel. The thread's starter message
/// is created in the same request from the message fields; at least one of
/// `content`, `embeds`, `components`, `sticker_ids`, or `files` must be
/// provided.
pub async fn Client::start_thread_in_forum_or_media_channel(
  self : Client,
  channel_id : @model.ChannelId,
  name : String,
  content? : String,
  embeds? : Array[@model.Embed],
  components? : Array[@model.Component],
  sticker_ids? : Array[@model.StickerId],
  files? : Array[FileUpload],
  flags? : @model.MessageFlags,
  allowed_mentions? : @model.AllowedMentions,
  auto_archive_duration? : @model.ThreadAutoArchiveDuration,
  rate_limit_per_user? : Int,
  applied_tags? : Array[@model.GenericId],
  audit_reason? : String,
) -> @model.Channel raise DiscordHttpError {
  validate_length("thread name", Some(name), min=1, max=100)
  validate_range("rate_limit_per_user", rate_limit_per_user, min=0, max=21600)
  validate_content(content)
  if content is None &&
    embeds is None &&
    components is None &&
    sticker_ids is None &&
    files is None {
    raise Validation(
      message="one of content/embeds/components/sticker_ids/files is required",
    )
  }
  if components is Some(values) && @model.requires_components_v2(values) {
    raise Validation(
      message="forum thread starter messages only allow the SUPPRESS_EMBEDS and SUPPRESS_NOTIFICATIONS flags, so Components V2 is unavailable",
    )
  }
  let message = self.with_channel_allowed_mentions(
    @model.ObjBuilder()
    .opt("content", content)
    .opt("embeds", embeds)
    .opt("components", components)
    .opt("sticker_ids", sticker_ids)
    .opt("flags", flags),
    allowed_mentions,
  )
  if files is Some(fs) && fs.length() > 0 {
    message.field("attachments", attachments_json(fs)) |> ignore
  }
  let body = @model.ObjBuilder()
    .field("name", name)
    .opt("auto_archive_duration", auto_archive_duration)
    .opt("rate_limit_per_user", rate_limit_per_user)
    .opt("applied_tags", applied_tags)
    .field("message", message.build())
    .build()
  decode(
    self.request(
      StartThreadWithoutMessage(channel_id~),
      body~,
      audit_reason?,
      files?,
    ),
  )
}

///|
/// Join the current user to a thread channel.
pub async fn Client::join_thread(
  self : Client,
  channel_id : @model.ChannelId,
) -> Unit raise DiscordHttpError {
  self.request(JoinThread(channel_id~)) |> ignore
}

///|
/// Remove the current user from a thread channel.
pub async fn Client::leave_thread(
  self : Client,
  channel_id : @model.ChannelId,
) -> Unit raise DiscordHttpError {
  self.request(LeaveThread(channel_id~)) |> ignore
}

///|
fn thread_members_query(
  with_member : Bool?,
  after : @model.UserId?,
  limit : Int?,
) -> String {
  let params : Array[String] = []
  if with_member is Some(value) {
    params.push("with_member=\{value}")
  }
  if after is Some(value) {
    params.push("after=\{value}")
  }
  if limit is Some(value) {
    params.push("limit=\{value}")
  }
  if params.length() == 0 {
    ""
  } else {
    "?" + params.join("&")
  }
}

///|
fn archived_threads_query(before : String?, limit : Int?) -> String {
  let params : Array[String] = []
  if before is Some(value) {
    // ISO8601 offsets contain `+`, which decodes to a space in query strings.
    params.push("before=" + value.replace(old="+", new="%2B"))
  }
  if limit is Some(value) {
    params.push("limit=\{value}")
  }
  if params.length() == 0 {
    ""
  } else {
    "?" + params.join("&")
  }
}

///|
/// List members of a thread channel.
pub async fn Client::list_thread_members(
  self : Client,
  channel_id : @model.ChannelId,
  with_member? : Bool,
  after? : @model.UserId,
  limit? : Int,
) -> Array[@model.ThreadMember] raise DiscordHttpError {
  validate_range("thread member limit", limit, min=1, max=100)
  let query = thread_members_query(with_member, after, limit)
  decode(self.request(ListThreadMembers(channel_id~, query~)))
}

///|
/// Fetch a user's membership record for a thread channel.
pub async fn Client::get_thread_member(
  self : Client,
  channel_id : @model.ChannelId,
  user_id : @model.UserId,
  with_member? : Bool,
) -> @model.ThreadMember raise DiscordHttpError {
  let query = thread_members_query(with_member, None, None)
  decode(self.request(GetThreadMember(channel_id~, user_id~, query~)))
}

///|
/// Add a user to a thread channel.
pub async fn Client::add_thread_member(
  self : Client,
  channel_id : @model.ChannelId,
  user_id : @model.UserId,
) -> Unit raise DiscordHttpError {
  self.request(AddThreadMember(channel_id~, user_id~)) |> ignore
}

///|
/// Remove a user from a thread channel.
pub async fn Client::remove_thread_member(
  self : Client,
  channel_id : @model.ChannelId,
  user_id : @model.UserId,
) -> Unit raise DiscordHttpError {
  self.request(RemoveThreadMember(channel_id~, user_id~)) |> ignore
}

///|
/// List public archived threads in a channel.
pub async fn Client::list_public_archived_threads(
  self : Client,
  channel_id : @model.ChannelId,
  before? : @model.Timestamp,
  limit? : Int,
) -> @model.ArchivedThreads raise DiscordHttpError {
  let before_string = match before {
    Some(value) => Some(value.to_string())
    None => None
  }
  let query = archived_threads_query(before_string, limit)
  decode(self.request(ListPublicArchivedThreads(channel_id~, query~)))
}

///|
/// List private archived threads in a channel.
pub async fn Client::list_private_archived_threads(
  self : Client,
  channel_id : @model.ChannelId,
  before? : @model.Timestamp,
  limit? : Int,
) -> @model.ArchivedThreads raise DiscordHttpError {
  let before_string = match before {
    Some(value) => Some(value.to_string())
    None => None
  }
  let query = archived_threads_query(before_string, limit)
  decode(self.request(ListPrivateArchivedThreads(channel_id~, query~)))
}

///|
/// List private archived threads joined by the current user.
pub async fn Client::list_joined_private_archived_threads(
  self : Client,
  channel_id : @model.ChannelId,
  before? : @model.ChannelId,
  limit? : Int,
) -> @model.ArchivedThreads raise DiscordHttpError {
  let before_string = match before {
    Some(value) => Some(value.to_string())
    None => None
  }
  let query = archived_threads_query(before_string, limit)
  decode(self.request(ListJoinedPrivateArchivedThreads(channel_id~, query~)))
}

///|
/// List every active thread in a guild.
pub async fn Client::list_active_guild_threads(
  self : Client,
  guild_id : @model.GuildId,
) -> @model.ActiveGuildThreads raise DiscordHttpError {
  decode(self.request(ListActiveGuildThreads(guild_id~)))
}