activematrix 0.0.7 → 0.0.9
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.
- checksums.yaml +4 -4
- data/README.md +96 -28
- data/app/models/active_matrix/agent.rb +36 -1
- data/app/models/active_matrix/agent_store.rb +29 -0
- data/app/models/active_matrix/application_record.rb +8 -0
- data/app/models/active_matrix/chat_session.rb +29 -0
- data/app/models/active_matrix/knowledge_base.rb +26 -0
- data/exe/activematrix +7 -0
- data/lib/active_matrix/agent_manager.rb +160 -121
- data/lib/active_matrix/agent_registry.rb +25 -21
- data/lib/active_matrix/api.rb +8 -2
- data/lib/active_matrix/async_query.rb +58 -0
- data/lib/active_matrix/bot/base.rb +3 -3
- data/lib/active_matrix/bot/builtin_commands.rb +188 -0
- data/lib/active_matrix/bot/command_parser.rb +175 -0
- data/lib/active_matrix/cli.rb +273 -0
- data/lib/active_matrix/client.rb +21 -6
- data/lib/active_matrix/client_pool.rb +38 -27
- data/lib/active_matrix/daemon/probe_server.rb +118 -0
- data/lib/active_matrix/daemon/signal_handler.rb +156 -0
- data/lib/active_matrix/daemon/worker.rb +109 -0
- data/lib/active_matrix/daemon.rb +236 -0
- data/lib/active_matrix/engine.rb +7 -3
- data/lib/active_matrix/errors.rb +1 -1
- data/lib/active_matrix/event_router.rb +61 -49
- data/lib/active_matrix/events.rb +1 -0
- data/lib/active_matrix/instrumentation.rb +148 -0
- data/lib/active_matrix/memory/agent_memory.rb +7 -21
- data/lib/active_matrix/memory/conversation_memory.rb +4 -20
- data/lib/active_matrix/memory/global_memory.rb +15 -30
- data/lib/active_matrix/message_dispatcher.rb +197 -0
- data/lib/active_matrix/metrics.rb +424 -0
- data/lib/active_matrix/presence_manager.rb +181 -0
- data/lib/active_matrix/telemetry.rb +134 -0
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +12 -2
- data/lib/generators/active_matrix/install/install_generator.rb +3 -15
- data/lib/generators/active_matrix/install/templates/README +5 -2
- data/lib/generators/active_matrix/install/templates/active_matrix.yml +32 -0
- metadata +142 -45
- data/lib/active_matrix/protocols/cs/message_relationships.rb +0 -318
- data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +0 -17
- data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
- data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
- data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +0 -26
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActiveMatrix
|
|
4
|
-
module Protocols
|
|
5
|
-
module CS
|
|
6
|
-
# Handles message relationships (replies, edits, reactions, threads)
|
|
7
|
-
# @see https://spec.matrix.org/latest/client-server-api/#relationships
|
|
8
|
-
module MessageRelationships
|
|
9
|
-
# Send a message as a reply to another event
|
|
10
|
-
#
|
|
11
|
-
# @param room_id [String] The room ID
|
|
12
|
-
# @param event_id [String] The event ID to reply to
|
|
13
|
-
# @param content [Hash,String] The message content
|
|
14
|
-
# @param msgtype [String] The message type (defaults to 'm.text')
|
|
15
|
-
# @param params [Hash] Additional parameters for send_message_event
|
|
16
|
-
# @return [Response] The response from the server
|
|
17
|
-
#
|
|
18
|
-
# @example Reply to a message
|
|
19
|
-
# api.reply_to(room_id, event_id, "I agree!")
|
|
20
|
-
#
|
|
21
|
-
# @example Reply with custom msgtype
|
|
22
|
-
# api.reply_to(room_id, event_id, { url: "mxc://..." }, msgtype: 'm.image')
|
|
23
|
-
def reply_to(room_id, event_id, content, msgtype: 'm.text', **params)
|
|
24
|
-
content = { body: content } if content.is_a?(String)
|
|
25
|
-
content[:msgtype] ||= msgtype
|
|
26
|
-
|
|
27
|
-
# Add the reply relationship
|
|
28
|
-
content[:'m.relates_to'] = {
|
|
29
|
-
'm.in_reply_to' => {
|
|
30
|
-
event_id: event_id
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
send_message_event(room_id, 'm.room.message', content, **params)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Send a threaded message
|
|
38
|
-
#
|
|
39
|
-
# @param room_id [String] The room ID
|
|
40
|
-
# @param thread_root_id [String] The root event ID of the thread
|
|
41
|
-
# @param content [Hash,String] The message content
|
|
42
|
-
# @param msgtype [String] The message type (defaults to 'm.text')
|
|
43
|
-
# @param latest_event_id [String,nil] The latest event in the thread (for fallback)
|
|
44
|
-
# @param include_fallback [Boolean] Include reply fallback for older clients
|
|
45
|
-
# @param params [Hash] Additional parameters for send_message_event
|
|
46
|
-
# @return [Response] The response from the server
|
|
47
|
-
#
|
|
48
|
-
# @example Send a threaded message
|
|
49
|
-
# api.send_threaded_message(room_id, root_event_id, "This is a thread reply")
|
|
50
|
-
#
|
|
51
|
-
# @example Send a threaded message with fallback
|
|
52
|
-
# api.send_threaded_message(room_id, root_id, "Reply", latest_event_id: last_id, include_fallback: true)
|
|
53
|
-
def send_threaded_message(room_id, thread_root_id, content, msgtype: 'm.text',
|
|
54
|
-
latest_event_id: nil, include_fallback: false, **params)
|
|
55
|
-
content = { body: content } if content.is_a?(String)
|
|
56
|
-
content[:msgtype] ||= msgtype
|
|
57
|
-
|
|
58
|
-
# Build the thread relationship
|
|
59
|
-
relates_to = {
|
|
60
|
-
rel_type: 'm.thread',
|
|
61
|
-
event_id: thread_root_id
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
# Add fallback for older clients if requested
|
|
65
|
-
if include_fallback && latest_event_id
|
|
66
|
-
relates_to[:'m.in_reply_to'] = {
|
|
67
|
-
event_id: latest_event_id
|
|
68
|
-
}
|
|
69
|
-
relates_to[:is_falling_back] = true
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
content[:'m.relates_to'] = relates_to
|
|
73
|
-
send_message_event(room_id, 'm.room.message', content, **params)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Edit an existing message
|
|
77
|
-
#
|
|
78
|
-
# @param room_id [String] The room ID
|
|
79
|
-
# @param event_id [String] The event ID to edit
|
|
80
|
-
# @param new_content [Hash,String] The new message content
|
|
81
|
-
# @param msgtype [String] The message type (defaults to 'm.text')
|
|
82
|
-
# @param params [Hash] Additional parameters for send_message_event
|
|
83
|
-
# @return [Response] The response from the server
|
|
84
|
-
#
|
|
85
|
-
# @example Edit a text message
|
|
86
|
-
# api.edit_message(room_id, event_id, "Updated message content")
|
|
87
|
-
#
|
|
88
|
-
# @example Edit with formatted content
|
|
89
|
-
# api.edit_message(room_id, event_id, {
|
|
90
|
-
# body: "Updated *formatted* message",
|
|
91
|
-
# format: "org.matrix.custom.html",
|
|
92
|
-
# formatted_body: "Updated <em>formatted</em> message"
|
|
93
|
-
# })
|
|
94
|
-
def edit_message(room_id, event_id, new_content, msgtype: 'm.text', **params)
|
|
95
|
-
new_content = { body: new_content } if new_content.is_a?(String)
|
|
96
|
-
new_content[:msgtype] ||= msgtype
|
|
97
|
-
|
|
98
|
-
# Build the edit event content
|
|
99
|
-
content = {
|
|
100
|
-
body: "* #{new_content[:body]}", # Fallback with asterisk prefix
|
|
101
|
-
msgtype: msgtype,
|
|
102
|
-
'm.new_content' => new_content,
|
|
103
|
-
'm.relates_to' => {
|
|
104
|
-
rel_type: 'm.replace',
|
|
105
|
-
event_id: event_id
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
# Copy format fields to top level if present
|
|
110
|
-
if new_content[:format]
|
|
111
|
-
content[:format] = new_content[:format]
|
|
112
|
-
content[:formatted_body] = "* #{new_content[:formatted_body]}"
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
send_message_event(room_id, 'm.room.message', content, **params)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Send a reaction to an event
|
|
119
|
-
#
|
|
120
|
-
# @param room_id [String] The room ID
|
|
121
|
-
# @param event_id [String] The event ID to react to
|
|
122
|
-
# @param key [String] The reaction key (usually an emoji)
|
|
123
|
-
# @param params [Hash] Additional parameters for send_message_event
|
|
124
|
-
# @return [Response] The response from the server
|
|
125
|
-
#
|
|
126
|
-
# @example React with an emoji
|
|
127
|
-
# api.send_reaction(room_id, event_id, "👍")
|
|
128
|
-
#
|
|
129
|
-
# @example React with custom text
|
|
130
|
-
# api.send_reaction(room_id, event_id, "agree")
|
|
131
|
-
def send_reaction(room_id, event_id, key, **params)
|
|
132
|
-
content = {
|
|
133
|
-
'm.relates_to' => {
|
|
134
|
-
rel_type: 'm.annotation',
|
|
135
|
-
event_id: event_id,
|
|
136
|
-
key: key
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
send_message_event(room_id, 'm.reaction', content, **params)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Remove a reaction from an event
|
|
144
|
-
#
|
|
145
|
-
# @param room_id [String] The room ID
|
|
146
|
-
# @param reaction_event_id [String] The reaction event ID to redact
|
|
147
|
-
# @param reason [String,nil] Optional reason for removing the reaction
|
|
148
|
-
# @param params [Hash] Additional parameters for redact_event
|
|
149
|
-
# @return [Response] The response from the server
|
|
150
|
-
#
|
|
151
|
-
# @example Remove a reaction
|
|
152
|
-
# api.remove_reaction(room_id, reaction_event_id)
|
|
153
|
-
def remove_reaction(room_id, reaction_event_id, reason: nil, **params)
|
|
154
|
-
redact_event(room_id, reaction_event_id, reason: reason, **params)
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Send a reference to another event
|
|
158
|
-
#
|
|
159
|
-
# @param room_id [String] The room ID
|
|
160
|
-
# @param event_id [String] The event ID to reference
|
|
161
|
-
# @param content [Hash,String] The message content
|
|
162
|
-
# @param msgtype [String] The message type (defaults to 'm.text')
|
|
163
|
-
# @param params [Hash] Additional parameters for send_message_event
|
|
164
|
-
# @return [Response] The response from the server
|
|
165
|
-
#
|
|
166
|
-
# @example Send a message referencing another event
|
|
167
|
-
# api.send_reference(room_id, event_id, "See the above message")
|
|
168
|
-
def send_reference(room_id, event_id, content, msgtype: 'm.text', **params)
|
|
169
|
-
content = { body: content } if content.is_a?(String)
|
|
170
|
-
content[:msgtype] ||= msgtype
|
|
171
|
-
|
|
172
|
-
content[:'m.relates_to'] = {
|
|
173
|
-
rel_type: 'm.reference',
|
|
174
|
-
event_id: event_id
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
send_message_event(room_id, 'm.room.message', content, **params)
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
# Get related events for a given event
|
|
181
|
-
#
|
|
182
|
-
# @param room_id [String] The room ID
|
|
183
|
-
# @param event_id [String] The event ID to get relations for
|
|
184
|
-
# @param rel_type [String,nil] Filter by specific relationship type
|
|
185
|
-
# @param event_type [String,nil] Filter by specific event type
|
|
186
|
-
# @param from [String,nil] Pagination token
|
|
187
|
-
# @param to [String,nil] Pagination token
|
|
188
|
-
# @param limit [Integer] Maximum number of events to return
|
|
189
|
-
# @param direction [String] Direction of pagination ('b' for backwards, 'f' for forwards)
|
|
190
|
-
# @param params [Hash] Additional query parameters
|
|
191
|
-
# @return [Response] The related events
|
|
192
|
-
#
|
|
193
|
-
# @example Get all relations
|
|
194
|
-
# api.get_relations(room_id, event_id)
|
|
195
|
-
#
|
|
196
|
-
# @example Get only reactions
|
|
197
|
-
# api.get_relations(room_id, event_id, rel_type: 'm.annotation', event_type: 'm.reaction')
|
|
198
|
-
#
|
|
199
|
-
# @example Get thread replies
|
|
200
|
-
# api.get_relations(room_id, event_id, rel_type: 'm.thread')
|
|
201
|
-
def get_relations(room_id, event_id, rel_type: nil, event_type: nil,
|
|
202
|
-
from: nil, to: nil, limit: nil, direction: 'b', **params)
|
|
203
|
-
query = {
|
|
204
|
-
from: from,
|
|
205
|
-
to: to,
|
|
206
|
-
limit: limit,
|
|
207
|
-
dir: direction
|
|
208
|
-
}.merge(params).compact
|
|
209
|
-
|
|
210
|
-
# Build the appropriate endpoint based on filters
|
|
211
|
-
endpoint = "/rooms/#{room_id}/relations/#{event_id}"
|
|
212
|
-
endpoint += "/#{rel_type}" if rel_type
|
|
213
|
-
endpoint += "/#{event_type}" if rel_type && event_type
|
|
214
|
-
|
|
215
|
-
request(:get, client_api_latest, endpoint, query: query)
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# Get aggregated relations for multiple events
|
|
219
|
-
#
|
|
220
|
-
# @param room_id [String] The room ID
|
|
221
|
-
# @param event_ids [Array<String>] The event IDs to get relations for
|
|
222
|
-
# @param rel_type [String,nil] Filter by specific relationship type
|
|
223
|
-
# @param event_type [String,nil] Filter by specific event type
|
|
224
|
-
# @param params [Hash] Additional body parameters
|
|
225
|
-
# @return [Response] The aggregated relations
|
|
226
|
-
#
|
|
227
|
-
# @example Get aggregated reactions for multiple events
|
|
228
|
-
# api.get_aggregated_relations(room_id, event_ids, rel_type: 'm.annotation')
|
|
229
|
-
def get_aggregated_relations(room_id, event_ids, rel_type: nil, event_type: nil, **params)
|
|
230
|
-
body = {
|
|
231
|
-
event_ids: event_ids,
|
|
232
|
-
rel_type: rel_type,
|
|
233
|
-
event_type: event_type
|
|
234
|
-
}.merge(params).compact
|
|
235
|
-
|
|
236
|
-
request(:post, client_api_latest, "/rooms/#{room_id}/aggregations", body: body)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Get the edit history of an event
|
|
240
|
-
#
|
|
241
|
-
# @param room_id [String] The room ID
|
|
242
|
-
# @param event_id [String] The event ID to get edit history for
|
|
243
|
-
# @param params [Hash] Additional query parameters
|
|
244
|
-
# @return [Response] The edit history
|
|
245
|
-
#
|
|
246
|
-
# @example Get edit history
|
|
247
|
-
# api.get_edit_history(room_id, event_id)
|
|
248
|
-
def get_edit_history(room_id, event_id, **params)
|
|
249
|
-
get_relations(room_id, event_id, rel_type: 'm.replace', event_type: 'm.room.message', **params)
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# Get all reactions for an event
|
|
253
|
-
#
|
|
254
|
-
# @param room_id [String] The room ID
|
|
255
|
-
# @param event_id [String] The event ID to get reactions for
|
|
256
|
-
# @param params [Hash] Additional query parameters
|
|
257
|
-
# @return [Response] The reactions
|
|
258
|
-
#
|
|
259
|
-
# @example Get all reactions
|
|
260
|
-
# api.get_reactions(room_id, event_id)
|
|
261
|
-
def get_reactions(room_id, event_id, **params)
|
|
262
|
-
get_relations(room_id, event_id, rel_type: 'm.annotation', event_type: 'm.reaction', **params)
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Get thread messages for a root event
|
|
266
|
-
#
|
|
267
|
-
# @param room_id [String] The room ID
|
|
268
|
-
# @param thread_root_id [String] The thread root event ID
|
|
269
|
-
# @param params [Hash] Additional query parameters
|
|
270
|
-
# @return [Response] The thread messages
|
|
271
|
-
#
|
|
272
|
-
# @example Get thread messages
|
|
273
|
-
# api.get_thread_messages(room_id, thread_root_id)
|
|
274
|
-
def get_thread_messages(room_id, thread_root_id, **params)
|
|
275
|
-
get_relations(room_id, thread_root_id, rel_type: 'm.thread', **params)
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
# Check if an event has been edited
|
|
279
|
-
#
|
|
280
|
-
# @param event [Hash] The event to check
|
|
281
|
-
# @return [Boolean] True if the event has been edited
|
|
282
|
-
def event_edited?(event)
|
|
283
|
-
event.dig(:unsigned, :'m.relations', :'m.replace').present?
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
# Get the latest edit content for an event
|
|
287
|
-
#
|
|
288
|
-
# @param event [Hash] The event to get latest content for
|
|
289
|
-
# @return [Hash,nil] The latest content or nil if not edited
|
|
290
|
-
def get_latest_edit_content(event)
|
|
291
|
-
edit_event = event.dig(:unsigned, :'m.relations', :'m.replace')
|
|
292
|
-
return nil unless edit_event
|
|
293
|
-
|
|
294
|
-
# Return the m.new_content if available, otherwise the content
|
|
295
|
-
edit_event.dig(:content, :'m.new_content') || edit_event[:content]
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
# Check if an event is part of a thread
|
|
299
|
-
#
|
|
300
|
-
# @param event [Hash] The event to check
|
|
301
|
-
# @return [Boolean] True if the event is part of a thread
|
|
302
|
-
def in_thread?(event)
|
|
303
|
-
rel_type = event.dig(:content, :'m.relates_to', :rel_type)
|
|
304
|
-
rel_type == 'm.thread'
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
# Get the thread root ID for an event
|
|
308
|
-
#
|
|
309
|
-
# @param event [Hash] The event to get thread root for
|
|
310
|
-
# @return [String,nil] The thread root event ID or nil if not in a thread
|
|
311
|
-
def get_thread_root_id(event)
|
|
312
|
-
return nil unless in_thread?(event)
|
|
313
|
-
event.dig(:content, :'m.relates_to', :event_id)
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
|
-
end
|
|
318
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class CreateAgentMemories < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
-
def change
|
|
5
|
-
create_table :agent_memories do |t|
|
|
6
|
-
t.references :matrix_agent, null: false, foreign_key: true
|
|
7
|
-
t.string :key, null: false
|
|
8
|
-
t.jsonb :value, default: {}
|
|
9
|
-
t.datetime :expires_at
|
|
10
|
-
|
|
11
|
-
t.timestamps
|
|
12
|
-
|
|
13
|
-
t.index [:matrix_agent_id, :key], unique: true
|
|
14
|
-
t.index :expires_at
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class CreateConversationContexts < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
-
def change
|
|
5
|
-
create_table :conversation_contexts do |t|
|
|
6
|
-
t.references :matrix_agent, null: false, foreign_key: true
|
|
7
|
-
t.string :user_id, null: false
|
|
8
|
-
t.string :room_id, null: false
|
|
9
|
-
t.jsonb :context, default: {}
|
|
10
|
-
t.jsonb :message_history, default: { messages: [] }
|
|
11
|
-
t.datetime :last_message_at
|
|
12
|
-
t.integer :message_count, default: 0
|
|
13
|
-
|
|
14
|
-
t.timestamps
|
|
15
|
-
|
|
16
|
-
t.index [:matrix_agent_id, :user_id, :room_id], unique: true, name: 'idx_conv_context_unique'
|
|
17
|
-
t.index :last_message_at
|
|
18
|
-
t.index :room_id
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class CreateGlobalMemories < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
-
def change
|
|
5
|
-
create_table :global_memories do |t|
|
|
6
|
-
t.string :key, null: false
|
|
7
|
-
t.jsonb :value, default: {}
|
|
8
|
-
t.string :category
|
|
9
|
-
t.datetime :expires_at
|
|
10
|
-
t.boolean :public_read, default: true
|
|
11
|
-
t.boolean :public_write, default: false
|
|
12
|
-
|
|
13
|
-
t.timestamps
|
|
14
|
-
|
|
15
|
-
t.index :key, unique: true
|
|
16
|
-
t.index :category
|
|
17
|
-
t.index :expires_at
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
class CreateMatrixAgents < ActiveRecord::Migration<%= migration_version %>
|
|
4
|
-
def change
|
|
5
|
-
create_table :matrix_agents do |t|
|
|
6
|
-
t.string :name, null: false
|
|
7
|
-
t.string :homeserver, null: false
|
|
8
|
-
t.string :username, null: false
|
|
9
|
-
t.string :encrypted_password
|
|
10
|
-
t.string :access_token
|
|
11
|
-
t.string :state, default: 'offline', null: false
|
|
12
|
-
t.string :bot_class, null: false
|
|
13
|
-
t.jsonb :settings, default: {}
|
|
14
|
-
t.string :last_sync_token
|
|
15
|
-
t.datetime :last_active_at
|
|
16
|
-
t.integer :rooms_count, default: 0
|
|
17
|
-
t.integer :messages_handled, default: 0
|
|
18
|
-
|
|
19
|
-
t.timestamps
|
|
20
|
-
|
|
21
|
-
t.index :name, unique: true
|
|
22
|
-
t.index :state
|
|
23
|
-
t.index [:homeserver, :username]
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|