Current section

Files

Jump to
hunter lib hunter.ex
Raw

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 """
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 """
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_uri` - where the user should be redirected after authorization,
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()], 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)
app = %Hunter.Application{app | scopes: scopes, redirect_uri: redirect_uris}
if save?, do: save_credentials(client_name, app)
app
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 """
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
* `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)
"""
@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
## 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
## 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
## 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 """
Retrieve access token
## Parameters
* `app` - application details, see: `Hunter.create_app/5` for more details.
* `username` - account's email
* `password` - account's password
* `base_url` - API base url, default: `https://mastodon.social`
"""
@spec log_in(Hunter.Application.t(), String.t(), String.t(), String.t()) :: Hunter.Client.t()
def log_in(
%Hunter.Application{} = app,
username,
password,
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: "password",
username: username,
password: password
}
payload =
case app.scopes do
scopes when is_list(scopes) and scopes != [] ->
Map.put(payload, :scope, Enum.join(scopes, " "))
_ ->
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 access token via OAuth
## Parameters
* `app` - application details, see: `Hunter.create_app/5` for more details.
* `oauth_code` - OAuth authentication code
* `base_url` - API base url, default: `https://mastodon.social`
"""
@spec log_in_oauth(Hunter.Application.t(), String.t(), String.t()) :: Hunter.Client.t()
def log_in_oauth(%Hunter.Application{} = app, oauth_code, 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: "authorization_code",
code: oauth_code,
# Doorkeeper rejects the exchange without a redirect_uri matching the
# authorization; fall back to create_app's default for stale credentials
redirect_uri: app.redirect_uri || "urn:ietf:wg:oauth:2.0:oob"
}
response = Request.request!(base_url, :post, "/oauth/token", nil, payload)
%Hunter.Client{base_url: base_url, access_token: response["access_token"]}
end
@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()) :: list
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 """
Returns Hunter version
"""
@spec version() :: String.t()
def version, do: @hunter_version
end