Class: DiscordStore::Search
- Inherits:
-
Object
- Object
- DiscordStore::Search
- Defined in:
- lib/discord_store/search.rb
Overview
Discord's message index, exposed deliberately and used for almost nothing.
Discord does have a query API. GET /guilds/{id}/messages/search takes a
full-text content filter, channel and author filters, snowflake bounds,
attachment filename and extension filters, and sorts by relevance or time.
It is more of a query interface than a chat platform owes anybody.
It is still not where this library reads from, for four reasons that are Discord's documentation rather than an opinion:
1. It is allowed to under-return. "Search may return slightly fewer
results than the limit specified", and clients "should not rely on the
length of the messages array to paginate". An index that silently drops
rows gives you wrong answers, not slow ones, and a query layer that is
sometimes wrong is worse than no query layer.
2. It is eventually consistent. Fresh answer 202 until indexed,
so there is no read-your-writes.
3. It pages at 25 and cannot offset past 9975, capping any single query
at ten thousand rows across four hundred round trips.
4. Everything this library stores is AES-256-GCM ciphertext. Discord's
index tokenises words; there are none. Making the payloads searchable
would mean storing them in the clear on somebody else's servers, which
is a much larger concession than the one on the front of the README.
So SQLite remains the query layer, and this exists for the things search is
genuinely better at than a channel scan: finding our own messages across a
guild without knowing which channel they are in. The metadata it matches on
-- the DS1 framing prefix and the .ds1 attachment extension -- is
plaintext already, and deliberately says nothing about the contents.
Defined Under Namespace
Classes: Result
Constant Summary collapse
- MARKER =
The plaintext framing prefix every message this library writes begins with. Not a secret, and not informative: it identifies the format, not the contents.
"DS1"- EXTENSION =
Attachment extension used by both blob chunks and spilled log records.
"ds1"- PAGE_SIZE =
One page. Discord's own maximum.
Transport::REST::SEARCH_PAGE_LIMIT
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
Instance Method Summary collapse
-
#attachments(**options) ⇒ Result
Every attachment this library uploaded: blob chunks and spilled records.
-
#each(**options) {|message| ... } ⇒ Enumerator, void
Pages through results.
-
#initialize(rest:, config:, guild_id: nil) ⇒ Search
constructor
A new instance of Search.
-
#messages(content: nil, channel_ids: nil, after: nil, before: nil, has: nil, attachment_extension: nil, attachment_filename: nil, pinned: nil, sort_by: nil, sort_order: nil, limit: PAGE_SIZE, offset: 0, author_id: nil) ⇒ Result
Searches for messages this bot wrote.
-
#ours(**options) ⇒ Result
Every message this library wrote, anywhere in the guild.
Constructor Details
#initialize(rest:, config:, guild_id: nil) ⇒ Search
Returns a new instance of Search.
77 78 79 80 81 82 83 |
# File 'lib/discord_store/search.rb', line 77 def initialize(rest:, config:, guild_id: nil) @rest = rest @config = config @guild_id = (guild_id || config.guild_id)&.to_s raise ConfigurationError, "guild_id is required to search" if @guild_id.nil? || @guild_id.empty? end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
72 73 74 |
# File 'lib/discord_store/search.rb', line 72 def config @config end |
Instance Method Details
#attachments(**options) ⇒ Result
Every attachment this library uploaded: blob chunks and spilled records.
138 139 140 |
# File 'lib/discord_store/search.rb', line 138 def (**) (has: "file", attachment_extension: EXTENSION, **) end |
#each(**options) {|message| ... } ⇒ Enumerator, void
Pages through results.
Pages by offset rather than by the size of the last page, because
Discord says not to paginate on that size. Stops at the offset ceiling
with an explicit error rather than quietly returning a truncated set.
150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/discord_store/search.rb', line 150 def each(**, &block) return enum_for(:each, **) unless block offset = .delete(:offset).to_i seen = 0 loop do page = (**, offset: offset, limit: PAGE_SIZE) page.each(&block) seen += page.size offset += PAGE_SIZE break if page.empty? || seen >= page.total_results break if exhausted?(offset, page) end end |
#messages(content: nil, channel_ids: nil, after: nil, before: nil, has: nil, attachment_extension: nil, attachment_filename: nil, pinned: nil, sort_by: nil, sort_order: nil, limit: PAGE_SIZE, offset: 0, author_id: nil) ⇒ Result
Searches for messages this bot wrote.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
# File 'lib/discord_store/search.rb', line 102 def (content: nil, channel_ids: nil, after: nil, before: nil, has: nil, attachment_extension: nil, attachment_filename: nil, pinned: nil, sort_by: nil, sort_order: nil, limit: PAGE_SIZE, offset: 0, author_id: nil) params = { content: content, channel_id: Array(channel_ids).map(&:to_s), min_id: bound(after), max_id: bound(before), has: Array(has), attachment_extension: Array(), attachment_filename: Array(), pinned: pinned, sort_by: sort_by, sort_order: sort_order, limit: limit.to_i.clamp(1, PAGE_SIZE), offset: assert_offset!(offset), author_id: () } body = @rest.(@guild_id, **params) Result.new( messages: body["messages"], total_results: body["total_results"], indexing: body["indexing"] ) end |
#ours(**options) ⇒ Result
Every message this library wrote, anywhere in the guild.
133 |
# File 'lib/discord_store/search.rb', line 133 def ours(**) = (content: MARKER, **) |