botiasloop 0.0.1 → 0.0.7

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.
@@ -1,225 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Botiasloop
4
- # Manages conversation state globally, mapping user IDs to conversation UUIDs
5
- # Handles all business logic: switching, finding current, labeling, etc.
6
- class ConversationManager
7
- # Valid label format: alphanumeric, dashes, and underscores
8
- LABEL_REGEX = /\A[a-zA-Z0-9_-]+\z/
9
-
10
- class << self
11
- # Get or create the current conversation for a user
12
- #
13
- # @param user_id [String] User identifier
14
- # @return [Conversation] Current conversation for the user
15
- def current_for(user_id)
16
- user_key = user_id.to_s
17
- conversation = Conversation.where(user_id: user_key, is_current: true, archived: false).first
18
-
19
- if conversation
20
- Conversation[conversation.id]
21
- else
22
- create_new(user_key)
23
- end
24
- end
25
-
26
- # Switch a user to a different conversation by label or UUID
27
- # Auto-unarchives archived conversations when switching to them
28
- #
29
- # @param user_id [String] User identifier
30
- # @param identifier [String] Conversation label or UUID to switch to
31
- # @return [Conversation] The switched-to conversation
32
- # @raise [Error] If conversation with given identifier doesn't exist
33
- def switch(user_id, identifier)
34
- user_key = user_id.to_s
35
- identifier = identifier.to_s.strip
36
-
37
- raise Error, "Usage: /switch <label-or-uuid>" if identifier.empty?
38
-
39
- # First try to find by label (include archived)
40
- conversation = Conversation.where(user_id: user_key, label: identifier).first
41
-
42
- # If not found by label, treat as UUID (include archived)
43
- conversation ||= Conversation.find(id: identifier, user_id: user_key)
44
-
45
- raise Error, "Conversation '#{identifier}' not found" unless conversation
46
-
47
- # Auto-unarchive if switching to an archived conversation
48
- conversation.update(archived: false) if conversation.archived
49
-
50
- # Clear current flag from all user's conversations
51
- Conversation.where(user_id: user_key).update(is_current: false)
52
-
53
- # Set new conversation as current
54
- conversation.update(is_current: true)
55
- Conversation[conversation.id]
56
- end
57
-
58
- # Create a new conversation and switch the user to it
59
- #
60
- # @param user_id [String] User identifier
61
- # @return [Conversation] The newly created conversation
62
- def create_new(user_id)
63
- user_key = user_id.to_s
64
-
65
- # Clear current flag from all user's conversations
66
- Conversation.where(user_id: user_key).update(is_current: false)
67
-
68
- # Create new conversation as current
69
- conversation = Conversation.create(user_id: user_key, is_current: true)
70
- Conversation[conversation.id]
71
- end
72
-
73
- # Get the UUID for a user's current conversation
74
- #
75
- # @param user_id [String] User identifier
76
- # @return [String, nil] Current conversation UUID or nil if none exists
77
- def current_uuid_for(user_id)
78
- conversation = Conversation.where(user_id: user_id.to_s, is_current: true).first
79
- conversation&.id
80
- end
81
-
82
- # List all conversation mappings (excluding archived by default)
83
- #
84
- # @param include_archived [Boolean] Whether to include archived conversations
85
- # @return [Hash] Hash mapping UUIDs to {user_id, label} hashes
86
- def all_mappings(include_archived: false)
87
- dataset = include_archived ? Conversation.dataset : Conversation.where(archived: false)
88
- dataset.all.map do |conv|
89
- [conv.id, {"user_id" => conv.user_id, "label" => conv.label}]
90
- end.to_h
91
- end
92
-
93
- # Remove a user's current conversation
94
- #
95
- # @param user_id [String] User identifier
96
- def remove(user_id)
97
- conversation = Conversation.where(user_id: user_id.to_s, is_current: true).first
98
- return unless conversation
99
-
100
- conversation.destroy
101
- end
102
-
103
- # Clear all conversations (use with caution)
104
- def clear_all
105
- Conversation.db[:conversations].delete
106
- end
107
-
108
- # Get the label for a conversation
109
- #
110
- # @param uuid [String] Conversation UUID
111
- # @return [String, nil] Label value or nil
112
- def label(uuid)
113
- conversation = Conversation.find(id: uuid)
114
- conversation&.label
115
- end
116
-
117
- # Set the label for a conversation
118
- #
119
- # @param uuid [String] Conversation UUID
120
- # @param value [String] Label value
121
- # @return [String] The label value
122
- # @raise [Error] If label format is invalid or already in use
123
- def set_label(uuid, value)
124
- conversation = Conversation.find(id: uuid)
125
- raise Error, "Conversation not found" unless conversation
126
-
127
- # Validate label format
128
- unless value.nil? || value.to_s.empty? || value.to_s.match?(LABEL_REGEX)
129
- raise Error, "Invalid label format. Use only letters, numbers, dashes, and underscores."
130
- end
131
-
132
- # Check uniqueness per user (excluding current conversation)
133
- user_id = conversation.user_id
134
- if value && !value.to_s.empty? && label_exists?(user_id, value, exclude_uuid: uuid)
135
- raise Error, "Label '#{value}' already in use by another conversation"
136
- end
137
-
138
- # Allow empty string to be treated as nil (clearing the label)
139
- value = nil if value.to_s.empty?
140
-
141
- Conversation.where(id: uuid).update(label: value)
142
- value
143
- end
144
-
145
- # Check if a label exists for a user
146
- #
147
- # @param user_id [String] User identifier
148
- # @param label [String] Label to check
149
- # @param exclude_uuid [String, nil] UUID to exclude from check
150
- # @return [Boolean] True if label exists for user
151
- def label_exists?(user_id, label, exclude_uuid: nil)
152
- return false unless label
153
-
154
- query = Conversation.where(user_id: user_id.to_s, label: label)
155
- query = query.exclude(id: exclude_uuid) if exclude_uuid
156
- query.count > 0
157
- end
158
-
159
- # List all conversations for a user
160
- # Sorted by updated_at in descending order (most recently updated first)
161
- #
162
- # @param user_id [String] User identifier
163
- # @param archived [Boolean, nil] Filter by archived status (nil = all, true = archived only, false = unarchived only)
164
- # @return [Array<Hash>] Array of {uuid, label, updated_at} hashes
165
- def list_by_user(user_id, archived: false)
166
- dataset = Conversation.where(user_id: user_id.to_s)
167
- dataset = dataset.where(archived: archived) unless archived.nil?
168
- dataset.order(Sequel.desc(:updated_at)).all.map do |conv|
169
- {uuid: conv.id, label: conv.label, updated_at: conv.updated_at}
170
- end
171
- end
172
-
173
- # Find conversation UUID by label for a user
174
- #
175
- # @param user_id [String] User identifier
176
- # @param label [String] Label to search for
177
- # @return [String, nil] UUID or nil if not found
178
- def find_by_label(user_id, label)
179
- conversation = Conversation.where(user_id: user_id.to_s, label: label).first
180
- conversation&.id
181
- end
182
-
183
- # Archive a conversation by label or UUID, or archive current if no identifier given
184
- # When archiving current conversation, automatically creates a new one
185
- #
186
- # @param user_id [String] User identifier
187
- # @param identifier [String, nil] Conversation label or UUID to archive (nil = archive current)
188
- # @return [Hash] Hash with :archived and :new_conversation keys
189
- # @raise [Error] If conversation not found
190
- def archive(user_id, identifier = nil)
191
- user_key = user_id.to_s
192
- identifier = identifier.to_s.strip
193
-
194
- if identifier.empty?
195
- # Archive current conversation
196
- conversation = Conversation.where(user_id: user_key, is_current: true).first
197
- raise Error, "No current conversation to archive" unless conversation
198
-
199
- # Archive the current conversation
200
- conversation.update(archived: true, is_current: false)
201
-
202
- # Create a new conversation (becomes current)
203
- new_conversation = create_new(user_key)
204
-
205
- {
206
- archived: Conversation[conversation.id],
207
- new_conversation: new_conversation
208
- }
209
- else
210
- # Archive by label or UUID
211
- conversation = Conversation.where(user_id: user_key, label: identifier).first
212
- conversation ||= Conversation.find(id: identifier, user_id: user_key)
213
-
214
- raise Error, "Conversation '#{identifier}' not found" unless conversation
215
-
216
- # Cannot archive current conversation (must use archive without args)
217
- raise Error, "Cannot archive the current conversation. Use /archive without arguments to archive current and start new." if conversation.is_current
218
-
219
- conversation.update(archived: true, is_current: false)
220
- {archived: Conversation[conversation.id]}
221
- end
222
- end
223
- end
224
- end
225
- end