Search¶
Message search matches indexed messages against a text query and a set of filters. TEBHOS indexes messages in Elasticsearch. Guild member search covers member search, and Discovery covers guild search.
There are two routes. Search messages (POST /search/messages) accepts user sessions and bot tokens and can search one channel, one guild, or every guild and direct message of the caller. Search messages (query string) (GET /messages/search) is an older, user-only TEBHOS form.
Every result is limited to channels the caller can read: in a guild channel the caller needs VIEW_CHANNEL and READ_MESSAGE_HISTORY, and in a private channel the caller must be a recipient. Messages in hidden channels never appear in results.
Search messages¶
POST /search/messages
Searches indexed messages and returns a global search result object. Accepts user sessions and bot tokens; a bot may only use the current scope.
Every hit is re-read from the message store before it is returned. A hit whose message no longer exists is dropped from the page and removed from the index, so a page can hold fewer entries than hits_per_page.
JSON body¶
| Field | Type | Description |
|---|---|---|
| scope? | string | Search scope, default current |
| context_channel_id? | snowflake | With scope current: the channel to search. For DM scopes: a DM of the caller that is always included |
| context_guild_id? | snowflake | With scope current: the guild to search (takes precedence over context_channel_id) |
| channel_ids? | array[snowflake] | At most 500. Narrow the search to these channels; each must be readable by the caller (403 MISSING_PERMISSIONS) and, with context_guild_id, belong to that guild |
| content? | string | Free-text query (1-1,024 characters), matched against message content and embed text |
| contents? | array[string] | At most 100 queries; a message matching any of them is returned (replaces content) |
| exact_phrases? | array[string] | At most 10 phrases that must all appear contiguously |
| hits_per_page? | integer | 1-25, default 25 |
| page? | integer | 1-400, default 1. Ignored when cursor is given |
| cursor? | array[string] | The cursor of the previous page, to page past page 400 |
| max_id? / min_id? | snowflake | Only messages with an ID below / above this value |
| exclude_channel_id? | array[snowflake] | Channels to leave out |
| author_id? / exclude_author_id? | array[snowflake] | Authors to include / exclude (at most 100) |
| author_type? / exclude_author_type? | array[string] | user, bot or webhook |
| mentions? | array[snowflake] | Users who must all be mentioned |
| exclude_mentions? | array[snowflake] | Users who must not be mentioned |
| mention_everyone? | boolean | Whether the message mentions @everyone |
| pinned? | boolean | Whether the message is pinned |
| has? / exclude_has? | array[string] | Content types that must all be present / must be absent |
| embed_type? / exclude_embed_type? | array[string] | Embed types: image, video, sound, article |
| embed_provider? / exclude_embed_provider? | array[string] | Embed provider names |
| link_hostname? / exclude_link_hostname? | array[string] | Hostnames of links in the text (compared in lowercase) |
| attachment_filename? / exclude_attachment_filename? | array[string] | Exact attachment file names |
| attachment_extension? / exclude_attachment_extension? | array[string] | Attachment extensions, without the dot |
| sort_by? | string | timestamp (default) or relevance |
| sort_order? | string | desc (default) or asc; applies to timestamp |
| include_nsfw? | boolean | Accepted for compatibility. TEBHOS has no age-restricted channels, so it changes nothing |
Snowflakes may be sent as strings or numbers. Filters on the same list must all match (mentions, has, embed_type, link_hostname, attachment_filename, attachment_extension), except author_id and author_type, which match any listed value.
Filters added to the index
author_type, mention_everyone, has: snapshot, the embed filters and embed text matching read fields that TEBHOS added to its search index. Messages indexed before these fields existed match them only after a re-index. has: poll matches nothing, because polls are not indexed.
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | global search result object | Search completed, possibly with no match |
| 400 | error response INVALID_FORM_BODY |
A field is out of range or malformed; errors[].code is CONTEXT_CHANNEL_OR_GUILD_ID_REQUIRED (scope current without a context), BOT_SEARCH_SCOPE_UNAVAILABLE (a bot used another scope) or ALL_CHANNELS_MUST_BELONG_TO_GUILD |
| 403 | error response | MISSING_PERMISSIONS: a listed channel is not readable. FORBIDDEN: the context channel cannot be viewed |
| 404 | error response | The context channel does not exist or is not visible (NOT_FOUND), or the caller is not a member of the context guild (UNKNOWN_GUILD) |
| 503 | error response FEATURE_TEMPORARILY_DISABLED |
No search backend is configured for the instance |
Rate limit¶
20 requests per 10 seconds per account, on the search:messages bucket (shared with the query-string route).
Example¶
curl -X POST https://tebhos.com/api/v1/search/messages \
-H "Authorization: Bot $TEBHOS_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"context_guild_id": "1489002177550843904", "content": "release notes", "has": ["link"], "hits_per_page": 10}'
Global search scopes¶
| Value | What is searched |
|---|---|
| current | With context_guild_id: every readable channel of that guild (the caller must be a member). Otherwise the context_channel_id channel; without READ_MESSAGE_HISTORY only messages after the guild's history cutoff are returned, and nothing when there is no cutoff |
| open_dms | Direct and group direct messages the caller has open |
| all_dms | Every direct and group direct message the caller has, including closed ones |
| all_guilds | Every readable channel of every guild the caller is a member of |
| all | all_dms and all_guilds together |
| open_dms_and_all_guilds | open_dms and all_guilds together |
Global search result object¶
| Field | Type | Description |
|---|---|---|
| messages | array[message object] | The matching messages on this page, without referenced_message |
| channels | array[channel object] | The channels the returned messages belong to |
| total | integer | The number of matching messages, reduced by stale hits dropped on this page. For a channel searched without READ_MESSAGE_HISTORY it is the number of messages returned |
| hits_per_page | integer | The page size used |
| page | integer | The page that was returned |
| cursor? | array[string] | Pass it back as cursor to get the next page; omitted when the page is empty |
Global search content types¶
| Value | Description |
|---|---|
| image | Has an image attachment |
| video | Has a video attachment |
| sound | Has an audio attachment |
| file | Has at least one attachment |
| sticker | Has at least one sticker |
| embed | Has at least one embed |
| link | Has a link in its text |
| snapshot | Is a forwarded message |
| poll | Has a poll (never matches on TEBHOS) |
Search messages (query string)¶
GET /messages/search
The older TEBHOS search route. User-only: a bot token is rejected with 403 ACCESS_DENIED.
Searches indexed messages and returns a message search result object.
Every hit is re-read from the message store before it is returned. A hit whose message no longer exists is dropped from the page and removed from the index, so a page can hold fewer entries than limit.
Query parameters¶
| Field | Type | Description |
|---|---|---|
| q | string | The free-text query, matched against message content and the author's display name. Required, but may be empty to match every message |
| channel_id? | snowflake | The channel to search. Required unless server_id is given without it (see search scopes) |
| server_id? | snowflake | The guild to search |
| scope? | string | channel (default) or server; see search scopes |
| limit? | integer | Results per page (at most 50, default 25) |
| page? | integer | The page number, starting at 1 (default 1) |
| author_id? | snowflake | Only messages by this user |
| exclude_author_id? | snowflake | Exclude messages by this user |
| mentions? | snowflake | Only messages that mention this user |
| pinned? | boolean | true returns only pinned messages |
| has? | string | Comma-separated content types that must all be present |
| exclude_has? | string | Comma-separated content types that must all be absent |
| link? | string | Only messages with a link to this exact hostname (compared in lowercase) |
| filename? | string | Only messages with an attachment of exactly this filename |
| ext? | string | Only messages with an attachment of this extension (without the dot, compared in lowercase) |
| before? | string | Only messages created before this local day (YYYY-MM-DD) |
| after? | string | Only messages created after this local day (YYYY-MM-DD), that is from the start of the following day |
| during? | string | Only messages created on this local day (YYYY-MM-DD) |
| tz_offset? | integer | The client's time-zone offset in minutes, as returned by JavaScript getTimezoneOffset() (for example -180 for UTC+3), used to turn the day filters into UTC bounds. Clamped to -840..840, default 0 |
| sort? | string | Sort mode (default newest) |
An invalid date in before, after or during returns 400 BAD_REQUEST.
Response¶
| Status | Body | Condition |
|---|---|---|
| 200 | message search result object | Search completed, possibly with no match |
| 400 | error response | Neither channel_id nor server_id was supplied, q is missing, or an ID or date is malformed (BAD_REQUEST) |
| 403 | error response | The caller is a bot (ACCESS_DENIED), is not a member of the guild, or cannot view the channel (FORBIDDEN) |
| 404 | error response | The channel does not exist or the caller cannot see it (NOT_FOUND) |
| 500 | error response | No search backend is configured for the instance |
Rate limit¶
20 requests per 10 seconds per user, on the search:messages bucket.
Example¶
curl -G "https://tebhos.com/api/v1/messages/search" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "q=release notes" \
--data-urlencode "channel_id=1489002177550843905" \
--data-urlencode "has=link,image" \
--data-urlencode "sort=newest"
Search scopes¶
| Parameters | What is searched |
|---|---|
channel_id (scope omitted or channel) |
That single channel. The caller needs VIEW_CHANNEL. Without READ_MESSAGE_HISTORY, only messages after the guild's message-history cutoff are returned, and nothing is returned when the guild has no cutoff |
channel_id + scope=server + server_id |
Every channel of the guild in which the caller has both VIEW_CHANNEL and READ_MESSAGE_HISTORY. The caller must be a member of the guild. All filters apply |
server_id only |
The whole guild, limited to channels the caller can read. Only q and limit apply; the other filters and page are ignored, and the response has only items |
Direct message and group direct message channels are searched with channel_id; the caller must be a recipient.
Message search result object¶
Structure¶
| Field | Type | Description |
|---|---|---|
| items | array[message object] | The matching messages on this page |
| total | integer | The approximate number of matching messages |
| page | integer | The page that was returned |
| total_pages | integer | ceil(total / limit), or 0 when nothing matched |
| next_cursor | ?string | The next page number as a string, or null on the last page |
total comes from the search index and is reduced by the number of stale hits dropped on the current page, so it is an estimate. Page through results by incrementing page until next_cursor is null.
The server_id-only form returns {"items": [...]} without the paging fields.
Example¶
Search content types¶
Values accepted by has and exclude_has. Unknown values are ignored.
| Value | Description |
|---|---|
| image | Has an image attachment |
| video | Has a video attachment |
| sound | Has an audio attachment |
| file | Has at least one attachment |
| link | Has a link in its text |
| embed | Has at least one embed |
| sticker | Has at least one sticker |
Search sort modes¶
| Value | Description |
|---|---|
| newest | Newest first (by creation time, then message ID) |
| oldest | Oldest first |
| relevance | Uses the backend's default ordering when q is not empty. The index always appends a descending message ID sort, so in practice this is also newest first. With an empty q, it falls back to newest |