defmodule Nostrum.Api.Thread do @moduledoc """ Functions for interacting with the Discord API's thread endpoints. Endpoints related to threads in the Discord Channels API: https://discord.com/developers/docs/resources/channel#start-thread-from-message """ @moduledoc since: "0.10.1" alias Nostrum.Api alias Nostrum.Api.Helpers alias Nostrum.Constants alias Nostrum.Struct.Channel alias Nostrum.Struct.Guild alias Nostrum.Struct.Guild.AuditLogEntry alias Nostrum.Struct.Message alias Nostrum.Struct.ThreadMember alias Nostrum.Struct.User alias Nostrum.Util import Api.Helpers, only: [has_files: 1] @doc """ Add a user to a thread, requires the ability to send messages in the thread. """ @doc since: "1.x.x" @spec add_member(Channel.id(), User.id()) :: {:ok} | Api.error() def add_member(thread_id, user_id) do Api.request(:put, Constants.thread_member(thread_id, user_id)) end @doc """ Returns a thread member object for the specified user if they are a member of the thread """ @doc since: "1.x.x" @spec member(Channel.id(), User.id()) :: {:ok, ThreadMember.t()} | Api.error() def member(thread_id, user_id) do Api.request(:get, Constants.thread_member(thread_id, user_id)) |> Helpers.handle_request_with_decode({:struct, ThreadMember}) end @doc """ Returns a list of thread members for the specified thread. This endpoint is restricted according to whether the `GUILD_MEMBERS` privileged intent is enabled. """ @doc since: "1.x.x" @spec members(Channel.id()) :: {:ok, [ThreadMember.t()]} | Api.error() def members(thread_id) do Api.request(:get, Constants.thread_members(thread_id)) |> Helpers.handle_request_with_decode({:list, {:struct, ThreadMember}}) end @doc """ Join an existing thread, requires that the thread is not archived. """ @doc since: "1.x.x" @spec join(Channel.id()) :: {:ok} | Api.error() def join(thread_id) do Api.request(:put, Constants.thread_member_me(thread_id)) end @doc """ Leave a thread, requires that the thread is not archived. """ @doc since: "1.x.x" @spec leave(Channel.id()) :: {:ok} | Api.error() def leave(thread_id) do Api.request(:delete, Constants.thread_member_me(thread_id)) end @doc """ Return all active threads for the current guild. Response body is a map with the following keys: - `threads`: A list of channel objects. - `members`: A list of `ThreadMember` objects, one for each returned thread the current user has joined. """ @doc since: "1.x.x" @spec list(Guild.id()) :: {:ok, %{threads: [Channel.t()], members: [ThreadMember.t()]}} | Api.error() def list(guild_id) do res = Api.request(:get, Constants.guild_active_threads(guild_id)) |> Helpers.handle_request_with_decode() case res do {:ok, %{threads: channels, members: thread_members}} -> map = %{ threads: Util.cast(channels, {:list, {:struct, Channel}}), members: Util.cast(thread_members, {:list, {:struct, ThreadMember}}) } {:ok, map} {:error, e} -> {:error, e} end end @doc """ Same as `public_archived_threads/2`, but only returns private threads that the current user has joined. """ @doc since: "1.x.x" @spec joined_private_archived_threads(Channel.id(), Api.options()) :: {:ok, %{threads: [Channel.t()], members: [ThreadMember.t()], has_more: boolean()}} | Api.error() def joined_private_archived_threads(channel_id, options \\ []) def joined_private_archived_threads(channel_id, options) when is_map(options) do Constants.private_joined_archived_threads(channel_id) |> list_archived_threads(Map.to_list(options)) end def joined_private_archived_threads(channel_id, options) when is_list(options) do Constants.private_joined_archived_threads(channel_id) |> list_archived_threads(options) end defp list_archived_threads(route, options) do options = options |> Helpers.maybe_convert_date_time(:before) res = Api.request(%{ method: :get, route: route, body: "", params: options, headers: [] }) |> Helpers.handle_request_with_decode() case res do {:ok, %{threads: channels, members: thread_members, has_more: has_more}} -> map = %{ threads: Util.cast(channels, {:list, {:struct, Channel}}), members: Util.cast(thread_members, {:list, {:struct, ThreadMember}}), has_more: has_more } {:ok, map} {:error, e} -> {:error, e} end end @doc """ Same as `public_archived_threads/2`, but for private threads instead of public. """ @doc since: "1.x.x" @spec private_archived_threads(Channel.id(), Api.options()) :: {:ok, %{threads: [Channel.t()], members: [ThreadMember.t()], has_more: boolean()}} | Api.error() def private_archived_threads(channel_id, options \\ []) def private_archived_threads(channel_id, options) when is_map(options) do Constants.private_archived_threads(channel_id) |> list_archived_threads(Map.to_list(options)) end def private_archived_threads(channel_id, options) when is_list(options) do Constants.private_archived_threads(channel_id) |> list_archived_threads(options) end @doc """ Returns a list of archived threads for a given channel. Threads are sorted by the `archive_timestamp` field, in descending order. ## Response body Response body is a map with the following keys: - `threads`: A list of channel objects. - `members`: A list of `ThreadMember` objects, one for each returned thread the current user has joined. - `has_more`: A boolean indicating whether there are more archived threads that can be fetched. ## Options - `before`: Returns threads before this timestamp, can be either a `DateTime` or [ISO8601 timestamp](`DateTime.to_iso8601/3`). - `limit`: Optional maximum number of threads to return. """ @doc since: "1.x.x" @spec public_archived_threads(Channel.id(), Api.options()) :: {:ok, %{threads: [Channel.t()], members: [ThreadMember.t()], has_more: boolean()}} | Api.error() def public_archived_threads(channel_id, options \\ []) def public_archived_threads(channel_id, options) when is_map(options) do Constants.public_archived_threads(channel_id) |> list_archived_threads(Map.to_list(options)) end def public_archived_threads(channel_id, options) when is_list(options) do Constants.public_archived_threads(channel_id) |> list_archived_threads(options) end @doc """ Removes another user from a thread, requires that the thread is not archived. Also requires the `MANAGE_THREADS` permission, or the creator of the thread if the thread is private. """ @doc since: "1.x.x" @spec remove_member(Channel.id(), User.id()) :: {:ok} | Api.error() def remove_member(thread_id, user_id) do Api.request(:delete, Constants.thread_member(thread_id, user_id)) end @type thread_without_message_params :: %{ required(:name) => String.t(), required(:type) => non_neg_integer(), optional(:auto_archive_duration) => 60 | 1440 | 4320 | 10_080, optional(:invitable) => boolean(), optional(:rate_limit_per_user) => 0..21_600 } @doc """ Create a thread on a channel without an associated message. If successful, returns `{:ok, Channel}`. Otherwise returns a `t:Nostrum.Api.error/0`. An optional `reason` argument can be given for the audit log. ## Options - `name`: Name of the thread, max 100 characters. - `type`: Type of thread, can be either 11 (`GUILD_PUBLIC_THREAD`) or 12 (`GUILD_PRIVATE_THREAD`). - `auto_archive_duration`: Duration in minutes to auto-archive the thread after it has been inactive, can be set to 60, 1440, 4320, or 10080. - `invitable`: whether non-moderators can add other non-moderators to a thread; only available when creating a private thread defaults to `false`. - `rate_limit_per_user`: Rate limit per user in seconds, can be set to any value in `0..21600`. """ @doc since: "1.x.x" @spec create(Channel.id(), thread_without_message_params, AuditLogEntry.reason()) :: {:ok, Channel.t()} | Api.error() def create(channel_id, options, reason \\ nil) do Api.request(%{ method: :post, route: Constants.thread_without_message(channel_id), body: options, params: [], headers: Helpers.maybe_add_reason(reason) }) |> Helpers.handle_request_with_decode({:struct, Channel}) end @doc """ Create a new thread in a forum channel. If successful, returns `{:ok, Channel}`. Otherwise returns a `t:Nostrum.Api.error/0`. An optional `reason` argument can be given for the audit log. ## Options - `name`: Name of the thread, max 100 characters. - `auto_archive_duration`: Duration in minutes to auto-archive the thread after it has been inactive, can be set to 60, 1440, 4320, or 10080. - `rate_limit_per_user`: Rate limit per user in seconds, can be set to any value in `0..21600`. - `applied_tags`: An array of tag ids to apply to the thread. - `message`: The first message in the created thread. ### Thread Message Options - `content`: The content of the message. - `embeds`: A list of embeds. - `allowed_mentions`: Allowed mentions object. - `components`: A list of components. - `sticker_ids`: A list of sticker ids. - `:files` - a list of files where each element is the same format as the `:file` option. If both `:file` and `:files` are specified, `:file` will be prepended to the `:files` list. At least one of `content`, `embeds`, `sticker_ids`, or `files` must be specified. """ @doc since: "1.x.x" @spec create_in_forum(Channel.id(), map(), AuditLogEntry.reason()) :: {:ok, Channel.t()} | Api.error() def create_in_forum(channel_id, options, reason \\ nil) def create_in_forum(channel_id, %{message: data} = body, reason) when has_files(data) do # done this way to avoid breaking changes to support audit log reasons in multipart requests boundary = Helpers.generate_boundary() {files, json} = Helpers.combine_files(body) |> Helpers.pop_files() body = Jason.encode_to_iodata!(json) headers = Helpers.maybe_add_reason(reason, [ {"content-type", "multipart/form-data; boundary=#{boundary}"} ]) %{ method: :post, route: Constants.thread_without_message(channel_id), body: {:multipart, Api.create_multipart(files, body, boundary)}, params: [], headers: headers } |> Api.request() |> Helpers.handle_request_with_decode({:struct, Channel}) end def create_in_forum(channel_id, options, reason) do Api.request(%{ method: :post, route: Constants.thread_without_message(channel_id), body: options, params: [], headers: Helpers.maybe_add_reason(reason) }) |> Helpers.handle_request_with_decode({:struct, Channel}) end @type thread_with_message_params :: %{ required(:name) => String.t(), optional(:auto_archive_duration) => 60 | 1440 | 4320 | 10_080, optional(:rate_limit_per_user) => 0..21_600 } @doc """ Create a thread on a channel message. The `thread_id` will be the same as the id of the message, as such no message can have more than one thread. If successful, returns `{:ok, Channel}`. Otherwise returns a `t:Nostrum.Api.error/0`. An optional `reason` argument can be given for the audit log. ## Options - `name`: Name of the thread, max 100 characters. - `auto_archive_duration`: Duration in minutes to auto-archive the thread after it has been inactive, can be set to 60, 1440, 4320, or 10080. - `rate_limit_per_user`: Rate limit per user in seconds, can be set to any value in `0..21600`. """ @doc since: "1.x.x" @spec create_with_message( Channel.id(), Message.id(), thread_with_message_params, AuditLogEntry.reason() ) :: {:ok, Channel.t()} | Api.error() def create_with_message(channel_id, message_id, options, reason \\ nil) do Api.request(%{ method: :post, route: Constants.thread_with_message(channel_id, message_id), body: options, params: [], headers: Helpers.maybe_add_reason(reason) }) |> Helpers.handle_request_with_decode({:struct, Channel}) end end