discord_rda 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +398 -0
  4. data/lib/discord_rda/bot.rb +842 -0
  5. data/lib/discord_rda/cache/configurable_cache.rb +283 -0
  6. data/lib/discord_rda/cache/entity_cache.rb +184 -0
  7. data/lib/discord_rda/cache/memory_store.rb +143 -0
  8. data/lib/discord_rda/cache/redis_store.rb +136 -0
  9. data/lib/discord_rda/cache/store.rb +56 -0
  10. data/lib/discord_rda/connection/gateway_client.rb +383 -0
  11. data/lib/discord_rda/connection/invalid_bucket.rb +205 -0
  12. data/lib/discord_rda/connection/rate_limiter.rb +280 -0
  13. data/lib/discord_rda/connection/request_queue.rb +340 -0
  14. data/lib/discord_rda/connection/reshard_manager.rb +328 -0
  15. data/lib/discord_rda/connection/rest_client.rb +316 -0
  16. data/lib/discord_rda/connection/rest_proxy.rb +165 -0
  17. data/lib/discord_rda/connection/scalable_rest_client.rb +526 -0
  18. data/lib/discord_rda/connection/shard_manager.rb +223 -0
  19. data/lib/discord_rda/core/async_runtime.rb +108 -0
  20. data/lib/discord_rda/core/configuration.rb +194 -0
  21. data/lib/discord_rda/core/logger.rb +188 -0
  22. data/lib/discord_rda/core/snowflake.rb +121 -0
  23. data/lib/discord_rda/entity/attachment.rb +88 -0
  24. data/lib/discord_rda/entity/base.rb +103 -0
  25. data/lib/discord_rda/entity/channel.rb +446 -0
  26. data/lib/discord_rda/entity/channel_builder.rb +280 -0
  27. data/lib/discord_rda/entity/color.rb +253 -0
  28. data/lib/discord_rda/entity/embed.rb +221 -0
  29. data/lib/discord_rda/entity/emoji.rb +89 -0
  30. data/lib/discord_rda/entity/factory.rb +99 -0
  31. data/lib/discord_rda/entity/guild.rb +619 -0
  32. data/lib/discord_rda/entity/member.rb +263 -0
  33. data/lib/discord_rda/entity/message.rb +405 -0
  34. data/lib/discord_rda/entity/message_builder.rb +369 -0
  35. data/lib/discord_rda/entity/role.rb +157 -0
  36. data/lib/discord_rda/entity/support.rb +294 -0
  37. data/lib/discord_rda/entity/user.rb +231 -0
  38. data/lib/discord_rda/entity/value_objects.rb +263 -0
  39. data/lib/discord_rda/event/auto_moderation.rb +294 -0
  40. data/lib/discord_rda/event/base.rb +986 -0
  41. data/lib/discord_rda/event/bus.rb +225 -0
  42. data/lib/discord_rda/event/scheduled_event.rb +257 -0
  43. data/lib/discord_rda/hot_reload_manager.rb +303 -0
  44. data/lib/discord_rda/interactions/application_command.rb +436 -0
  45. data/lib/discord_rda/interactions/command_system.rb +484 -0
  46. data/lib/discord_rda/interactions/components.rb +464 -0
  47. data/lib/discord_rda/interactions/interaction.rb +553 -0
  48. data/lib/discord_rda/plugin/analytics_plugin.rb +528 -0
  49. data/lib/discord_rda/plugin/base.rb +190 -0
  50. data/lib/discord_rda/plugin/registry.rb +126 -0
  51. data/lib/discord_rda/version.rb +5 -0
  52. data/lib/discord_rda.rb +70 -0
  53. metadata +302 -0
@@ -0,0 +1,283 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscordRDA
4
+ # Configurable cache manager.
5
+ # By default, caches NOTHING - follows Discordeno philosophy.
6
+ # Users can opt-in to caching only what they need.
7
+ #
8
+ class ConfigurableCache
9
+ # Cache nothing strategy
10
+ STRATEGY_NONE = :none
11
+
12
+ # Cache everything strategy
13
+ STRATEGY_FULL = :full
14
+
15
+ # Custom strategy - user specifies what to cache
16
+ STRATEGY_CUSTOM = :custom
17
+
18
+ # @return [Symbol] Current cache strategy
19
+ attr_reader :strategy
20
+
21
+ # @return [CacheStore] Cache store backend
22
+ attr_reader :store
23
+
24
+ # @return [Logger] Logger instance
25
+ attr_reader :logger
26
+
27
+ # @return [Hash] Enabled cache types
28
+ attr_reader :enabled_caches
29
+
30
+ # @return [Array<Symbol>] Properties to cache per entity
31
+ attr_reader :cached_properties
32
+
33
+ # Initialize configurable cache
34
+ # @param strategy [Symbol] Cache strategy (:none, :full, :custom)
35
+ # @param store [CacheStore] Cache store backend
36
+ # @param logger [Logger] Logger instance
37
+ # @param enabled_caches [Array<Symbol>] Which entity types to cache (for :custom)
38
+ # @param cached_properties [Hash] Which properties to cache per entity
39
+ def initialize(
40
+ strategy: STRATEGY_NONE,
41
+ store: nil,
42
+ logger: nil,
43
+ enabled_caches: [],
44
+ cached_properties: {}
45
+ )
46
+ @strategy = strategy
47
+ @store = store || MemoryStore.new
48
+ @logger = logger
49
+ @enabled_caches = enabled_caches
50
+ @cached_properties = cached_properties
51
+
52
+ @logger&.info('Cache initialized', strategy: strategy, enabled: enabled_caches)
53
+ end
54
+
55
+ # Cache an entity (only if enabled for this type)
56
+ # @param type [Symbol] Entity type
57
+ # @param id [String, Snowflake] Entity ID
58
+ # @param entity [Entity] Entity to cache
59
+ # @param ttl [Integer] Time to live in seconds
60
+ # @return [void]
61
+ def cache(type, id, entity, ttl: 300)
62
+ return unless should_cache?(type)
63
+
64
+ # If custom properties specified, only cache those
65
+ if @cached_properties[type]
66
+ entity = filter_properties(entity, @cached_properties[type])
67
+ end
68
+
69
+ key = "#{type}:#{id}"
70
+ @store.set(key, entity, ttl: ttl)
71
+
72
+ @logger&.debug('Cached entity', type: type, id: id, strategy: @strategy)
73
+ end
74
+
75
+ # Get cached entity
76
+ # @param type [Symbol] Entity type
77
+ # @param id [String, Snowflake] Entity ID
78
+ # @return [Entity, nil] Cached entity or nil
79
+ def get(type, id)
80
+ return nil unless should_cache?(type)
81
+
82
+ key = "#{type}:#{id}"
83
+ @store.get(key)
84
+ end
85
+
86
+ # Check if entity should be cached
87
+ # @param type [Symbol] Entity type
88
+ # @return [Boolean] True if should cache
89
+ def should_cache?(type)
90
+ case @strategy
91
+ when STRATEGY_NONE
92
+ false
93
+ when STRATEGY_FULL
94
+ true
95
+ when STRATEGY_CUSTOM
96
+ @enabled_caches.include?(type)
97
+ else
98
+ false
99
+ end
100
+ end
101
+
102
+ # Invalidate an entity
103
+ # @param type [Symbol] Entity type
104
+ # @param id [String, Snowflake] Entity ID
105
+ # @return [void]
106
+ def invalidate(type, id)
107
+ key = "#{type}:#{id}"
108
+ @store.delete(key)
109
+ end
110
+
111
+ # Clear all cached data
112
+ # @return [void]
113
+ def clear
114
+ @store.clear
115
+ end
116
+
117
+ # Get cache statistics
118
+ # @return [Hash] Statistics
119
+ def stats
120
+ base_stats = @store.respond_to?(:stats) ? @store.stats : {}
121
+
122
+ {
123
+ strategy: @strategy,
124
+ enabled_caches: @enabled_caches,
125
+ **base_stats
126
+ }
127
+ end
128
+
129
+ # Create a new cache with different settings (immutable)
130
+ # @param overrides [Hash] Settings to override
131
+ # @return [ConfigurableCache] New cache instance
132
+ def with(**overrides)
133
+ self.class.new(
134
+ strategy: overrides.fetch(:strategy, @strategy),
135
+ store: overrides.fetch(:store, @store),
136
+ logger: overrides.fetch(:logger, @logger),
137
+ enabled_caches: overrides.fetch(:enabled_caches, @enabled_caches),
138
+ cached_properties: overrides.fetch(:cached_properties, @cached_properties)
139
+ )
140
+ end
141
+
142
+ private
143
+
144
+ def filter_properties(entity, properties)
145
+ return entity unless entity.respond_to?(:to_h)
146
+ return entity if properties.nil? || properties.empty?
147
+
148
+ data = entity.to_h
149
+ # Support nested property selection with dot notation (e.g., "author.username")
150
+ filtered = {}
151
+ properties.each do |prop|
152
+ if prop.to_s.include?('.')
153
+ parts = prop.to_s.split('.')
154
+ current = data
155
+ current_filtered = filtered
156
+
157
+ parts.each_with_index do |part, idx|
158
+ if idx == parts.length - 1
159
+ current_filtered[part] = current[part] if current && current.key?(part)
160
+ else
161
+ current_filtered[part] ||= {}
162
+ current = current&.dig(part)
163
+ current_filtered = current_filtered[part]
164
+ end
165
+ end
166
+ else
167
+ filtered[prop.to_s] = data[prop.to_s] if data.key?(prop.to_s)
168
+ end
169
+ end
170
+
171
+ # Preserve ID if it's a filtered entity
172
+ filtered['id'] = data['id'] if data.key?('id') && !filtered.key?('id')
173
+
174
+ # Create new entity with filtered data
175
+ entity.class.new(filtered)
176
+ end
177
+
178
+ # Advanced property filtering with transforms
179
+ # @param entity [Entity] Entity to filter
180
+ # @param config [Hash] Filter config with :only, :except, :transform options
181
+ # @return [Entity] Filtered entity
182
+ def advanced_filter(entity, config = {})
183
+ return entity unless entity.respond_to?(:to_h)
184
+
185
+ data = entity.to_h
186
+
187
+ # Apply :only filter
188
+ if config[:only]
189
+ data = data.slice(*config[:only].map(&:to_s))
190
+ end
191
+
192
+ # Apply :except filter
193
+ if config[:except]
194
+ data = data.except(*config[:except].map(&:to_s))
195
+ end
196
+
197
+ # Apply transforms
198
+ if config[:transform]
199
+ config[:transform].each do |key, transform|
200
+ key_str = key.to_s
201
+ data[key_str] = transform.call(data[key_str]) if data.key?(key_str)
202
+ end
203
+ end
204
+
205
+ entity.class.new(data)
206
+ end
207
+
208
+ # Filter entities by custom predicate
209
+ # @param type [Symbol] Entity type
210
+ # @yield Block to filter entities
211
+ # @return [Array<Entity>] Filtered entities
212
+ def filter_by(type)
213
+ return [] unless block_given?
214
+ return [] unless should_cache?(type)
215
+
216
+ pattern = "#{type}:*"
217
+ all = @store.scan(pattern)
218
+ all.select { |_, entity| yield(entity) }.map { |_, entity| entity }
219
+ end
220
+
221
+ # Get filtered properties configuration for an entity type
222
+ # @param type [Symbol] Entity type
223
+ # @return [Array<Symbol>, nil] Properties to cache, or nil for all
224
+ def filter_for(type)
225
+ @cached_properties[type]
226
+ end
227
+
228
+ # Set filtered properties for an entity type
229
+ # @param type [Symbol] Entity type
230
+ # @param properties [Array<Symbol>] Properties to cache
231
+ # @return [void]
232
+ def set_filter(type, *properties)
233
+ @cached_properties[type] = properties.flatten
234
+ end
235
+
236
+ # Clear filter for an entity type
237
+ # @param type [Symbol] Entity type
238
+ # @return [void]
239
+ def clear_filter(type)
240
+ @cached_properties.delete(type)
241
+ end
242
+
243
+ # Batch cache entities with property filtering
244
+ # @param type [Symbol] Entity type
245
+ # @param entities [Array<Entity>] Entities to cache
246
+ # @param ttl [Integer] Time to live in seconds
247
+ # @return [void]
248
+ def cache_batch(type, entities, ttl: 300)
249
+ return unless should_cache?(type)
250
+
251
+ entities.each do |entity|
252
+ cache(type, entity.id, entity, ttl: ttl) if entity.respond_to?(:id)
253
+ end
254
+ end
255
+
256
+ # Get multiple entities by IDs
257
+ # @param type [Symbol] Entity type
258
+ # @param ids [Array<String, Snowflake>] Entity IDs
259
+ # @return [Array<Entity>] Found entities
260
+ def get_many(type, ids)
261
+ return [] unless should_cache?(type)
262
+
263
+ ids.map { |id| get(type, id) }.compact
264
+ end
265
+
266
+ # Invalidate multiple entities
267
+ # @param type [Symbol] Entity type
268
+ # @param ids [Array<String, Snowflake>] Entity IDs
269
+ # @return [void]
270
+ def invalidate_many(type, ids)
271
+ ids.each { |id| invalidate(type, id) }
272
+ end
273
+
274
+ # Invalidate by pattern
275
+ # @param pattern [String] Pattern to match (e.g., "guild:*:members")
276
+ # @return [Integer] Number of entries invalidated
277
+ def invalidate_pattern(pattern)
278
+ keys = @store.scan(pattern).map { |k, _| k }
279
+ keys.each { |k| @store.delete(k) }
280
+ keys.size
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DiscordRDA
4
+ # Typed entity cache with automatic invalidation.
5
+ # Provides methods for caching and retrieving Discord entities.
6
+ #
7
+ class EntityCache
8
+ # @return [CacheStore] Cache store
9
+ attr_reader :store
10
+
11
+ # @return [Logger] Logger instance
12
+ attr_reader :logger
13
+
14
+ # Entity TTLs in seconds
15
+ TTL = {
16
+ user: 300,
17
+ guild: 60,
18
+ channel: 300,
19
+ message: 60,
20
+ role: 300,
21
+ member: 120
22
+ }.freeze
23
+
24
+ # Initialize entity cache
25
+ # @param store [CacheStore] Cache store instance
26
+ # @param logger [Logger] Logger instance
27
+ def initialize(store, logger: nil)
28
+ @store = store
29
+ @logger = logger
30
+ end
31
+
32
+ # Cache a user
33
+ # @param user [User] User to cache
34
+ # @return [void]
35
+ def cache_user(user)
36
+ cache(:user, user.id, user)
37
+ end
38
+
39
+ # Get a cached user
40
+ # @param user_id [String, Snowflake] User ID
41
+ # @return [User, nil] Cached user
42
+ def user(user_id)
43
+ get(:user, user_id)
44
+ end
45
+
46
+ # Cache a guild
47
+ # @param guild [Guild] Guild to cache
48
+ # @return [void]
49
+ def cache_guild(guild)
50
+ cache(:guild, guild.id, guild)
51
+ end
52
+
53
+ # Get a cached guild
54
+ # @param guild_id [String, Snowflake] Guild ID
55
+ # @return [Guild, nil] Cached guild
56
+ def guild(guild_id)
57
+ get(:guild, guild_id)
58
+ end
59
+
60
+ # Cache a channel
61
+ # @param channel [Channel] Channel to cache
62
+ # @return [void]
63
+ def cache_channel(channel)
64
+ cache(:channel, channel.id, channel)
65
+ end
66
+
67
+ # Get a cached channel
68
+ # @param channel_id [String, Snowflake] Channel ID
69
+ # @return [Channel, nil] Cached channel
70
+ def channel(channel_id)
71
+ get(:channel, channel_id)
72
+ end
73
+
74
+ # Cache a message
75
+ # @param message [Message] Message to cache
76
+ # @return [void]
77
+ def cache_message(message)
78
+ cache(:message, message.id, message)
79
+ end
80
+
81
+ # Get a cached message
82
+ # @param message_id [String, Snowflake] Message ID
83
+ # @return [Message, nil] Cached message
84
+ def message(message_id)
85
+ get(:message, message_id)
86
+ end
87
+
88
+ # Cache a role
89
+ # @param role [Role] Role to cache
90
+ # @return [void]
91
+ def cache_role(role)
92
+ cache(:role, role.id, role)
93
+ end
94
+
95
+ # Get a cached role
96
+ # @param role_id [String, Snowflake] Role ID
97
+ # @return [Role, nil] Cached role
98
+ def role(role_id)
99
+ get(:role, role_id)
100
+ end
101
+
102
+ # Cache a member
103
+ # @param member [Member] Member to cache
104
+ # @param guild_id [String, Snowflake] Guild ID
105
+ # @return [void]
106
+ def cache_member(member, guild_id)
107
+ key = "#{guild_id}:#{member.id}"
108
+ cache(:member, key, member)
109
+ end
110
+
111
+ # Get a cached member
112
+ # @param user_id [String, Snowflake] User ID
113
+ # @param guild_id [String, Snowflake] Guild ID
114
+ # @return [Member, nil] Cached member
115
+ def member(user_id, guild_id)
116
+ key = "#{guild_id}:#{user_id}"
117
+ get(:member, key)
118
+ end
119
+
120
+ # Invalidate an entity
121
+ # @param type [Symbol] Entity type
122
+ # @param id [String, Snowflake] Entity ID
123
+ # @return [void]
124
+ def invalidate(type, id)
125
+ key = build_key(type, id)
126
+ @store.delete(key)
127
+ @logger&.debug('Invalidated cache', type: type, id: id)
128
+ end
129
+
130
+ # Invalidate by guild ID
131
+ # @param guild_id [String, Snowflake] Guild ID
132
+ # @return [void]
133
+ def invalidate_guild(guild_id)
134
+ guild_key = guild_id.to_s
135
+ deleted_count = 0
136
+
137
+ # Delete the guild itself
138
+ @store.delete("guild:#{guild_key}")
139
+ deleted_count += 1
140
+
141
+ # Delete all members associated with this guild
142
+ if @store.respond_to?(:keys)
143
+ member_keys = @store.keys("member:#{guild_key}:*")
144
+ member_keys.each do |key|
145
+ @store.delete(key)
146
+ deleted_count += 1
147
+ end
148
+ end
149
+
150
+ @logger&.debug('Invalidated guild cache', guild_id: guild_id, deleted: deleted_count)
151
+ end
152
+
153
+ # Clear all cached entities
154
+ # @return [void]
155
+ def clear
156
+ @store.clear
157
+ @logger&.info('Cleared entity cache')
158
+ end
159
+
160
+ # Get cache statistics
161
+ # @return [Hash] Statistics
162
+ def stats
163
+ @store.respond_to?(:stats) ? @store.stats : {}
164
+ end
165
+
166
+ private
167
+
168
+ def cache(type, id, entity)
169
+ key = build_key(type, id)
170
+ ttl = TTL[type]
171
+ @store.set(key, entity, ttl: ttl)
172
+ @logger&.debug('Cached entity', type: type, id: id)
173
+ end
174
+
175
+ def get(type, id)
176
+ key = build_key(type, id)
177
+ @store.get(key)
178
+ end
179
+
180
+ def build_key(type, id)
181
+ "#{type}:#{id}"
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lru_redux'
4
+
5
+ module DiscordRDA
6
+ # In-memory LRU cache store.
7
+ # Provides fast, thread-safe caching with size limits and TTL.
8
+ #
9
+ class MemoryStore < CacheStore
10
+ # Default cache size
11
+ DEFAULT_SIZE = 10_000
12
+
13
+ # @return [Integer] Maximum cache size
14
+ attr_reader :max_size
15
+
16
+ # @return [Hash] TTL tracking
17
+ attr_reader :ttl_data
18
+
19
+ # Initialize memory store
20
+ # @param max_size [Integer] Maximum cache size
21
+ def initialize(max_size: DEFAULT_SIZE)
22
+ @max_size = max_size
23
+ @cache = LruRedux::Cache.new(max_size)
24
+ @ttl_data = {}
25
+ @mutex = Mutex.new
26
+ end
27
+
28
+ # Get a value from cache
29
+ # @param key [String] Cache key
30
+ # @return [Object, nil] Cached value or nil if expired/missing
31
+ def get(key)
32
+ @mutex.synchronize do
33
+ check_ttl(key)
34
+ @cache[key]
35
+ end
36
+ end
37
+
38
+ # Set a value in cache
39
+ # @param key [String] Cache key
40
+ # @param value [Object] Value to cache
41
+ # @param ttl [Integer] Time to live in seconds
42
+ # @return [void]
43
+ def set(key, value, ttl: nil)
44
+ @mutex.synchronize do
45
+ @cache[key] = value
46
+ @ttl_data[key] = Time.now.utc + ttl if ttl
47
+ end
48
+ end
49
+
50
+ # Delete a value from cache
51
+ # @param key [String] Cache key
52
+ # @return [void]
53
+ def delete(key)
54
+ @mutex.synchronize do
55
+ @cache.delete(key)
56
+ @ttl_data.delete(key)
57
+ end
58
+ end
59
+
60
+ # Check if key exists and is not expired
61
+ # @param key [String] Cache key
62
+ # @return [Boolean] True if exists
63
+ def exist?(key)
64
+ @mutex.synchronize do
65
+ check_ttl(key)
66
+ @cache.key?(key)
67
+ end
68
+ end
69
+
70
+ # Clear all cached values
71
+ # @return [void]
72
+ def clear
73
+ @mutex.synchronize do
74
+ @cache.clear
75
+ @ttl_data.clear
76
+ end
77
+ end
78
+
79
+ # Get current cache size
80
+ # @return [Integer] Number of cached items
81
+ def size
82
+ @mutex.synchronize do
83
+ clean_expired
84
+ @cache.size
85
+ end
86
+ end
87
+
88
+ # Get cache statistics
89
+ # @return [Hash] Statistics
90
+ def stats
91
+ @mutex.synchronize do
92
+ clean_expired
93
+ {
94
+ size: @cache.size,
95
+ max_size: @max_size,
96
+ ttl_entries: @ttl_data.size
97
+ }
98
+ end
99
+ end
100
+
101
+ # Get keys matching a pattern
102
+ # @param pattern [String, Regexp] Pattern to match (supports globs like "member:123:*")
103
+ # @return [Array<String>] Matching keys
104
+ def keys(pattern)
105
+ @mutex.synchronize do
106
+ clean_expired
107
+
108
+ regex = if pattern.is_a?(Regexp)
109
+ pattern
110
+ else
111
+ # Convert glob pattern to regex
112
+ regex_str = pattern.gsub('.', '\\.')
113
+ .gsub('*', '.*')
114
+ .gsub('?', '.')
115
+ Regexp.new("^#{regex_str}$")
116
+ end
117
+
118
+ @cache.keys.select { |k| k.match?(regex) }
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def check_ttl(key)
125
+ ttl = @ttl_data[key]
126
+ return unless ttl
127
+
128
+ if Time.now.utc > ttl
129
+ @cache.delete(key)
130
+ @ttl_data.delete(key)
131
+ end
132
+ end
133
+
134
+ def clean_expired
135
+ now = Time.now.utc
136
+ expired = @ttl_data.select { |_, ttl| now > ttl }.keys
137
+ expired.each do |key|
138
+ @cache.delete(key)
139
+ @ttl_data.delete(key)
140
+ end
141
+ end
142
+ end
143
+ end