Current section
Files
Jump to
Current section
Files
lib/hunter.ex
defmodule Hunter do
@moduledoc """
An Elixir client for the Mastodon API
"""
@hunter_version Mix.Project.config()[:version]
alias Hunter.{Api.Request, Config}
@doc """
Retrieve account of authenticated user
## Parameters
* `conn` - connection credentials
## Examples
iex> conn = Hunter.new([base_url: "https://social.lou.lt", access_token: "123456"])
%Hunter.Client{base_url: "https://social.lou.lt", access_token: "123456"}
iex> Hunter.verify_credentials(conn)
%Hunter.Account{acct: "milmazz",
avatar: "https://social.lou.lt/avatars/original/missing.png",
avatar_static: "https://social.lou.lt/avatars/original/missing.png",
created_at: "2017-04-06T17:43:55.325Z",
display_name: "Milton Mazzarri", followers_count: 4,
following_count: 4,
header: "https://social.lou.lt/headers/original/missing.png",
header_static: "https://social.lou.lt/headers/original/missing.png",
id: "8039", locked: false, note: "", statuses_count: 3,
url: "https://social.lou.lt/@milmazz", username: "milmazz"}
"""
@spec verify_credentials(Hunter.Client.t()) :: Hunter.Account.t()
def verify_credentials(conn) do
Request.request!(conn, :get, "/api/v1/accounts/verify_credentials", :account)
end
@doc """
Make changes to the authenticated user
## Parameters
* `conn` - connection credentials
* `data` - data payload
## Possible keys for payload
* `display_name` - name to display in the user's profile
* `note` - new biography for the user
* `avatar` - base64 encoded image to display as the user's avatar (e.g. `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...`)
* `header` - base64 encoded image to display as the user's header image (e.g. `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUoAAADrCAYAAAA...`)
"""
@spec update_credentials(Hunter.Client.t(), map) :: Hunter.Account.t()
def update_credentials(conn, data) do
Request.request!(conn, :patch, "/api/v1/accounts/update_credentials", :account, data)
end
@doc """
Retrieve account
## Parameters
* `conn` - connection credentials
* `id` - account identifier
"""
@spec account(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Account.t()
def account(conn, id) do
Request.request!(conn, :get, "/api/v1/accounts/#{id}", :account)
end
@doc """
Look up an account by its webfinger address, without requiring a search
## Parameters
* `conn` - connection credentials
* `acct` - the username or webfinger address (e.g. `user@domain`) to look up
"""
@spec lookup_account(Hunter.Client.t(), String.t()) :: Hunter.Account.t()
def lookup_account(conn, acct) do
Request.request!(conn, :get, "/api/v1/accounts/lookup", :account, %{acct: acct})
end
@doc """
Retrieve multiple accounts by id
## Parameters
* `conn` - connection credentials
* `ids` - list of account identifiers
"""
@spec accounts_by_ids(Hunter.Client.t(), [String.t() | non_neg_integer]) :: [Hunter.Account.t()]
def accounts_by_ids(conn, ids) do
Request.request!(conn, :get, "/api/v1/accounts", :accounts, %{id: ids})
end
@doc """
Get a list of followers
## Parameters
* `conn` - connection credentials
* `id` - account identifier
* `options` - options list
## Options
* `max_id` - get a list of followers with id less than or equal this value
* `since_id` - get a list of followers with id greater than this value
* `limit` - maximum number of followers to get, default: 40, maximum: 80
**Note:** `max_id` and `since_id` for next and previous pages are provided in
the `Link` header. It is **not** possible to use the `id` of the returned
objects to construct your own URLs, because the results are sorted by an
internal key.
"""
@spec followers(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) :: [
Hunter.Account.t()
]
def followers(conn, id, options \\ []) do
Request.request!(conn, :get, "/api/v1/accounts/#{id}/followers", :accounts, options)
end
@doc """
Get a list of followed accounts
## Parameters
* `conn` - connection credentials
* `id` - account identifier
* `options` - options list
## Options
* `max_id` - get a list of followings with id less than or equal this value
* `since_id` - get a list of followings with id greater than this value
* `limit` - maximum number of followings to get, default: 40, maximum: 80
**Note:** `max_id` and `since_id` for next and previous pages are provided in
the `Link` header. It is **not** possible to use the `id` of the returned
objects to construct your own URLs, because the results are sorted by an
internal key.
"""
@spec following(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) :: [
Hunter.Account.t()
]
def following(conn, id, options \\ []) do
Request.request!(conn, :get, "/api/v1/accounts/#{id}/following", :accounts, options)
end
@doc """
Find out which of the accounts you follow also follow the given accounts
## Parameters
* `conn` - connection credentials
* `ids` - list of account identifiers
"""
@spec familiar_followers(Hunter.Client.t(), [String.t() | non_neg_integer]) :: [
Hunter.FamiliarFollowers.t()
]
def familiar_followers(conn, ids) do
Request.request!(conn, :get, "/api/v1/accounts/familiar_followers", :familiar_followers, %{
id: ids
})
end
@doc """
Retrieve the hashtags an account is featuring on their profile
## Parameters
* `conn` - connection credentials
* `id` - account identifier
"""
@spec account_featured_tags(Hunter.Client.t(), String.t() | non_neg_integer) :: [
Hunter.FeaturedTag.t()
]
def account_featured_tags(conn, id) do
Request.request!(conn, :get, "/api/v1/accounts/#{id}/featured_tags", :featured_tags)
end
@doc """
Search for accounts
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `q`: what to search for
* `limit`: maximum number of matching accounts to return, default: 40
"""
@spec search_account(Hunter.Client.t(), Keyword.t()) :: [Hunter.Account.t()]
def search_account(conn, options) do
opts = %{
q: Keyword.fetch!(options, :q),
limit: Keyword.get(options, :limit, 40)
}
Request.request!(conn, :get, "/api/v1/accounts/search", :accounts, opts)
end
@doc """
Retrieve user's blocks
## Parameters
* `conn` - connection credentials
## Options
* `max_id` - get a list of blocks with id less than or equal this value
* `since_id` - get a list of blocks with id greater than this value
* `limit` - maximum number of blocks to get, default: 40, max: 80
"""
@spec blocks(Hunter.Client.t(), Keyword.t()) :: [Hunter.Account.t()]
def blocks(conn, options \\ []) do
Request.request!(conn, :get, "/api/v1/blocks", :accounts, options)
end
@doc """
Retrieve a list of follow requests
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get a list of follow requests with id less than or equal this value
* `since_id` - get a list of follow requests with id greater than this value
* `limit` - maximum number of requests to get, default: 40, max: 80
"""
@spec follow_requests(Hunter.Client.t(), Keyword.t()) :: [Hunter.Account.t()]
def follow_requests(conn, options \\ []) do
Request.request!(conn, :get, "/api/v1/follow_requests", :accounts, options)
end
@doc """
Retrieve user's mutes
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get a list of mutes with id less than or equal this value
* `since_id` - get a list of mutes with id greater than this value
* `limit` - maximum number of mutes to get, default: 40, max: 80
"""
@spec mutes(Hunter.Client.t(), Keyword.t()) :: [Hunter.Account.t()]
def mutes(conn, options \\ []) do
Request.request!(conn, :get, "/api/v1/mutes", :accounts, options)
end
@doc """
Accepts a follow request
## Parameters
* `conn` - connection credentials
* `id` - follow request id
"""
@spec accept_follow_request(Hunter.Client.t(), String.t() | non_neg_integer) ::
Hunter.Relationship.t()
def accept_follow_request(conn, id) do
Request.request!(conn, :post, "/api/v1/follow_requests/#{id}/authorize", :relationship)
end
@doc """
Rejects a follow request
## Parameters
* `conn` - connection credentials
* `id` - follow request id
"""
@spec reject_follow_request(Hunter.Client.t(), String.t() | non_neg_integer) ::
Hunter.Relationship.t()
def reject_follow_request(conn, id) do
Request.request!(conn, :post, "/api/v1/follow_requests/#{id}/reject", :relationship)
end
## Application
@doc """
Register a new OAuth client app on the target instance
## Parameters
* `name` - name of your application
* `redirect_uris` - where the user should be redirected after
authorization; a single URI or a list of URIs (Mastodon 4.3+),
default: `urn:ietf:wg:oauth:2.0:oob` (no redirect)
* `scopes` - scope list, see the scope section for more details, default: `read`
* `website` - URL to the homepage of your app, default: `nil`
* `options` - option list
## Scopes
* `read` - read data
* `write` - post statuses and upload media for statuses
* `follow` - follow, unfollow, block, unblock
Multiple scopes can be requested during the authorization phase with the `scope` query param
## Options
* `save?` - persists your application information to a file, so, you can use
them later. default: `false`
* `api_base_url` - specifies if you want to register an application on a
different instance. default: `https://mastodon.social`
## Examples
iex> Hunter.create_app("hunter", "urn:ietf:wg:oauth:2.0:oob", ["read", "write", "follow"], nil, [save?: true, api_base_url: "https://example.com"])
%Hunter.Application{client_id: "1234567890",
client_secret: "1234567890",
id: "1234"}
"""
@spec create_app(
String.t(),
String.t() | [String.t()],
[String.t()],
nil | String.t(),
Keyword.t()
) :: Hunter.Application.t() | no_return
def create_app(
client_name,
redirect_uris \\ "urn:ietf:wg:oauth:2.0:oob",
scopes \\ ["read"],
website \\ nil,
options \\ []
) do
{save?, options} = Keyword.pop(options, :save?, false)
base_url = Keyword.get(options, :api_base_url, Config.api_base_url())
payload = %{
client_name: client_name,
redirect_uris: redirect_uris,
scopes: Enum.join(scopes, " "),
website: website
}
%Hunter.Application{} =
app = Request.request!(base_url, :post, "/api/v1/apps", :application, payload)
# Mastodon 4.3+ returns scopes/redirect_uris itself; only backfill the
# requested values for older servers that omit them
requested_uris = List.wrap(redirect_uris)
app = %Hunter.Application{
app
| scopes: app.scopes || scopes,
redirect_uris: app.redirect_uris || requested_uris,
redirect_uri: app.redirect_uri || List.first(requested_uris)
}
if save?, do: save_credentials(client_name, app)
app
end
@doc """
Confirm that the app-level token works
## Parameters
* `conn` - connection credentials holding an *app-level* access token,
see `log_in_app/2`
Returns the `Hunter.Application` as the server sees it (never includes
`client_secret`; includes `scopes` and `redirect_uris` since Mastodon 4.3).
"""
@spec verify_app_credentials(Hunter.Client.t()) :: Hunter.Application.t()
def verify_app_credentials(conn) do
Request.request!(conn, :get, "/api/v1/apps/verify_credentials", :application)
end
@doc """
Load persisted application's credentials
## Parameters
* `name` - application's name
"""
@spec load_credentials(String.t()) :: Hunter.Application.t()
def load_credentials(name) do
Config.home()
|> Path.join("apps/#{name}.json")
|> File.read!()
|> Poison.decode!(as: %Hunter.Application{})
end
defp save_credentials(name, app) do
home = Path.join(Config.home(), "apps")
unless File.exists?(home), do: File.mkdir_p!(home)
File.write!("#{home}/#{name}.json", Poison.encode!(app))
end
@doc """
Initializes a client
## Options
* `base_url` - URL of the instance you want to connect to
* `access_token` - [String] OAuth access token for your authenticated user
"""
@spec new(Keyword.t()) :: Hunter.Client.t()
def new(options \\ []), do: struct(Hunter.Client, options)
@doc """
User agent of the client
"""
@spec user_agent() :: String.t()
def user_agent, do: "Hunter.Elixir/#{version()}"
@doc """
Upload a media file
## Parameters
* `conn` - connection credentials
* `file` - media to be uploaded
* `options` - option list
## Options
* `description` - plain-text description of the media for accessibility (max 420 chars)
* `focus` - two floating points, comma-delimited
**Note:** the v2 media endpoint processes large files asynchronously: the
returned attachment's `url` may be `nil` until the server finishes
processing (HTTP 202). The `id` can be attached to a status with
`create_status` as soon as processing completes.
"""
@spec upload_media(Hunter.Client.t(), Path.t(), Keyword.t()) :: Hunter.Attachment.t()
def upload_media(conn, file, options \\ []) do
# stream raw byte chunks (Elixir 1.16+ argument order): the default line
# mode rewrites \r\n and transmits fewer bytes than the declared
# content-length
parts =
[file: {File.stream!(file, 65_536), filename: Path.basename(file)}] ++
Enum.map(options, fn {key, value} -> {key, to_string(value)} end)
Request.request!(conn, :post, "/api/v2/media", :attachment, {:form_multipart, parts})
end
@doc """
Retrieve a media attachment, to check the processing status of an
asynchronous upload
## Parameters
* `conn` - connection credentials
* `id` - attachment identifier
"""
@spec media_attachment(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Attachment.t()
def media_attachment(conn, id) do
Request.request!(conn, :get, "/api/v1/media/#{id}", :attachment)
end
@doc """
Update a media attachment, before it is attached to a status
## Parameters
* `conn` - connection credentials
* `id` - attachment identifier
* `options` - option list
## Options
* `description` - plain-text description of the media for accessibility
* `focus` - two floating points between -1.0 and 1.0, comma-delimited
* `thumbnail` - path of a custom thumbnail image
"""
@spec update_media(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) ::
Hunter.Attachment.t()
def update_media(conn, id, options \\ []) do
Request.request!(conn, :put, "/api/v1/media/#{id}", :attachment, Map.new(options))
end
@doc """
Delete a media attachment that is not currently attached to a status
## Parameters
* `conn` - connection credentials
* `id` - attachment identifier
"""
@spec delete_media(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def delete_media(conn, id) do
Request.request!(conn, :delete, "/api/v1/media/#{id}", :empty)
end
@doc """
Get the relationships of authenticated user towards given other users
## Parameters
* `conn` - connection credentials
* `id` - list of relationship IDs
"""
@spec relationships(Hunter.Client.t(), [String.t() | non_neg_integer]) :: [
Hunter.Relationship.t()
]
def relationships(conn, ids) do
Request.request!(conn, :get, "/api/v1/accounts/relationships", :relationships, %{id: ids})
end
@doc """
Follow a user
## Parameters
* `conn` - connection credentials
* `id` - user identifier
"""
@spec follow(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Relationship.t()
def follow(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/follow", :relationship)
end
@doc """
Unfollow a user
## Parameters
* `conn` - connection credentials
* `id` - user identifier
"""
@spec unfollow(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Relationship.t()
def unfollow(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/unfollow", :relationship)
end
@doc """
Set a private note on an account
## Parameters
* `conn` - connection credentials
* `id` - account identifier
* `comment` - the note text; pass an empty string to clear the note
"""
@spec set_account_note(Hunter.Client.t(), String.t() | non_neg_integer, String.t()) ::
Hunter.Relationship.t()
def set_account_note(conn, id, comment) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/note", :relationship, %{
comment: comment
})
end
@doc """
Remove an account from your followers
## Parameters
* `conn` - connection credentials
* `id` - account identifier
"""
@spec remove_from_followers(Hunter.Client.t(), String.t() | non_neg_integer) ::
Hunter.Relationship.t()
def remove_from_followers(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/remove_from_followers", :relationship)
end
@doc """
Feature an account on your profile
## Parameters
* `conn` - connection credentials
* `id` - account identifier
"""
@spec endorse(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Relationship.t()
def endorse(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/endorse", :relationship)
end
@doc """
Stop featuring an account on your profile
## Parameters
* `conn` - connection credentials
* `id` - account identifier
"""
@spec unendorse(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Relationship.t()
def unendorse(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/unendorse", :relationship)
end
@doc """
Retrieve the accounts you are featuring on your profile
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get a list of endorsements with id less than or equal this value
* `since_id` - get a list of endorsements with id greater than this value
* `limit` - maximum number of endorsements to get
"""
@spec endorsements(Hunter.Client.t(), Keyword.t()) :: [Hunter.Account.t()]
def endorsements(conn, options \\ []) do
Request.request!(conn, :get, "/api/v1/endorsements", :accounts, options)
end
@doc """
Retrieve the accounts a given account is featuring on their profile
## Parameters
* `conn` - connection credentials
* `id` - account identifier
* `options` - option list
## Options
* `max_id` - get a list of endorsements with id less than or equal this value
* `since_id` - get a list of endorsements with id greater than this value
* `limit` - maximum number of endorsements to get
"""
@spec account_endorsements(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) :: [
Hunter.Account.t()
]
def account_endorsements(conn, id, options \\ []) do
Request.request!(conn, :get, "/api/v1/accounts/#{id}/endorsements", :accounts, options)
end
@doc """
Block a user
## Parameters
* `conn` - connection credentials
* `id` - user identifier
"""
@spec block(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Relationship.t()
def block(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/block", :relationship)
end
@doc """
Unblock a user
* `conn` - connection credentials
* `id` - user identifier
"""
@spec unblock(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Relationship.t()
def unblock(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/unblock", :relationship)
end
@doc """
Mute a user
## Parameters
* `conn` - connection credentials
* `id` - user identifier
"""
@spec mute(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Relationship.t()
def mute(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/mute", :relationship)
end
@doc """
Unmute a user
## Parameters
* `conn` - connection credentials
* `id` - user identifier
"""
@spec unmute(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Relationship.t()
def unmute(conn, id) do
Request.request!(conn, :post, "/api/v1/accounts/#{id}/unmute", :relationship)
end
@doc """
Search for content
## Parameters
* `conn` - connection credentials
* `q` - the search query, if `q` is a URL Mastodon will attempt to fetch
the provided account or status, otherwise it will do a local account
and hashtag search
* `options` - option list
## Options
* `resolve` - whether to resolve non-local accounts
"""
@spec search(Hunter.Client.t(), String.t(), Keyword.t()) :: Hunter.Result.t()
def search(conn, query, options \\ []) do
options = options |> Keyword.merge(q: query) |> Map.new()
Request.request!(conn, :get, "/api/v2/search", :result, options)
end
@doc """
Create new status
## Parameters
* `conn` - connection credentials
* `status` - text of the status
* `options` - option list
## Options
* `in_reply_to_id` - local ID of the status you want to reply to
* `media_ids` - list of media IDs to attach to the status (maximum: 4)
* `sensitive` - whether the media of the status is NSFW
* `spoiler_text` - text to be shown as a warning before the actual content
* `visibility` - either `direct`, `private`, `unlisted` or `public`
* `language` - ISO 639-1 language code for the status
* `poll` - map with `options` (list of choices) and `expires_in`
(seconds); optional: `multiple`, `hide_totals`
* `quoted_status_id` - ID of a status to quote
* `scheduled_at` - ISO 8601 datetime (at least 5 minutes ahead) at which
the status should be published; the call returns a
`Hunter.ScheduledStatus` instead of a `Hunter.Status`
* `idempotency_key` - unique string sent as the `Idempotency-Key` header
to prevent duplicate submissions
"""
@spec create_status(Hunter.Client.t(), String.t(), Keyword.t()) ::
Hunter.Status.t() | Hunter.ScheduledStatus.t() | no_return
def create_status(conn, status, options \\ []) do
{idempotency_key, options} = Keyword.pop(options, :idempotency_key)
body = options |> Keyword.put(:status, status) |> Map.new()
headers =
case idempotency_key do
nil -> []
key -> [{"idempotency-key", key}]
end
# scheduling a status returns a ScheduledStatus instead of a Status
to = if Keyword.has_key?(options, :scheduled_at), do: :scheduled_status, else: :status
Request.request!(conn, :post, "/api/v1/statuses", to, body, headers: headers)
end
@doc """
Retrieve a poll
## Parameters
* `conn` - connection credentials
* `id` - poll identifier
"""
@spec poll(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Poll.t()
def poll(conn, id) do
Request.request!(conn, :get, "/api/v1/polls/#{id}", :poll)
end
@doc """
Vote on one or more options in a poll
## Parameters
* `conn` - connection credentials
* `id` - poll identifier
* `choices` - list of option indices to vote for (zero-based); multiple
choices are only allowed on multiple-choice polls
"""
@spec vote(Hunter.Client.t(), String.t() | non_neg_integer, [non_neg_integer]) ::
Hunter.Poll.t()
def vote(conn, id, choices) do
Request.request!(conn, :post, "/api/v1/polls/#{id}/votes", :poll, %{choices: choices})
end
@doc """
Retrieve status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec status(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def status(conn, id) do
Request.request!(conn, :get, "/api/v1/statuses/#{id}", :status)
end
@doc """
Retrieve multiple statuses by id
## Parameters
* `conn` - connection credentials
* `ids` - list of status identifiers
"""
@spec statuses_by_ids(Hunter.Client.t(), [String.t() | non_neg_integer]) :: [Hunter.Status.t()]
def statuses_by_ids(conn, ids) do
Request.request!(conn, :get, "/api/v1/statuses", :statuses, %{id: ids})
end
@doc """
Edit a status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
* `status` - the new text of the status
* `options` - option list
## Options
* `spoiler_text` - text to be shown as a warning before the actual content
* `sensitive` - whether the media of the status is NSFW
* `language` - ISO 639-1 language code for the status
* `media_ids` - list of media IDs to attach to the status
* `poll` - map with `options` (list of choices) and `expires_in`
(seconds); replaces the current poll
"""
@spec edit_status(Hunter.Client.t(), String.t() | non_neg_integer, String.t(), Keyword.t()) ::
Hunter.Status.t() | no_return
def edit_status(conn, id, status, options \\ []) do
body = options |> Keyword.put(:status, status) |> Map.new()
Request.request!(conn, :put, "/api/v1/statuses/#{id}", :status, body)
end
@doc """
Retrieve the edit history of a status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec status_history(Hunter.Client.t(), String.t() | non_neg_integer) :: [Hunter.StatusEdit.t()]
def status_history(conn, id) do
Request.request!(conn, :get, "/api/v1/statuses/#{id}/history", :status_edits)
end
@doc """
Retrieve the plain-text source of a status, for editing
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec status_source(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.StatusSource.t()
def status_source(conn, id) do
Request.request!(conn, :get, "/api/v1/statuses/#{id}/source", :status_source)
end
@doc """
Bookmark a status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec bookmark(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def bookmark(conn, id), do: status_action(conn, id, :bookmark)
@doc """
Remove a status from bookmarks
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec unbookmark(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def unbookmark(conn, id), do: status_action(conn, id, :unbookmark)
@doc """
Fetch the user's bookmarked statuses
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get a list of bookmarks with id less than or equal this value
* `since_id` - get a list of bookmarks with id greater than this value
* `min_id` - get a list of bookmarks with id greater than this value,
immediately newer
* `limit` - maximum number of bookmarks to get, default: 20, max: 40
"""
@spec bookmarks(Hunter.Client.t(), Keyword.t()) :: [Hunter.Status.t()]
def bookmarks(conn, options \\ []) do
Request.request!(conn, :get, "/api/v1/bookmarks", :statuses, options)
end
@doc """
Pin a status to the top of the user's profile
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec pin(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def pin(conn, id), do: status_action(conn, id, :pin)
@doc """
Unpin a status from the user's profile
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec unpin(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def unpin(conn, id), do: status_action(conn, id, :unpin)
@doc """
Mute a conversation, so its thread stops generating notifications
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec mute_conversation(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def mute_conversation(conn, id), do: status_action(conn, id, :mute)
@doc """
Unmute a conversation
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec unmute_conversation(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def unmute_conversation(conn, id), do: status_action(conn, id, :unmute)
defp status_action(conn, id, action) do
Request.request!(conn, :post, "/api/v1/statuses/#{id}/#{action}", :status)
end
@doc """
Translate a status into some language
## Parameters
* `conn` - connection credentials
* `id` - status identifier
* `options` - option list
## Options
* `lang` - ISO 639-1 language code the status should be translated into;
defaults to the user's current locale
"""
@spec translate_status(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) ::
Hunter.Translation.t()
def translate_status(conn, id, options \\ []) do
Request.request!(
conn,
:post,
"/api/v1/statuses/#{id}/translate",
:translation,
Map.new(options)
)
end
@doc """
Destroy status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec destroy_status(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def destroy_status(conn, id) do
Request.request!(conn, :delete, "/api/v1/statuses/#{id}", :empty)
end
@doc """
Reblog a status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec reblog(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def reblog(conn, id) do
Request.request!(conn, :post, "/api/v1/statuses/#{id}/reblog", :status)
end
@doc """
Undo a reblog of a status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec unreblog(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def unreblog(conn, id) do
Request.request!(conn, :post, "/api/v1/statuses/#{id}/unreblog", :status)
end
@doc """
Fetch the list of users who reblogged the status.
## Parameters
* `conn` - connection credentials
* `id` - status identifier
* `options` - option list
## Options
* `max_id` - get a list of *reblogged by* ids less than or equal this value
* `since_id` - get a list of *reblogged by* ids greater than this value
* `limit` - maximum number of *reblogged by* to get, default: 40, max: 80
"""
@spec reblogged_by(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) :: [
Hunter.Account.t()
]
def reblogged_by(conn, id, options \\ []) do
Request.request!(conn, :get, "/api/v1/statuses/#{id}/reblogged_by", :accounts, options)
end
@doc """
Favorite a status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec favourite(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def favourite(conn, id) do
Request.request!(conn, :post, "/api/v1/statuses/#{id}/favourite", :status)
end
@doc """
Undo a favorite of a status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec unfavourite(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Status.t()
def unfavourite(conn, id) do
Request.request!(conn, :post, "/api/v1/statuses/#{id}/unfavourite", :status)
end
@doc """
Fetch a user's favourites
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get a list of favourites with id less than or equal this value
* `since_id` - get a list of favourites with id greater than this value
* `limit` - maximum of favourites to get, default: 20, max: 40
"""
@spec favourites(Hunter.Client.t(), Keyword.t()) :: [Hunter.Status.t()]
def favourites(conn, options \\ []) do
Request.request!(conn, :get, "/api/v1/favourites", :statuses, options)
end
@doc """
Fetch the list of users who favourited the status
## Parameters
* `conn` - connection credentials
* `id` - status identifier
* `options` - option list
## Options
* `max_id` - get a list of *favourited by* ids less than or equal this value
* `since_id` - get a list of *favourited by* ids greater than this value
* `limit` - maximum number of *favourited by* to get, default: 40, max: 80
"""
@spec favourited_by(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) :: [
Hunter.Account.t()
]
def favourited_by(conn, id, options \\ []) do
Request.request!(conn, :get, "/api/v1/statuses/#{id}/favourited_by", :accounts, options)
end
@doc """
Get a list of statuses by a user
## Parameters
* `conn` - connection credentials -
* `account_id` - account identifier
* `options` - option list
## Options
* `only_media` - only return `Hunter.Status.t` that have media attachments
* `exclude_replies` - skip statuses that reply to other statuses
* `max_id` - get a list of statuses with id less than or equal this value
* `since_id` - get a list of statuses with id greater than this value
* `limit` - maximum number of statuses to get, default: 20, max: 40
"""
@spec statuses(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) :: [
Hunter.Status.t()
]
def statuses(conn, account_id, options \\ []) do
Request.request!(conn, :get, "/api/v1/accounts/#{account_id}/statuses", :statuses, options)
end
@doc """
Retrieve statuses from the home timeline
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get a list of timelines with id less than or equal this value
* `since_id` - get a list of timelines with id greater than this value
* `limit` - maximum number of statuses on the requested timeline to get, default: 20, max: 40
"""
@spec home_timeline(Hunter.Client.t(), Keyword.t()) :: [Hunter.Status.t()]
def home_timeline(conn, options \\ []) do
retrieve_timeline(conn, "/api/v1/timelines/home", options)
end
@doc """
Retrieve statuses from the public timeline
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `local` - only return statuses originating from this instance
* `max_id` - get a list of timelines with id less than or equal this value
* `since_id` - get a list of timelines with id greater than this value
* `limit` - maximum number of statuses on the requested timeline to get, default: 20, max: 40
"""
@spec public_timeline(Hunter.Client.t(), Keyword.t()) :: [Hunter.Status.t()]
def public_timeline(conn, options \\ []) do
retrieve_timeline(conn, "/api/v1/timelines/public", options)
end
@doc """
Retrieve statuses from a hashtag
## Parameters
* `conn` - connection credentials
* `hashtag` - string list
* `options` - option list
## Options
* `local` - only return statuses originating from this instance
* `max_id` - get a list of timelines with id less than or equal this value
* `since_id` - get a list of timelines with id greater than this value
* `limit` - maximum number of statuses on the requested timeline to get, default: 20, max: 40
"""
@spec hashtag_timeline(Hunter.Client.t(), [String.t()], Keyword.t()) :: [Hunter.Status.t()]
def hashtag_timeline(conn, hashtag, options \\ []) do
retrieve_timeline(conn, "/api/v1/timelines/tag/#{hashtag}", options)
end
@doc """
Retrieve statuses from the given list's timeline
## Parameters
* `conn` - connection credentials
* `list_id` - list identifier
* `options` - option list
## Options
* `max_id` - get a list of timelines with id less than or equal this value
* `since_id` - get a list of timelines with id greater than this value
* `limit` - maximum number of statuses on the requested timeline to get, default: 20, max: 40
"""
@spec list_timeline(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) :: [
Hunter.Status.t()
]
def list_timeline(conn, list_id, options \\ []) do
retrieve_timeline(conn, "/api/v1/timelines/list/#{list_id}", options)
end
defp retrieve_timeline(conn, endpoint, options) do
Request.request!(conn, :get, endpoint, :statuses, options)
end
@doc """
Retrieve all lists the user owns
## Parameters
* `conn` - connection credentials
"""
@spec lists(Hunter.Client.t()) :: [Hunter.List.t()]
def lists(conn) do
Request.request!(conn, :get, "/api/v1/lists", :lists)
end
@doc """
Retrieve a list
## Parameters
* `conn` - connection credentials
* `id` - list identifier
"""
@spec list(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.List.t()
def list(conn, id) do
Request.request!(conn, :get, "/api/v1/lists/#{id}", :list)
end
@doc """
Create a new list
## Parameters
* `conn` - connection credentials
* `title` - the title of the list
* `options` - option list
## Options
* `replies_policy` - which replies should be shown in the list, one of:
`followed`, `list`, `none`; default: `list`
* `exclusive` - whether members of the list are removed from the home
timeline
"""
@spec create_list(Hunter.Client.t(), String.t(), Keyword.t()) :: Hunter.List.t()
def create_list(conn, title, options \\ []) do
body = options |> Keyword.put(:title, title) |> Map.new()
Request.request!(conn, :post, "/api/v1/lists", :list, body)
end
@doc """
Update a list
## Parameters
* `conn` - connection credentials
* `id` - list identifier
* `options` - option list
## Options
* `title` - the new title of the list
* `replies_policy` - which replies should be shown in the list, one of:
`followed`, `list`, `none`
* `exclusive` - whether members of the list are removed from the home
timeline
"""
@spec update_list(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) ::
Hunter.List.t()
def update_list(conn, id, options) do
Request.request!(conn, :put, "/api/v1/lists/#{id}", :list, Map.new(options))
end
@doc """
Delete a list
## Parameters
* `conn` - connection credentials
* `id` - list identifier
"""
@spec destroy_list(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def destroy_list(conn, id) do
Request.request!(conn, :delete, "/api/v1/lists/#{id}", :empty)
end
@doc """
Retrieve the accounts in a list
## Parameters
* `conn` - connection credentials
* `id` - list identifier
* `options` - option list
## Options
* `max_id` - get a list of accounts with id less than or equal this value
* `since_id` - get a list of accounts with id greater than this value
* `limit` - maximum number of accounts to get, default: 40; set to 0 to
get all accounts in the list
"""
@spec list_accounts(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) :: [
Hunter.Account.t()
]
def list_accounts(conn, id, options \\ []) do
Request.request!(conn, :get, "/api/v1/lists/#{id}/accounts", :accounts, options)
end
@doc """
Add accounts to a list; the user must be following each of them
## Parameters
* `conn` - connection credentials
* `id` - list identifier
* `account_ids` - account identifiers to add
"""
@spec add_accounts_to_list(Hunter.Client.t(), String.t() | non_neg_integer, [
String.t() | non_neg_integer
]) :: boolean
def add_accounts_to_list(conn, id, account_ids) do
Request.request!(conn, :post, "/api/v1/lists/#{id}/accounts", :empty, %{
account_ids: account_ids
})
end
@doc """
Remove accounts from a list
## Parameters
* `conn` - connection credentials
* `id` - list identifier
* `account_ids` - account identifiers to remove
"""
@spec remove_accounts_from_list(Hunter.Client.t(), String.t() | non_neg_integer, [
String.t() | non_neg_integer
]) ::
boolean
def remove_accounts_from_list(conn, id, account_ids) do
Request.request!(conn, :delete, "/api/v1/lists/#{id}/accounts", :empty, %{
account_ids: account_ids
})
end
@doc """
Retrieve the user's lists that contain a given account
## Parameters
* `conn` - connection credentials
* `account_id` - account identifier
"""
@spec account_lists(Hunter.Client.t(), String.t() | non_neg_integer) :: [Hunter.List.t()]
def account_lists(conn, account_id) do
Request.request!(conn, :get, "/api/v1/accounts/#{account_id}/lists", :lists)
end
@doc """
Retrieve instance information
## Parameters
* `conn` - connection credentials
## Examples
iex> conn = Hunter.new([base_url: "https://social.lou.lt", access_token: "123456"])
%Hunter.Client{base_url: "https://social.lou.lt", access_token: "123456"}
iex> Hunter.instance_info(conn)
%Hunter.Instance{description: "Mostly French instance - <a href=\\"/about/more#rules\\">Read full description</a> for rules.",
domain: "social.lou.lt", title: "Loultstodon", version: "4.3.8"}
"""
@spec instance_info(Hunter.Client.t()) :: Hunter.Instance.t()
def instance_info(conn) do
Request.request!(conn, :get, "/api/v2/instance", :instance)
end
@doc """
Retrieve user's notifications
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get a list of notifications with id less than or equal this value
* `since_id` - get a list of notifications with id greater than this value
* `limit` - maximum number of notifications to get, default: 15, max: 30
## Examples
Hunter.notifications(conn)
#=> [%Hunter.Notification{account: %{"acct" => "paperswelove@mstdn.io", ...}]
"""
@spec notifications(Hunter.Client.t(), Keyword.t()) :: [Hunter.Notification.t()]
def notifications(conn, options \\ []) do
Request.request!(conn, :get, "/api/v1/notifications", :notifications, options)
end
@doc """
Retrieve single notification
## Parameters
* `conn` - connection credentials
* `id` - notification identifier
## Examples
Hunter.notification(conn, 17_476)
#=> %Hunter.Notification{account: %{"acct" => "paperswelove@mstdn.io", ...}
"""
@spec notification(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Notification.t()
def notification(conn, id) do
Request.request!(conn, :get, "/api/v1/notifications/#{id}", :notification)
end
@doc """
Deletes all notifications from the Mastodon server for the authenticated user
## Parameters
* `conn` - connection credentials
"""
@spec clear_notifications(Hunter.Client.t()) :: boolean
def clear_notifications(conn) do
Request.request!(conn, :post, "/api/v1/notifications/clear", :empty)
end
@doc """
Dismiss a single notification
## Parameters
* `conn` - connection credentials
* `id` - notification id
"""
@spec clear_notification(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def clear_notification(conn, id) do
Request.request!(conn, :post, "/api/v1/notifications/#{id}/dismiss", :empty)
end
@doc """
Retrieve the number of unread notifications, capped by the server
## Parameters
* `conn` - connection credentials
"""
@spec unread_count(Hunter.Client.t()) :: non_neg_integer
def unread_count(conn) do
Request.request!(conn, :get, "/api/v1/notifications/unread_count", nil)
|> Map.fetch!("count")
end
@doc """
Retrieve the notification filtering policy for the user
## Parameters
* `conn` - connection credentials
"""
@spec notification_policy(Hunter.Client.t()) :: Hunter.NotificationPolicy.t()
def notification_policy(conn) do
Request.request!(conn, :get, "/api/v2/notifications/policy", :notification_policy)
end
@doc """
Update the notification filtering policy for the user
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
Each takes one of `accept`, `filter` or `drop`:
* `for_not_following`
* `for_not_followers`
* `for_new_accounts`
* `for_private_mentions`
* `for_limited_accounts`
"""
@spec update_notification_policy(Hunter.Client.t(), Keyword.t()) ::
Hunter.NotificationPolicy.t()
def update_notification_policy(conn, options) do
Request.request!(
conn,
:patch,
"/api/v2/notifications/policy",
:notification_policy,
Map.new(options)
)
end
@doc """
Retrieve notification requests (groups of filtered notifications)
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get requests with id less than or equal this value
* `since_id` - get requests with id greater than this value
* `limit` - maximum number of requests to get, default: 40, max: 80
"""
@spec notification_requests(Hunter.Client.t(), Keyword.t()) :: [
Hunter.NotificationRequest.t()
]
def notification_requests(conn, options \\ []) do
Request.request!(
conn,
:get,
"/api/v1/notifications/requests",
:notification_requests,
options
)
end
@doc """
Retrieve a single notification request
## Parameters
* `conn` - connection credentials
* `id` - notification request identifier
"""
@spec notification_request(Hunter.Client.t(), String.t() | non_neg_integer) ::
Hunter.NotificationRequest.t()
def notification_request(conn, id) do
Request.request!(conn, :get, "/api/v1/notifications/requests/#{id}", :notification_request)
end
@doc """
Accept a notification request, so future notifications from the account
are delivered normally
## Parameters
* `conn` - connection credentials
* `id` - notification request identifier
"""
@spec accept_notification_request(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def accept_notification_request(conn, id) do
Request.request!(conn, :post, "/api/v1/notifications/requests/#{id}/accept", :empty)
end
@doc """
Dismiss a notification request, removing it and its filtered notifications
## Parameters
* `conn` - connection credentials
* `id` - notification request identifier
"""
@spec dismiss_notification_request(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def dismiss_notification_request(conn, id) do
Request.request!(conn, :post, "/api/v1/notifications/requests/#{id}/dismiss", :empty)
end
@doc """
Accept multiple notification requests
## Parameters
* `conn` - connection credentials
* `ids` - notification request identifiers
"""
@spec accept_notification_requests(Hunter.Client.t(), [String.t() | non_neg_integer]) ::
boolean
def accept_notification_requests(conn, ids) do
Request.request!(conn, :post, "/api/v1/notifications/requests/accept", :empty, %{id: ids})
end
@doc """
Dismiss multiple notification requests, removing them and their filtered
notifications
## Parameters
* `conn` - connection credentials
* `ids` - notification request identifiers
"""
@spec dismiss_notification_requests(Hunter.Client.t(), [String.t() | non_neg_integer]) ::
boolean
def dismiss_notification_requests(conn, ids) do
Request.request!(conn, :post, "/api/v1/notifications/requests/dismiss", :empty, %{id: ids})
end
@doc """
Check whether accepted notification requests have been merged
## Parameters
* `conn` - connection credentials
"""
@spec notification_requests_merged?(Hunter.Client.t()) :: boolean
def notification_requests_merged?(conn) do
Request.request!(conn, :get, "/api/v1/notifications/requests/merged", nil)
|> Map.fetch!("merged")
end
@doc """
Retrieve grouped notifications, together with the accounts and statuses
they reference
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get groups with id less than or equal this value
* `since_id` - get groups with id greater than this value
* `limit` - maximum number of groups to get, default: 40, max: 80
* `types` - notification types to include
* `exclude_types` - notification types to exclude
* `grouped_types` - which types can be grouped together
"""
@spec grouped_notifications(Hunter.Client.t(), Keyword.t()) ::
Hunter.GroupedNotificationsResults.t()
def grouped_notifications(conn, options \\ []) do
Request.request!(conn, :get, "/api/v2/notifications", :grouped_notifications, options)
end
@doc """
Retrieve a single notification group by its group key
## Parameters
* `conn` - connection credentials
* `group_key` - notification group key
"""
@spec notification_group(Hunter.Client.t(), String.t()) ::
Hunter.GroupedNotificationsResults.t()
def notification_group(conn, group_key) do
Request.request!(conn, :get, "/api/v2/notifications/#{group_key}", :grouped_notifications)
end
@doc """
Dismiss a notification group
## Parameters
* `conn` - connection credentials
* `group_key` - notification group key
"""
@spec dismiss_notification_group(Hunter.Client.t(), String.t()) :: boolean
def dismiss_notification_group(conn, group_key) do
Request.request!(conn, :post, "/api/v2/notifications/#{group_key}/dismiss", :empty)
end
@doc """
Retrieve the accounts of all notifications in a notification group
## Parameters
* `conn` - connection credentials
* `group_key` - notification group key
"""
@spec notification_group_accounts(Hunter.Client.t(), String.t()) :: [Hunter.Account.t()]
def notification_group_accounts(conn, group_key) do
Request.request!(conn, :get, "/api/v2/notifications/#{group_key}/accounts", :accounts)
end
@doc """
Retrieve the number of unread notification groups, capped by the server
## Parameters
* `conn` - connection credentials
"""
@spec grouped_unread_count(Hunter.Client.t()) :: non_neg_integer
def grouped_unread_count(conn) do
Request.request!(conn, :get, "/api/v2/notifications/unread_count", nil)
|> Map.fetch!("count")
end
@doc """
Subscribe to Web Push notifications; each access token can have exactly
one subscription, and creating a new one replaces it
## Parameters
* `conn` - connection credentials
* `subscription` - map with `endpoint`, `keys` (`p256dh` and `auth`)
and optionally `standard`
* `data` - optional map with `alerts` and `policy`
"""
@spec create_push_subscription(Hunter.Client.t(), map, map) ::
Hunter.WebPushSubscription.t()
def create_push_subscription(conn, subscription, data \\ %{}) do
Request.request!(conn, :post, "/api/v1/push/subscription", :web_push_subscription, %{
subscription: subscription,
data: data
})
end
@doc """
Retrieve the Web Push subscription tied to the access token
## Parameters
* `conn` - connection credentials
"""
@spec push_subscription(Hunter.Client.t()) :: Hunter.WebPushSubscription.t()
def push_subscription(conn) do
Request.request!(conn, :get, "/api/v1/push/subscription", :web_push_subscription)
end
@doc """
Update the `data` portion of the Web Push subscription
## Parameters
* `conn` - connection credentials
* `data` - map with `alerts` and/or `policy`
"""
@spec update_push_subscription(Hunter.Client.t(), map) :: Hunter.WebPushSubscription.t()
def update_push_subscription(conn, data) do
Request.request!(conn, :put, "/api/v1/push/subscription", :web_push_subscription, %{
data: data
})
end
@doc """
Remove the Web Push subscription tied to the access token
## Parameters
* `conn` - connection credentials
"""
@spec delete_push_subscription(Hunter.Client.t()) :: boolean
def delete_push_subscription(conn) do
Request.request!(conn, :delete, "/api/v1/push/subscription", :empty)
end
@doc """
Report a user
## Parameters
* `conn` - connection credentials
* `account_id` - the ID of the account to report
* `status_ids` - the IDs of statuses to report
* `comment` - a comment to associate with the report
"""
@spec report(
Hunter.Client.t(),
String.t() | non_neg_integer,
[String.t() | non_neg_integer],
String.t()
) ::
Hunter.Report.t()
def report(conn, account_id, status_ids, comment) do
payload = %{
account_id: account_id,
status_ids: status_ids,
comment: comment
}
Request.request!(conn, :post, "/api/v1/reports", :report, payload)
end
@doc """
Retrieve status context
## Parameters
* `conn` - connection credentials
* `id` - status identifier
"""
@spec status_context(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Context.t()
def status_context(conn, id) do
Request.request!(conn, :get, "/api/v1/statuses/#{id}/context", :context)
end
@doc """
Register a new account and obtain its access token
The given client must carry an *app-level* access token (from the OAuth
client-credentials flow); returns a new `Hunter.Client` holding the created
user's access token.
## Parameters
* `conn` - connection credentials with the app-level access token
* `params` - registration params
## Possible keys for params
* `username` - desired username
* `email` - the account owner's email address
* `password` - the account password
* `agreement` - whether the user agrees to the server rules and terms (must be `true`)
* `locale` - the language of the confirmation email (e.g. `"en"`)
* `reason` - (optional) why you want to join, when registrations require approval
"""
@spec register_account(Hunter.Client.t(), map) :: Hunter.Client.t()
def register_account(conn, params) do
response = Request.request!(conn, :post, "/api/v1/accounts", nil, params)
%Hunter.Client{base_url: conn.base_url, access_token: response["access_token"]}
end
@doc """
Retrieve access token via the OAuth authorization-code flow
## Parameters
* `app` - application details, see: `Hunter.create_app/5` for more details.
* `oauth_code` - authorization code from the redirect (or the out-of-band
form)
* `base_url` - API base url, default: `https://mastodon.social`
* `opts` - optional params
## Options
* `code_verifier` - PKCE verifier matching the `code_challenge` sent to
`authorization_url/3`, see `generate_pkce/0`
* `redirect_uri` - must match the URI used on the authorization request;
defaults to the app's first registered URI
"""
@spec log_in_oauth(Hunter.Application.t(), String.t(), String.t(), Keyword.t()) ::
Hunter.Client.t()
def log_in_oauth(
%Hunter.Application{} = app,
oauth_code,
base_url \\ "https://mastodon.social",
opts \\ []
) do
opts = Keyword.validate!(opts, [:code_verifier, :redirect_uri])
base_url = base_url || Config.api_base_url()
payload = %{
client_id: app.client_id,
client_secret: app.client_secret,
grant_type: "authorization_code",
code: oauth_code,
redirect_uri: Keyword.get(opts, :redirect_uri) || first_redirect_uri(app)
}
payload =
case Keyword.fetch(opts, :code_verifier) do
{:ok, verifier} when is_binary(verifier) -> Map.put(payload, :code_verifier, verifier)
_ -> payload
end
response = Request.request!(base_url, :post, "/oauth/token", nil, payload)
%Hunter.Client{base_url: base_url, access_token: response["access_token"]}
end
@doc """
Retrieve an app-level access token via the client-credentials grant
The returned client can call app-scoped endpoints such as
`verify_app_credentials/1` and `register_account/2`.
## Parameters
* `app` - application details, see: `Hunter.create_app/5` for more details.
* `base_url` - API base url, default: `https://mastodon.social`
"""
@spec log_in_app(Hunter.Application.t(), String.t()) :: Hunter.Client.t()
def log_in_app(%Hunter.Application{} = app, base_url \\ "https://mastodon.social") do
base_url = base_url || Config.api_base_url()
payload = %{
client_id: app.client_id,
client_secret: app.client_secret,
grant_type: "client_credentials"
}
payload =
case default_scope(app) do
nil -> payload
scope -> Map.put(payload, :scope, scope)
end
response = Request.request!(base_url, :post, "/oauth/token", nil, payload)
%Hunter.Client{base_url: base_url, access_token: response["access_token"]}
end
@doc """
Fetch the instance's OAuth server metadata (RFC 8414)
Use this to discover supported scopes, grant types and endpoints instead
of hardcoding them. Available since Mastodon 4.3. Does not require
authentication.
## Parameters
* `base_url` - API base url, default: `https://mastodon.social`
Returns the decoded metadata map (`"issuer"`, `"authorization_endpoint"`,
`"token_endpoint"`, `"scopes_supported"`,
`"code_challenge_methods_supported"`, ...).
"""
@spec oauth_server_metadata(String.t()) :: map
def oauth_server_metadata(base_url \\ "https://mastodon.social") do
base_url = base_url || Config.api_base_url()
Request.request!(base_url, :get, "/.well-known/oauth-authorization-server", nil)
end
@doc """
Fetch OIDC-style claims about the authenticated user
Available since Mastodon 4.4; the token must carry the `profile` (or
`read`) scope.
## Parameters
* `conn` - connection credentials
Returns the decoded claims map (`"iss"`, `"sub"`, `"name"`,
`"preferred_username"`, ...).
"""
@spec userinfo(Hunter.Client.t()) :: map
def userinfo(conn) do
Request.request!(conn, :get, "/oauth/userinfo", nil)
end
@doc """
Revoke an access token
## Parameters
* `app` - application details (must be the client the token was issued
to), see: `Hunter.create_app/5`
* `token` - the access token to revoke
* `base_url` - API base url, default: `https://mastodon.social`
Returns `true` on success. Raises `Hunter.Error` if the token does not
belong to the given client.
"""
@spec revoke_token(Hunter.Application.t(), String.t(), String.t()) :: true
def revoke_token(%Hunter.Application{} = app, token, base_url \\ "https://mastodon.social") do
base_url = base_url || Config.api_base_url()
payload = %{
client_id: app.client_id,
client_secret: app.client_secret,
token: token
}
Request.request!(base_url, :post, "/oauth/revoke", :empty, payload)
end
@doc """
Generate a PKCE verifier/challenge pair (RFC 7636, S256)
Returns a map with `code_verifier`, `code_challenge` and
`code_challenge_method`. Pass the challenge params to
`authorization_url/3` and the verifier to `log_in_oauth/4`.
"""
@spec generate_pkce() :: %{
code_verifier: String.t(),
code_challenge: String.t(),
code_challenge_method: String.t()
}
def generate_pkce do
verifier = Base.url_encode64(:crypto.strong_rand_bytes(32), padding: false)
%{
code_verifier: verifier,
code_challenge: Base.url_encode64(:crypto.hash(:sha256, verifier), padding: false),
code_challenge_method: "S256"
}
end
@doc """
Build the URL to send the user to for authorization (`GET /oauth/authorize`)
## Parameters
* `app` - application details, see: `Hunter.create_app/5`
* `base_url` - API base url, default: `https://mastodon.social`
* `opts` - optional params
## Options
* `redirect_uri` - overrides the app's registered redirect URI
* `scope` - space-separated scopes, defaults to the app's scopes
* `code_challenge` / `code_challenge_method` - PKCE params, see
`generate_pkce/0`
* `state` - opaque value returned to your redirect URI unchanged
* `force_login` - forces re-login when `true`
* `lang` - ISO 639-1 language code for the authorization form
Builds a URL only; performs no request.
"""
@spec authorization_url(Hunter.Application.t(), String.t(), Keyword.t()) :: String.t()
def authorization_url(
%Hunter.Application{} = app,
base_url \\ "https://mastodon.social",
opts \\ []
) do
opts =
Keyword.validate!(opts, [
:redirect_uri,
:scope,
:code_challenge,
:code_challenge_method,
:state,
:force_login,
:lang
])
base_url = base_url || Config.api_base_url()
query =
[
response_type: "code",
client_id: app.client_id,
redirect_uri: first_redirect_uri(app),
scope: default_scope(app)
]
|> Keyword.merge(opts)
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> URI.encode_query()
base_url <> "/oauth/authorize?" <> query
end
defp first_redirect_uri(%Hunter.Application{redirect_uris: [uri | _]}), do: uri
defp first_redirect_uri(%Hunter.Application{redirect_uri: uri}) when is_binary(uri), do: uri
# Doorkeeper rejects requests without a redirect_uri matching the
# registration; fall back to create_app's default for stale credentials
defp first_redirect_uri(_app), do: "urn:ietf:wg:oauth:2.0:oob"
defp default_scope(%Hunter.Application{scopes: scopes}) when is_list(scopes) and scopes != [],
do: Enum.join(scopes, " ")
defp default_scope(_app), do: nil
@doc """
Fetch user's blocked domains
## Parameters
* `conn` - connection credentials
* `options` - option list
## Options
* `max_id` - get a list of blocks with id less than or equal this value
* `since_id` - get a list of blocks with id greater than this value
* `limit` - maximum number of blocks to get, default: 40, max: 80
"""
@spec blocked_domains(Hunter.Client.t(), Keyword.t()) :: [String.t()]
def blocked_domains(conn, options \\ []) do
Request.request!(conn, :get, "/api/v1/domain_blocks", nil, options)
end
@doc """
Block a domain
## Parameters
* `conn` - connection credentials
* `domain` - domain to block
"""
@spec block_domain(Hunter.Client.t(), String.t()) :: boolean()
def block_domain(conn, domain) do
Request.request!(conn, :post, "/api/v1/domain_blocks", :empty, %{domain: domain})
end
@doc """
Unblock a domain
## Parameters
* `conn` - connection credentials
* `domain` - domain to unblock
"""
@spec unblock_domain(Hunter.Client.t(), String.t()) :: boolean()
def unblock_domain(conn, domain) do
Request.request!(conn, :delete, "/api/v1/domain_blocks", :empty, %{domain: domain})
end
@doc """
Retrieve all filters the user owns
## Parameters
* `conn` - connection credentials
"""
@spec filters(Hunter.Client.t()) :: [Hunter.Filter.t()]
def filters(conn) do
Request.request!(conn, :get, "/api/v2/filters", :filters)
end
@doc """
Retrieve a filter
## Parameters
* `conn` - connection credentials
* `id` - filter identifier
"""
@spec filter(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.Filter.t()
def filter(conn, id) do
Request.request!(conn, :get, "/api/v2/filters/#{id}", :filter)
end
@doc """
Create a new filter
## Parameters
* `conn` - connection credentials
* `title` - the name of the filter group
* `context` - contexts in which the filter is applied, list of:
`home`, `notifications`, `public`, `thread`, `account`
* `options` - option list
## Options
* `filter_action` - action to take when a status matches the filter, one
of: `warn`, `hide`, `blur`; default: `warn`
* `expires_in` - seconds from now the filter should expire, `nil` to
never expire
* `keywords_attributes` - list of maps with `keyword` and `whole_word`
keys, the keywords to add to the newly-created filter
"""
@spec create_filter(Hunter.Client.t(), String.t(), [String.t()], Keyword.t()) ::
Hunter.Filter.t()
def create_filter(conn, title, context, options \\ []) do
body = options |> Keyword.merge(title: title, context: context) |> Map.new()
Request.request!(conn, :post, "/api/v2/filters", :filter, body)
end
@doc """
Update a filter
## Parameters
* `conn` - connection credentials
* `id` - filter identifier
* `options` - option list
## Options
* `title` - the new name of the filter group
* `context` - contexts in which the filter is applied, list of:
`home`, `notifications`, `public`, `thread`, `account`
* `filter_action` - action to take when a status matches the filter, one
of: `warn`, `hide`, `blur`
* `expires_in` - seconds from now the filter should expire, `nil` to
never expire
* `keywords_attributes` - list of maps with `keyword`, `whole_word`,
`id` and `_destroy` keys, keywords to add, update or remove in place
"""
@spec update_filter(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) ::
Hunter.Filter.t()
def update_filter(conn, id, options) do
Request.request!(conn, :put, "/api/v2/filters/#{id}", :filter, Map.new(options))
end
@doc """
Delete a filter
## Parameters
* `conn` - connection credentials
* `id` - filter identifier
"""
@spec destroy_filter(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def destroy_filter(conn, id) do
Request.request!(conn, :delete, "/api/v2/filters/#{id}", :empty)
end
@doc """
Retrieve the keywords grouped under a filter
## Parameters
* `conn` - connection credentials
* `filter_id` - filter identifier
"""
@spec filter_keywords(Hunter.Client.t(), String.t() | non_neg_integer) :: [
Hunter.FilterKeyword.t()
]
def filter_keywords(conn, filter_id) do
Request.request!(conn, :get, "/api/v2/filters/#{filter_id}/keywords", :filter_keywords)
end
@doc """
Add a keyword to a filter
## Parameters
* `conn` - connection credentials
* `filter_id` - filter identifier
* `keyword` - the phrase to be matched against
* `options` - option list
## Options
* `whole_word` - whether the keyword should consider word boundaries
"""
@spec add_keyword_to_filter(
Hunter.Client.t(),
String.t() | non_neg_integer,
String.t(),
Keyword.t()
) :: Hunter.FilterKeyword.t()
def add_keyword_to_filter(conn, filter_id, keyword, options \\ []) do
body = options |> Keyword.put(:keyword, keyword) |> Map.new()
Request.request!(conn, :post, "/api/v2/filters/#{filter_id}/keywords", :filter_keyword, body)
end
@doc """
Retrieve a filter keyword
## Parameters
* `conn` - connection credentials
* `id` - filter keyword identifier
"""
@spec filter_keyword(Hunter.Client.t(), String.t() | non_neg_integer) ::
Hunter.FilterKeyword.t()
def filter_keyword(conn, id) do
Request.request!(conn, :get, "/api/v2/filters/keywords/#{id}", :filter_keyword)
end
@doc """
Update a filter keyword
## Parameters
* `conn` - connection credentials
* `id` - filter keyword identifier
* `options` - option list
## Options
* `keyword` - the phrase to be matched against
* `whole_word` - whether the keyword should consider word boundaries
"""
@spec update_filter_keyword(Hunter.Client.t(), String.t() | non_neg_integer, Keyword.t()) ::
Hunter.FilterKeyword.t()
def update_filter_keyword(conn, id, options) do
Request.request!(
conn,
:put,
"/api/v2/filters/keywords/#{id}",
:filter_keyword,
Map.new(options)
)
end
@doc """
Remove a keyword from a filter
## Parameters
* `conn` - connection credentials
* `id` - filter keyword identifier
"""
@spec destroy_filter_keyword(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def destroy_filter_keyword(conn, id) do
Request.request!(conn, :delete, "/api/v2/filters/keywords/#{id}", :empty)
end
@doc """
Retrieve the status filters grouped under a filter
## Parameters
* `conn` - connection credentials
* `filter_id` - filter identifier
"""
@spec filter_statuses(Hunter.Client.t(), String.t() | non_neg_integer) :: [
Hunter.FilterStatus.t()
]
def filter_statuses(conn, filter_id) do
Request.request!(conn, :get, "/api/v2/filters/#{filter_id}/statuses", :filter_statuses)
end
@doc """
Add a status to a filter group
## Parameters
* `conn` - connection credentials
* `filter_id` - filter identifier
* `status_id` - status identifier
"""
@spec add_status_to_filter(
Hunter.Client.t(),
String.t() | non_neg_integer,
String.t() | non_neg_integer
) :: Hunter.FilterStatus.t()
def add_status_to_filter(conn, filter_id, status_id) do
Request.request!(conn, :post, "/api/v2/filters/#{filter_id}/statuses", :filter_status, %{
status_id: status_id
})
end
@doc """
Retrieve a status filter
## Parameters
* `conn` - connection credentials
* `id` - status filter identifier
"""
@spec filter_status(Hunter.Client.t(), String.t() | non_neg_integer) :: Hunter.FilterStatus.t()
def filter_status(conn, id) do
Request.request!(conn, :get, "/api/v2/filters/statuses/#{id}", :filter_status)
end
@doc """
Remove a status from a filter group
## Parameters
* `conn` - connection credentials
* `id` - status filter identifier
"""
@spec destroy_filter_status(Hunter.Client.t(), String.t() | non_neg_integer) :: boolean
def destroy_filter_status(conn, id) do
Request.request!(conn, :delete, "/api/v2/filters/statuses/#{id}", :empty)
end
@doc """
Checks the streaming server's health endpoint (Mastodon 2.5+)
## Parameters
* `conn` - connection credentials
* `opts` - `url:` overrides the streaming base URL (`ws://`/`wss://`
accepted and mapped to `http://`/`https://`)
See `Hunter.Streaming.health?/2`; streaming connections themselves are
opened with `Hunter.Streaming.connect/2`.
"""
@spec streaming_health?(Hunter.Client.t(), Keyword.t()) :: boolean
defdelegate streaming_health?(conn, opts \\ []), to: Hunter.Streaming, as: :health?
@doc """
Returns Hunter version
"""
@spec version() :: String.t()
def version, do: @hunter_version
end