number_station 0.2.0 → 0.3.0
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.asciidoc +81 -3
- data/Rakefile +58 -4
- data/lib/number_station/GLaDOS_espeak.rb +69 -26
- data/lib/number_station/cli.rb +1441 -138
- data/lib/number_station/config_reader.rb +91 -14
- data/lib/number_station/decrypt_message.rb +75 -31
- data/lib/number_station/encrypt_message.rb +112 -41
- data/lib/number_station/examine_pads.rb +171 -0
- data/lib/number_station/make_onetime_pad.rb +58 -23
- data/lib/number_station/phonetic_conversion.rb +38 -49
- data/lib/number_station/version.rb +1 -1
- data/lib/number_station.rb +36 -6
- data/number_station.gemspec +3 -3
- data/resources/conf.yaml +415 -0
- data/resources/repeat_message.txt +1 -0
- metadata +20 -18
- data/resources/conf.json +0 -14
data/lib/number_station/cli.rb
CHANGED
|
@@ -21,193 +21,1496 @@
|
|
|
21
21
|
|
|
22
22
|
require 'thor'
|
|
23
23
|
require 'fileutils'
|
|
24
|
+
require 'date'
|
|
24
25
|
|
|
25
26
|
module NumberStation
|
|
26
|
-
class
|
|
27
|
+
class Pad < Thor
|
|
28
|
+
# Remove the built-in 'tree' command
|
|
29
|
+
def self.all_commands
|
|
30
|
+
super.reject { |k, v| k == 'tree' }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
desc "create [--name NAME --path PATH --numpads NUM --length LENGTH]", "Generate a one time pad of LENGTH containing NUM entries"
|
|
34
|
+
long_desc <<-CREATE_PAD_LONG_DESC
|
|
35
|
+
Generate a one time pad of LENGTH containing NUM entries
|
|
36
|
+
|
|
37
|
+
Parameters:
|
|
38
|
+
--name NAME - Agent name to associate with this pad. Creates subdirectory ~/number_station/pads/NAME/ if --path is not provided
|
|
39
|
+
--path PATH - Directory where the pad file will be created (defaults to current directory, or ~/number_station/pads/NAME/ if --name is provided)
|
|
40
|
+
--numpads NUM - Number of pads to generate (defaults to 500)
|
|
41
|
+
--length LENGTH - Length of each pad in characters (defaults to 500)
|
|
42
|
+
|
|
43
|
+
If no parameters are passed it will generate 500 one time pads in the current
|
|
44
|
+
directory of size 500 characters.
|
|
45
|
+
|
|
46
|
+
If --name is provided without --path, the pad will be created in ~/number_station/pads/NAME/
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
number_station pad create --name Shadow --numpads 1000 --length 1000
|
|
50
|
+
number_station pad create --name Shadow
|
|
51
|
+
number_station pad create --path ~/number_station/pads --numpads 10 --length 500
|
|
52
|
+
number_station pad create
|
|
53
|
+
CREATE_PAD_LONG_DESC
|
|
54
|
+
option :name, type: :string
|
|
55
|
+
option :length, type: :numeric
|
|
56
|
+
option :numpads, type: :numeric
|
|
57
|
+
option :path, type: :string
|
|
58
|
+
def create
|
|
59
|
+
ensure_config_loaded
|
|
60
|
+
|
|
61
|
+
# Determine the pad directory and agent name
|
|
62
|
+
pad_path = options[:path]
|
|
63
|
+
agent_name = options[:name]
|
|
64
|
+
|
|
65
|
+
if pad_path.nil? && agent_name
|
|
66
|
+
# If name is provided but no path, create agent-specific directory
|
|
67
|
+
pad_path = File.join(Dir.home, "number_station", "pads", agent_name)
|
|
68
|
+
FileUtils.mkdir_p(pad_path)
|
|
69
|
+
NumberStation.log.info "Created pad directory for agent: #{pad_path}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Pass options (make_otp will use defaults: 500 pads, 500 characters if nil)
|
|
73
|
+
# Also pass agent_name so it can be used in the filename
|
|
74
|
+
NumberStation.make_otp(pad_path, options[:length], options[:numpads], agent_name)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
desc "stats [--path PATH]", "Show statistics about one-time pads"
|
|
78
|
+
long_desc <<-PAD_STATS_LONG_DESC
|
|
79
|
+
Lists all one-time pad files in the pads directory (default: ~/number_station/pads),
|
|
80
|
+
including pads in subdirectories. Pads are grouped by agent name (subdirectory name).
|
|
81
|
+
|
|
82
|
+
For each pad file, shows:
|
|
83
|
+
- Pad filename
|
|
84
|
+
- Maximum message length (in characters) that can be encrypted
|
|
85
|
+
- Number of unconsumed pads remaining
|
|
86
|
+
|
|
87
|
+
Optional parameters:
|
|
88
|
+
--path PATH Specify a custom directory path to examine pads from
|
|
89
|
+
|
|
90
|
+
Examples:
|
|
91
|
+
number_station pad stats
|
|
92
|
+
number_station pad stats --path ~/custom/pads
|
|
93
|
+
PAD_STATS_LONG_DESC
|
|
94
|
+
option :path, type: :string
|
|
95
|
+
def stats
|
|
96
|
+
ensure_config_loaded
|
|
97
|
+
|
|
98
|
+
pads_dir = options[:path] || File.join(Dir.home, "number_station", "pads")
|
|
99
|
+
|
|
100
|
+
unless Dir.exist?(pads_dir)
|
|
101
|
+
puts "Pads directory does not exist: #{pads_dir}"
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Find all pad files recursively in various formats:
|
|
106
|
+
# - Old format: one_time_pad_XXXXX.json (random number)
|
|
107
|
+
# - New format: agentname-YYYY-MM-DD.json or one_time_pad-YYYY-MM-DD.json (date-based)
|
|
108
|
+
# - New format with counter: agentname-YYYY-MM-DD-001.json
|
|
109
|
+
pad_files = Dir.glob(File.join(pads_dir, "**", "*.json")).select do |file|
|
|
110
|
+
basename = File.basename(file)
|
|
111
|
+
# Match old format: one_time_pad_XXXXX.json
|
|
112
|
+
# Match new format: agentname-YYYY-MM-DD.json or one_time_pad-YYYY-MM-DD.json
|
|
113
|
+
# Match new format with counter: agentname-YYYY-MM-DD-001.json
|
|
114
|
+
basename.match?(/^(one_time_pad|[\w-]+)[_-]\d{4}-\d{2}-\d{2}(-\d{3})?\.json$/) ||
|
|
115
|
+
basename.match?(/^one_time_pad_\d+\.json$/) ||
|
|
116
|
+
basename.match?(/^[\w-]+_\d+\.json$/)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if pad_files.empty?
|
|
120
|
+
puts "No pad files found in #{pads_dir}"
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Group pads by their parent directory (agent name)
|
|
125
|
+
pads_by_agent = {}
|
|
126
|
+
|
|
127
|
+
pad_files.each do |file_path|
|
|
128
|
+
# Get relative path from pads_dir
|
|
129
|
+
relative_path = file_path.sub(/^#{Regexp.escape(pads_dir)}\/?/, '')
|
|
130
|
+
dir_parts = File.dirname(relative_path).split(File::SEPARATOR)
|
|
131
|
+
|
|
132
|
+
# Determine agent name: if in subdirectory, use subdirectory name; otherwise use "root"
|
|
133
|
+
agent_name = dir_parts.empty? || dir_parts == ['.'] ? "root" : dir_parts.first
|
|
134
|
+
|
|
135
|
+
pads_by_agent[agent_name] ||= []
|
|
136
|
+
pad_info = NumberStation.examine_pad_file(file_path)
|
|
137
|
+
pad_info[:file_path] = file_path
|
|
138
|
+
pad_info[:agent_name] = agent_name
|
|
139
|
+
pads_by_agent[agent_name] << pad_info
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
puts "\nOne-Time Pad Statistics"
|
|
143
|
+
puts "=" * 80
|
|
144
|
+
puts "Directory: #{pads_dir}"
|
|
145
|
+
puts "=" * 80
|
|
146
|
+
puts
|
|
147
|
+
|
|
148
|
+
# Sort agents: root first, then alphabetically
|
|
149
|
+
sorted_agents = pads_by_agent.keys.sort_by { |k| k == "root" ? "" : k }
|
|
150
|
+
|
|
151
|
+
total_pad_files = 0
|
|
152
|
+
total_unconsumed = 0
|
|
153
|
+
total_pads = 0
|
|
154
|
+
|
|
155
|
+
sorted_agents.each do |agent_name|
|
|
156
|
+
agent_pads = pads_by_agent[agent_name]
|
|
157
|
+
agent_unconsumed = agent_pads.inject(0) { |sum, info| sum + (info[:unconsumed_pads] || 0) }
|
|
158
|
+
agent_total_pads = agent_pads.inject(0) { |sum, info| sum + (info[:total_pads] || 0) }
|
|
159
|
+
|
|
160
|
+
# Display agent header
|
|
161
|
+
if agent_name == "root"
|
|
162
|
+
puts "Root Directory:"
|
|
163
|
+
else
|
|
164
|
+
puts "Agent: #{agent_name}"
|
|
165
|
+
end
|
|
166
|
+
puts "-" * 80
|
|
167
|
+
|
|
168
|
+
# Display each pad for this agent
|
|
169
|
+
agent_pads.each do |info|
|
|
170
|
+
if info[:error]
|
|
171
|
+
puts " ✗ #{info[:filename]}: ERROR - #{info[:error]}"
|
|
172
|
+
else
|
|
173
|
+
puts " Pad: #{info[:filename]}"
|
|
174
|
+
puts " ID: #{info[:pad_id]}"
|
|
175
|
+
puts " Maximum message length: #{info[:max_message_length]} characters"
|
|
176
|
+
puts " Total pads: #{info[:total_pads]}"
|
|
177
|
+
puts " Unconsumed: #{info[:unconsumed_pads]}"
|
|
178
|
+
puts " Consumed: #{info[:consumed_pads]}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
puts " Summary: #{agent_pads.size} pad file(s), #{agent_unconsumed} unconsumed pad(s) out of #{agent_total_pads} total"
|
|
183
|
+
puts
|
|
184
|
+
|
|
185
|
+
total_pad_files += agent_pads.size
|
|
186
|
+
total_unconsumed += agent_unconsumed
|
|
187
|
+
total_pads += agent_total_pads
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
puts "=" * 80
|
|
191
|
+
puts "Overall Summary: #{total_pad_files} pad file(s), #{total_unconsumed} unconsumed pad(s) out of #{total_pads} total"
|
|
192
|
+
puts "=" * 80
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private
|
|
196
|
+
|
|
197
|
+
def ensure_config_loaded
|
|
198
|
+
NumberStation::ConfigReader.read_config unless NumberStation.data
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
class Agents < Thor
|
|
203
|
+
# Remove the built-in 'tree' command
|
|
204
|
+
def self.all_commands
|
|
205
|
+
super.reject { |k, v| k == 'tree' }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Map hyphenated command names
|
|
209
|
+
map "update-handler" => :update_handler
|
|
210
|
+
map "list-all" => :list_all
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
desc "list", "List all active agents in a condensed format"
|
|
214
|
+
long_desc <<-LIST_AGENTS_LONG_DESC
|
|
215
|
+
Shows a condensed list of all active agents with their key information:
|
|
216
|
+
- Name
|
|
217
|
+
- Location
|
|
218
|
+
- Handler Codeword
|
|
219
|
+
- Start Date
|
|
220
|
+
LIST_AGENTS_LONG_DESC
|
|
221
|
+
def list
|
|
222
|
+
ensure_config_loaded
|
|
223
|
+
|
|
224
|
+
active_agents = NumberStation.active_agents
|
|
225
|
+
|
|
226
|
+
if active_agents.empty?
|
|
227
|
+
puts "No active agents found."
|
|
228
|
+
return
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
puts "\nActive Agents (#{active_agents.size})"
|
|
232
|
+
puts "=" * 100
|
|
233
|
+
|
|
234
|
+
# Print header
|
|
235
|
+
printf "%-20s %-25s %-25s %-15s\n", "Name", "Location", "Handler Codeword", "Start Date"
|
|
236
|
+
puts "-" * 100
|
|
237
|
+
|
|
238
|
+
# Print each agent
|
|
239
|
+
active_agents.each do |agent|
|
|
240
|
+
name = agent['name'] || 'N/A'
|
|
241
|
+
location = agent['location'] || '-'
|
|
242
|
+
handler = agent['handler_codeword'] || '-'
|
|
243
|
+
start_date = agent['start_date'] || '-'
|
|
244
|
+
|
|
245
|
+
printf "%-20s %-25s %-25s %-15s\n", name, location, handler, start_date
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
puts "=" * 100
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
desc "list-all", "List all active agents and inactive agents with end dates"
|
|
252
|
+
long_desc <<-LIST_ALL_AGENTS_LONG_DESC
|
|
253
|
+
Shows all active agents and all inactive agents that have an end_date in a condensed format with:
|
|
254
|
+
- Name
|
|
255
|
+
- Status (Active/Inactive)
|
|
256
|
+
- Location
|
|
257
|
+
- Handler Codeword
|
|
258
|
+
- Start Date
|
|
259
|
+
- End Date
|
|
260
|
+
|
|
261
|
+
Includes:
|
|
262
|
+
- All currently active agents (regardless of end_date)
|
|
263
|
+
- All inactive agents that have an end_date (were active and then deactivated)
|
|
264
|
+
LIST_ALL_AGENTS_LONG_DESC
|
|
265
|
+
def list_all
|
|
266
|
+
ensure_config_loaded
|
|
267
|
+
|
|
268
|
+
all_agents = NumberStation.agent_list
|
|
269
|
+
|
|
270
|
+
# Filter to: active agents OR inactive agents with end_date
|
|
271
|
+
filtered_agents = all_agents.select do |a|
|
|
272
|
+
a['active'] == true || (a['end_date'] && !a['end_date'].to_s.empty?)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
if filtered_agents.empty?
|
|
276
|
+
puts "No active agents or agents with end dates found."
|
|
277
|
+
return
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
active_count = filtered_agents.count { |a| a['active'] }
|
|
281
|
+
inactive_count = filtered_agents.size - active_count
|
|
282
|
+
|
|
283
|
+
puts "\nAll Active Agents and Agents with End Dates (#{filtered_agents.size} total: #{active_count} active, #{inactive_count} inactive)"
|
|
284
|
+
puts "=" * 120
|
|
285
|
+
|
|
286
|
+
# Print header
|
|
287
|
+
printf "%-20s %-10s %-20s %-25s %-15s %-15s\n",
|
|
288
|
+
"Name", "Status", "Location", "Handler Codeword", "Start Date", "End Date"
|
|
289
|
+
puts "-" * 120
|
|
290
|
+
|
|
291
|
+
# Print each agent
|
|
292
|
+
filtered_agents.each do |agent|
|
|
293
|
+
name = agent['name'] || 'N/A'
|
|
294
|
+
status = agent['active'] ? 'Active' : 'Inactive'
|
|
295
|
+
location = agent['location'] || '-'
|
|
296
|
+
handler = agent['handler_codeword'] || '-'
|
|
297
|
+
start_date = agent['start_date'] || '-'
|
|
298
|
+
end_date = agent['end_date'] || '-'
|
|
299
|
+
|
|
300
|
+
printf "%-20s %-10s %-20s %-25s %-15s %-15s\n",
|
|
301
|
+
name, status, location, handler, start_date, end_date
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
puts "=" * 120
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
desc "stats", "Display statistics about active agents"
|
|
308
|
+
long_desc <<-AGENT_STATS_LONG_DESC
|
|
309
|
+
Shows statistics and details about all active agents, including:
|
|
310
|
+
- Total number of active agents
|
|
311
|
+
- Agent names and details (location, handler codeword, dates, etc.)
|
|
312
|
+
- Summary statistics
|
|
313
|
+
AGENT_STATS_LONG_DESC
|
|
314
|
+
def stats
|
|
315
|
+
ensure_config_loaded
|
|
316
|
+
|
|
317
|
+
all_agents = NumberStation.agent_list
|
|
318
|
+
active_agents = NumberStation.active_agents
|
|
319
|
+
inactive_count = all_agents.size - active_agents.size
|
|
320
|
+
|
|
321
|
+
puts "\nAgent Statistics"
|
|
322
|
+
puts "=" * 80
|
|
323
|
+
puts
|
|
324
|
+
|
|
325
|
+
# Summary statistics
|
|
326
|
+
puts "Summary:"
|
|
327
|
+
puts " Total agents: #{all_agents.size}"
|
|
328
|
+
puts " Active agents: #{active_agents.size}"
|
|
329
|
+
puts " Inactive agents: #{inactive_count}"
|
|
330
|
+
puts
|
|
331
|
+
|
|
332
|
+
if active_agents.empty?
|
|
333
|
+
puts "No active agents found."
|
|
334
|
+
puts "=" * 80
|
|
335
|
+
return
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
puts "Active Agents:"
|
|
339
|
+
puts "-" * 80
|
|
340
|
+
puts
|
|
341
|
+
|
|
342
|
+
active_agents.each_with_index do |agent, index|
|
|
343
|
+
puts "#{index + 1}. #{agent['name']}"
|
|
344
|
+
puts " Location: #{agent['location'] || 'Not specified'}"
|
|
345
|
+
puts " Handler Codeword: #{agent['handler_codeword'] || 'Not specified'}"
|
|
346
|
+
puts " Start Date: #{agent['start_date'] || 'Not specified'}"
|
|
347
|
+
puts " End Date: #{agent['end_date'] || 'Not specified'}"
|
|
348
|
+
puts " Status: Active"
|
|
349
|
+
puts
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Additional statistics
|
|
353
|
+
agents_with_location = active_agents.count { |a| a['location'] && !a['location'].to_s.empty? }
|
|
354
|
+
agents_with_codeword = active_agents.count { |a| a['handler_codeword'] && !a['handler_codeword'].to_s.empty? }
|
|
355
|
+
agents_with_start_date = active_agents.count { |a| a['start_date'] && !a['start_date'].to_s.empty? }
|
|
356
|
+
agents_with_end_date = active_agents.count { |a| a['end_date'] && !a['end_date'].to_s.empty? }
|
|
357
|
+
|
|
358
|
+
puts "-" * 80
|
|
359
|
+
puts "Additional Statistics:"
|
|
360
|
+
puts " Agents with location: #{agents_with_location}/#{active_agents.size}"
|
|
361
|
+
puts " Agents with handler codeword: #{agents_with_codeword}/#{active_agents.size}"
|
|
362
|
+
puts " Agents with start date: #{agents_with_start_date}/#{active_agents.size}"
|
|
363
|
+
puts " Agents with end date: #{agents_with_end_date}/#{active_agents.size}"
|
|
364
|
+
puts "=" * 80
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
desc "create NAME", "Create a new agent"
|
|
368
|
+
long_desc <<-CREATE_AGENT_LONG_DESC
|
|
369
|
+
Create a new agent with the specified name. Optionally set location and handler codeword.
|
|
370
|
+
|
|
371
|
+
Parameters:
|
|
372
|
+
NAME - The agent name (required)
|
|
373
|
+
|
|
374
|
+
Options:
|
|
375
|
+
--location LOCATION - Set the agent's location
|
|
376
|
+
--handler HANDLER - Set the handler codeword
|
|
377
|
+
|
|
378
|
+
Examples:
|
|
379
|
+
number_station agents create Shadow --location "Berlin" --handler "NIGHTFALL"
|
|
380
|
+
number_station agents create Ghost --location "Moscow"
|
|
381
|
+
number_station agents create Phantom
|
|
382
|
+
CREATE_AGENT_LONG_DESC
|
|
383
|
+
option :location, type: :string
|
|
384
|
+
option :handler, type: :string
|
|
385
|
+
def create(name = nil)
|
|
386
|
+
if name.nil? || name.empty?
|
|
387
|
+
help("create")
|
|
388
|
+
return
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
ensure_config_loaded
|
|
392
|
+
|
|
393
|
+
# Check if agent already exists
|
|
394
|
+
if NumberStation.find_agent_by_name(name)
|
|
395
|
+
raise Thor::Error, "Agent '#{name}' already exists"
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Create new agent
|
|
399
|
+
new_agent = {
|
|
400
|
+
"name" => name,
|
|
401
|
+
"location" => options[:location],
|
|
402
|
+
"handler_codeword" => options[:handler],
|
|
403
|
+
"start_date" => nil,
|
|
404
|
+
"end_date" => nil,
|
|
405
|
+
"active" => false
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
# Add to agent list
|
|
409
|
+
config_data = NumberStation.data.dup
|
|
410
|
+
config_data["agent_list"] ||= []
|
|
411
|
+
config_data["agent_list"] << new_agent
|
|
412
|
+
|
|
413
|
+
# Save config
|
|
414
|
+
NumberStation::ConfigReader.save_config(config_data)
|
|
415
|
+
puts "Created agent: #{name}"
|
|
416
|
+
puts " Location: #{options[:location] || 'Not specified'}"
|
|
417
|
+
puts " Handler Codeword: #{options[:handler] || 'Not specified'}"
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
desc "activate NAME", "Activate an agent"
|
|
421
|
+
long_desc <<-ACTIVATE_AGENT_LONG_DESC
|
|
422
|
+
Activate an agent by name. Sets active status to true and optionally sets start_date.
|
|
423
|
+
|
|
424
|
+
Parameters:
|
|
425
|
+
NAME - The agent name (required)
|
|
426
|
+
|
|
427
|
+
Options:
|
|
428
|
+
--start-date DATE - Set the start date (defaults to today if not specified)
|
|
429
|
+
|
|
430
|
+
Examples:
|
|
431
|
+
number_station agents activate Shadow
|
|
432
|
+
number_station agents activate Shadow --start-date "2024-01-15"
|
|
433
|
+
ACTIVATE_AGENT_LONG_DESC
|
|
434
|
+
option :start_date, type: :string
|
|
435
|
+
def activate(name = nil)
|
|
436
|
+
if name.nil? || name.empty?
|
|
437
|
+
help("activate")
|
|
438
|
+
return
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
ensure_config_loaded
|
|
442
|
+
|
|
443
|
+
agent = NumberStation.find_agent_by_name(name)
|
|
444
|
+
unless agent
|
|
445
|
+
raise Thor::Error, "Agent '#{name}' not found"
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
if agent["active"]
|
|
449
|
+
puts "Agent '#{name}' is already active"
|
|
450
|
+
return
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Update agent
|
|
454
|
+
config_data = NumberStation.data.dup
|
|
455
|
+
agent_index = config_data["agent_list"].index { |a| a["name"] == name }
|
|
456
|
+
|
|
457
|
+
config_data["agent_list"][agent_index]["active"] = true
|
|
458
|
+
config_data["agent_list"][agent_index]["start_date"] = options[:start_date] || Date.today.to_s
|
|
459
|
+
config_data["agent_list"][agent_index]["end_date"] = nil # Reset end_date when reactivating
|
|
460
|
+
|
|
461
|
+
# Save config
|
|
462
|
+
NumberStation::ConfigReader.save_config(config_data)
|
|
463
|
+
puts "Activated agent: #{name}"
|
|
464
|
+
puts " Start Date: #{config_data["agent_list"][agent_index]["start_date"]}"
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
desc "deactivate NAME", "Deactivate an agent"
|
|
468
|
+
long_desc <<-DEACTIVATE_AGENT_LONG_DESC
|
|
469
|
+
Deactivate an agent by name. Sets active status to false and optionally sets end_date.
|
|
470
|
+
|
|
471
|
+
Parameters:
|
|
472
|
+
NAME - The agent name (required)
|
|
473
|
+
|
|
474
|
+
Options:
|
|
475
|
+
--end-date DATE - Set the end date (defaults to today if not specified)
|
|
476
|
+
|
|
477
|
+
Examples:
|
|
478
|
+
number_station agents deactivate Shadow
|
|
479
|
+
number_station agents deactivate Shadow --end-date "2024-12-31"
|
|
480
|
+
DEACTIVATE_AGENT_LONG_DESC
|
|
481
|
+
option :end_date, type: :string
|
|
482
|
+
def deactivate(name = nil)
|
|
483
|
+
if name.nil? || name.empty?
|
|
484
|
+
help("deactivate")
|
|
485
|
+
return
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
ensure_config_loaded
|
|
489
|
+
|
|
490
|
+
agent = NumberStation.find_agent_by_name(name)
|
|
491
|
+
unless agent
|
|
492
|
+
raise Thor::Error, "Agent '#{name}' not found"
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
unless agent["active"]
|
|
496
|
+
puts "Agent '#{name}' is already inactive"
|
|
497
|
+
return
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
# Update agent
|
|
501
|
+
config_data = NumberStation.data.dup
|
|
502
|
+
agent_index = config_data["agent_list"].index { |a| a["name"] == name }
|
|
503
|
+
|
|
504
|
+
config_data["agent_list"][agent_index]["active"] = false
|
|
505
|
+
config_data["agent_list"][agent_index]["end_date"] = options[:end_date] || Date.today.to_s
|
|
506
|
+
|
|
507
|
+
# Save config
|
|
508
|
+
NumberStation::ConfigReader.save_config(config_data)
|
|
509
|
+
puts "Deactivated agent: #{name}"
|
|
510
|
+
puts " End Date: #{config_data["agent_list"][agent_index]["end_date"]}"
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
desc "update_handler NAME HANDLER", "Update the handler codeword for an agent"
|
|
514
|
+
long_desc <<-UPDATE_HANDLER_LONG_DESC
|
|
515
|
+
Update the handler codeword for an agent.
|
|
516
|
+
|
|
517
|
+
Parameters:
|
|
518
|
+
NAME - The agent name (required)
|
|
519
|
+
HANDLER - The new handler codeword (required)
|
|
520
|
+
|
|
521
|
+
Examples:
|
|
522
|
+
number_station agents update-handler Shadow DAWNBREAK
|
|
523
|
+
number_station agents update-handler Ghost NEWCODE123
|
|
524
|
+
UPDATE_HANDLER_LONG_DESC
|
|
525
|
+
def update_handler(name = nil, handler = nil)
|
|
526
|
+
if name.nil? || name.empty? || handler.nil? || handler.empty?
|
|
527
|
+
help("update_handler")
|
|
528
|
+
return
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
ensure_config_loaded
|
|
532
|
+
|
|
533
|
+
agent = NumberStation.find_agent_by_name(name)
|
|
534
|
+
unless agent
|
|
535
|
+
raise Thor::Error, "Agent '#{name}' not found"
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Update agent
|
|
539
|
+
config_data = NumberStation.data.dup
|
|
540
|
+
agent_index = config_data["agent_list"].index { |a| a["name"] == name }
|
|
541
|
+
|
|
542
|
+
old_handler = config_data["agent_list"][agent_index]["handler_codeword"]
|
|
543
|
+
config_data["agent_list"][agent_index]["handler_codeword"] = handler
|
|
544
|
+
|
|
545
|
+
# Save config
|
|
546
|
+
NumberStation::ConfigReader.save_config(config_data)
|
|
547
|
+
puts "Updated handler codeword for agent: #{name}"
|
|
548
|
+
puts " Old handler: #{old_handler || 'Not specified'}"
|
|
549
|
+
puts " New handler: #{handler}"
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
private
|
|
27
553
|
|
|
554
|
+
def ensure_config_loaded
|
|
555
|
+
NumberStation::ConfigReader.read_config unless NumberStation.data
|
|
556
|
+
end
|
|
557
|
+
end
|
|
28
558
|
|
|
29
|
-
|
|
30
|
-
desc "create_config [--path PATH]", "
|
|
559
|
+
class CLI < Thor
|
|
560
|
+
desc "create_config [--path PATH]", "create the config file conf.yaml"
|
|
31
561
|
long_desc <<-CREATE_CONFIG_LONG_DESC
|
|
32
|
-
`create_config` will copy the sample config from
|
|
33
|
-
optional parameter `--path PATH` is passed then
|
|
34
|
-
PATH parameter.
|
|
35
|
-
Sample Intro and Outro messages are also copied to this directory. If you wish to automatically append
|
|
36
|
-
these messages, change the conf.json boolean.
|
|
562
|
+
`create_config` will copy the sample config from resources/conf.yaml to ~/number_station/conf.yaml.
|
|
563
|
+
If the optional parameter `--path PATH` is passed then the config file is created at PATH/conf.yaml.
|
|
37
564
|
CREATE_CONFIG_LONG_DESC
|
|
38
|
-
option :path, :
|
|
39
|
-
def create_config
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
565
|
+
option :path, type: :string
|
|
566
|
+
def create_config
|
|
567
|
+
ensure_config_loaded
|
|
568
|
+
|
|
569
|
+
target_path = options[:path] || File.join(Dir.home, "number_station")
|
|
570
|
+
FileUtils.mkdir_p(target_path)
|
|
571
|
+
|
|
572
|
+
target_file = File.join(target_path, "conf.yaml")
|
|
573
|
+
|
|
574
|
+
# Find the source config file - try multiple locations
|
|
575
|
+
source_file = find_config_template
|
|
576
|
+
|
|
577
|
+
unless File.exist?(target_file)
|
|
578
|
+
if source_file && File.exist?(source_file)
|
|
579
|
+
FileUtils.cp(source_file, target_file)
|
|
580
|
+
NumberStation.log.info "Created config file: #{target_file}"
|
|
581
|
+
else
|
|
582
|
+
# If template not found, create a default config
|
|
583
|
+
create_default_config(target_file)
|
|
584
|
+
NumberStation.log.info "Created default config file: #{target_file}"
|
|
585
|
+
end
|
|
586
|
+
else
|
|
587
|
+
NumberStation.log.info "Config file already exists: #{target_file}"
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Copy message template files (intro, outro, repeat) to target directory
|
|
591
|
+
copy_message_template_files(target_path)
|
|
592
|
+
|
|
593
|
+
NumberStation::ConfigReader.read_config
|
|
51
594
|
NumberStation.log.debug "create_config completed"
|
|
52
595
|
end
|
|
53
596
|
|
|
54
597
|
|
|
55
|
-
# convert_to_phonetic
|
|
56
598
|
desc "convert_to_phonetic [MESSAGE_PATH]", "Convert a message to phonetic output."
|
|
57
599
|
long_desc <<-CONVERT_MESSAGE_LONG_DESC
|
|
58
600
|
convert_message takes a parameter which should point to a text file containing a message.
|
|
59
601
|
MESSAGE_PATH\n
|
|
60
602
|
Optional parameters:\n
|
|
61
|
-
--intro [INTRO_PATH] should be a text file containing intro message. Overrides value in conf.
|
|
62
|
-
--outro [OUTRO_PATH] should be a text file containing the outro message. Overrides value in conf.
|
|
603
|
+
--intro [INTRO_PATH] should be a text file containing intro message. Overrides value in conf.yaml\n
|
|
604
|
+
--outro [OUTRO_PATH] should be a text file containing the outro message. Overrides value in conf.yaml\n
|
|
605
|
+
--repeat [REPEAT_PATH] should be a text file containing repeat message. Included after first message.\n
|
|
63
606
|
--mp3 [MP3] output message as an mp3 file.
|
|
64
607
|
|
|
65
|
-
Final message will be created from intro + message + outro
|
|
608
|
+
Final message will be created from: intro (as-is) + message (phonetic) + repeat (as-is) + message (phonetic again) + outro (as-is)
|
|
66
609
|
CONVERT_MESSAGE_LONG_DESC
|
|
67
|
-
option :intro, :
|
|
68
|
-
option :outro, :
|
|
69
|
-
option :
|
|
610
|
+
option :intro, type: :string
|
|
611
|
+
option :outro, type: :string
|
|
612
|
+
option :repeat, type: :string
|
|
613
|
+
option :mp3, type: :string
|
|
70
614
|
def convert_to_phonetic(message_path)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if options[:intro]
|
|
74
|
-
intro_path = options[:intro]
|
|
75
|
-
intro = NumberStation.to_phonetic(intro_path)
|
|
76
|
-
elsif NumberStation.data["resources"]["intro"]["enabled"]
|
|
77
|
-
intro_path = NumberStation.data["resources"]["intro"]["template"]
|
|
78
|
-
intro = NumberStation.to_phonetic(intro_path)
|
|
79
|
-
else
|
|
80
|
-
intro_path = ""
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
if options[:outro]
|
|
84
|
-
outro_path = options[:outro]
|
|
85
|
-
outro = NumberStation.to_phonetic(outro_path)
|
|
86
|
-
elsif NumberStation.data["resources"]["outro"]["enabled"]
|
|
87
|
-
outro_path = NumberStation.data["resources"]["outro"]["template"]
|
|
88
|
-
outro = NumberStation.to_phonetic(outro_path)
|
|
89
|
-
else
|
|
90
|
-
outro_path = ""
|
|
91
|
-
outro = ""
|
|
92
|
-
end
|
|
93
|
-
NumberStation.log.debug "intro enabled: #{NumberStation.data["resources"]["intro"]["enabled"]} path: #{intro_path}"
|
|
94
|
-
NumberStation.log.debug "message_path: #{message_path}"
|
|
95
|
-
NumberStation.log.debug "outro enabled: #{NumberStation.data["resources"]["outro"]["enabled"]} path: #{outro_path}"
|
|
615
|
+
ensure_config_loaded
|
|
96
616
|
|
|
617
|
+
# Load intro, outro, and repeat as-is (not converted to phonetic)
|
|
618
|
+
intro = load_message_component(:intro, options[:intro])
|
|
619
|
+
outro = load_message_component(:outro, options[:outro])
|
|
620
|
+
repeat = load_message_component(:repeat, options[:repeat])
|
|
621
|
+
|
|
622
|
+
# Convert message to phonetic
|
|
97
623
|
message = NumberStation.to_phonetic(message_path)
|
|
98
|
-
|
|
624
|
+
|
|
625
|
+
# Build output: intro (as-is) + message (phonetic) + repeat (as-is) + message (phonetic again) + outro (as-is)
|
|
626
|
+
output_parts = []
|
|
627
|
+
output_parts << intro.strip if intro && !intro.strip.empty?
|
|
628
|
+
output_parts << message.strip if message && !message.strip.empty?
|
|
629
|
+
output_parts << repeat.strip if repeat && !repeat.strip.empty?
|
|
630
|
+
output_parts << message.strip if message && !message.strip.empty? # Repeat the message
|
|
631
|
+
output_parts << outro.strip if outro && !outro.strip.empty?
|
|
632
|
+
|
|
633
|
+
# Join with newlines between components for readability
|
|
634
|
+
# Strip each part to remove trailing newlines that might cause extra blank lines
|
|
635
|
+
output = output_parts.join("\n")
|
|
99
636
|
NumberStation.log.info "output: #{output}"
|
|
100
637
|
|
|
638
|
+
# Generate output filename based on input filename
|
|
639
|
+
input_basename = File.basename(message_path, File.extname(message_path))
|
|
640
|
+
output_filename = "#{input_basename}_phonetic.txt"
|
|
641
|
+
output_path = File.join(File.dirname(message_path), output_filename)
|
|
642
|
+
|
|
643
|
+
# Save output to file
|
|
644
|
+
File.write(output_path, output)
|
|
645
|
+
NumberStation.log.info "Saved phonetic output to: #{output_path}"
|
|
646
|
+
|
|
101
647
|
if options[:mp3]
|
|
102
|
-
|
|
103
|
-
NumberStation.
|
|
104
|
-
NumberStation.log.debug "Generating mp3 output: #{mp3_path}"
|
|
105
|
-
NumberStation.write_mp3(output, mp3_path)
|
|
648
|
+
NumberStation.log.debug "Generating mp3 output: #{options[:mp3]}"
|
|
649
|
+
NumberStation.write_mp3(output, options[:mp3])
|
|
106
650
|
end
|
|
107
|
-
|
|
651
|
+
|
|
652
|
+
output
|
|
108
653
|
end
|
|
109
654
|
|
|
655
|
+
desc "convert_to_espeak FILE", "Convert a phonetic message file to GLaDOS-style espeak XML"
|
|
656
|
+
long_desc <<-CONVERT_TO_ESPEAK_LONG_DESC
|
|
657
|
+
Convert a phonetic message file (typically generated by convert_to_phonetic) to GLaDOS-style espeak XML format.
|
|
658
|
+
|
|
659
|
+
Parameters:
|
|
660
|
+
FILE - Path to phonetic message file (e.g., Abyss_Abyss-2026-01-12-001_pad2_encrypted_phonetic.txt)
|
|
661
|
+
|
|
662
|
+
The output XML file will be saved with the same name as the input file, but with .xml extension.
|
|
663
|
+
|
|
664
|
+
Example:
|
|
665
|
+
number_station convert_to_espeak Abyss_Abyss-2026-01-12-001_pad2_encrypted_phonetic.txt
|
|
666
|
+
# Creates: Abyss_Abyss-2026-01-12-001_pad2_encrypted_phonetic.xml
|
|
667
|
+
CONVERT_TO_ESPEAK_LONG_DESC
|
|
668
|
+
def convert_to_espeak(file)
|
|
669
|
+
ensure_config_loaded
|
|
670
|
+
|
|
671
|
+
unless File.exist?(file)
|
|
672
|
+
raise Thor::Error, "File not found: #{file}"
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
output_path = NumberStation.generate_glados_espeak(file)
|
|
676
|
+
puts "Generated GLaDOS espeak XML: #{output_path}"
|
|
677
|
+
end
|
|
110
678
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Parameters
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
NumberStation.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
Parameters:\n
|
|
146
|
-
MESSAGE
|
|
147
|
-
--numpad NUMPAD\n
|
|
148
|
-
--padpath PADPATH
|
|
679
|
+
desc "espeak XML_FILE", "Use espeak to read an XML file"
|
|
680
|
+
long_desc <<-ESPEAK_LONG_DESC
|
|
681
|
+
Use the espeak utility to read an XML file with GLaDOS-style voice settings.
|
|
682
|
+
|
|
683
|
+
Parameters:
|
|
684
|
+
XML_FILE - Path to XML file (typically generated by convert_to_espeak)
|
|
685
|
+
|
|
686
|
+
The command checks if espeak is available on the system before executing.
|
|
687
|
+
Uses GLaDOS voice settings: -ven+f3 -m -p 60 -s 100 -g 4
|
|
688
|
+
|
|
689
|
+
Example:
|
|
690
|
+
number_station espeak Abyss_Abyss-2026-01-12-001_pad2_encrypted_phonetic.xml
|
|
691
|
+
ESPEAK_LONG_DESC
|
|
692
|
+
def espeak(xml_file)
|
|
693
|
+
ensure_config_loaded
|
|
694
|
+
|
|
695
|
+
# Check if espeak is available
|
|
696
|
+
unless NumberStation.command?('espeak')
|
|
697
|
+
raise Thor::Error, "espeak utility is not available on this system. Please install espeak to use this command."
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
unless File.exist?(xml_file)
|
|
701
|
+
raise Thor::Error, "File not found: #{xml_file}"
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# Call espeak with GLaDOS voice settings
|
|
705
|
+
cmd = "espeak -ven+f3 -m -p 60 -s 100 -g 4 -f #{xml_file}"
|
|
706
|
+
NumberStation.log.info "Running espeak: #{cmd}"
|
|
707
|
+
system(cmd)
|
|
708
|
+
|
|
709
|
+
unless $?.success?
|
|
710
|
+
raise Thor::Error, "espeak command failed with exit code #{$?.exitstatus}"
|
|
711
|
+
end
|
|
712
|
+
end
|
|
149
713
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
714
|
+
desc "convert_to_mp3 XML_FILE", "Convert an XML file to MP3 using espeak and ffmpeg"
|
|
715
|
+
long_desc <<-CONVERT_TO_MP3_LONG_DESC
|
|
716
|
+
Convert an XML file (typically generated by convert_to_espeak) to MP3 audio format.
|
|
717
|
+
|
|
718
|
+
Parameters:
|
|
719
|
+
XML_FILE - Path to XML file (e.g., Abyss_Abyss-2026-01-12-001_pad2_encrypted_phonetic.xml)
|
|
720
|
+
|
|
721
|
+
The command checks if both espeak and ffmpeg utilities are available before executing.
|
|
722
|
+
The output MP3 file will be saved with the same name as the input file, but with .mp3 extension.
|
|
723
|
+
|
|
724
|
+
Example:
|
|
725
|
+
number_station convert_to_mp3 Abyss_Abyss-2026-01-12-001_pad2_encrypted_phonetic.xml
|
|
726
|
+
# Creates: Abyss_Abyss-2026-01-12-001_pad2_encrypted_phonetic.mp3
|
|
727
|
+
CONVERT_TO_MP3_LONG_DESC
|
|
728
|
+
def convert_to_mp3(xml_file)
|
|
729
|
+
ensure_config_loaded
|
|
730
|
+
|
|
731
|
+
# Check if espeak and ffmpeg are available
|
|
732
|
+
unless NumberStation.command?('espeak')
|
|
733
|
+
raise Thor::Error, "espeak utility is not available on this system. Please install espeak to use this command."
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
unless NumberStation.command?('ffmpeg')
|
|
737
|
+
raise Thor::Error, "ffmpeg utility is not available on this system. Please install ffmpeg to use this command."
|
|
738
|
+
end
|
|
739
|
+
|
|
740
|
+
unless File.exist?(xml_file)
|
|
741
|
+
raise Thor::Error, "File not found: #{xml_file}"
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
# Generate output filename: replace .xml extension with .mp3
|
|
745
|
+
input_basename = File.basename(xml_file, File.extname(xml_file))
|
|
746
|
+
output_filename = "#{input_basename}.mp3"
|
|
747
|
+
output_path = File.join(File.dirname(xml_file), output_filename)
|
|
748
|
+
|
|
749
|
+
# Call espeak piped to ffmpeg
|
|
750
|
+
cmd = "espeak -ven+f3 -m -p 60 -s 100 -g 4 -f #{xml_file} --stdout | ffmpeg -i - -ar 44100 -ac 2 -ab 192k -f mp3 #{output_path}"
|
|
751
|
+
NumberStation.log.info "Running: #{cmd}"
|
|
752
|
+
system(cmd)
|
|
753
|
+
|
|
754
|
+
unless $?.success?
|
|
755
|
+
raise Thor::Error, "convert_to_mp3 command failed with exit code #{$?.exitstatus}"
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
puts "Generated MP3 file: #{output_path}"
|
|
759
|
+
end
|
|
156
760
|
|
|
157
|
-
message_data = File.read(message)
|
|
158
|
-
numpad = options[:numpad]
|
|
159
|
-
padpath = options[:padpath]
|
|
160
761
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
762
|
+
desc "encrypt [MESSAGE --file FILE --agent AGENT --numpad NUMPAD --padpath PADPATH]", "Encrypt a message using the key: NUMPAD in one time pad PADPATH"
|
|
763
|
+
long_desc <<-ENCRYPT_MESSAGE_LONG_DESC
|
|
764
|
+
Encrypt a message using key NUMPAD in one-time-pad PADPATH
|
|
765
|
+
|
|
766
|
+
Parameters:
|
|
767
|
+
MESSAGE - Message string to encrypt (if --file is not provided)
|
|
768
|
+
--file FILE - Path to message file (alternative to passing message as argument)
|
|
769
|
+
--agent AGENT - Agent name. If provided, searches for oldest pad in ~/number_station/pads/AGENT/
|
|
770
|
+
--numpad NUMPAD - Pad number (optional, will try to auto-detect if not provided)
|
|
771
|
+
--padpath PADPATH - Path to pad file (optional, will try to auto-detect if not provided)
|
|
164
772
|
|
|
165
|
-
|
|
166
|
-
|
|
773
|
+
If --agent is provided, the system will search for the oldest pad with unconsumed pads
|
|
774
|
+
in the agent-specific directory (~/number_station/pads/AGENT/).
|
|
775
|
+
|
|
776
|
+
If --agent is not provided and --padpath/--numpad are not specified, the system will
|
|
777
|
+
search for the oldest pad with unconsumed pads in ~/number_station/pads/.
|
|
778
|
+
|
|
779
|
+
Examples:
|
|
780
|
+
number_station encrypt "Hello World" --agent Shadow
|
|
781
|
+
number_station encrypt --file message.txt --agent Shadow
|
|
782
|
+
number_station encrypt "Hello World" --agent Shadow --numpad 5
|
|
783
|
+
number_station encrypt --file message.txt --padpath ~/number_station/pads/Shadow/Shadow-2024-01-15.json --numpad 10
|
|
784
|
+
ENCRYPT_MESSAGE_LONG_DESC
|
|
785
|
+
option :file, type: :string
|
|
786
|
+
option :agent, type: :string
|
|
787
|
+
option :numpad, type: :string
|
|
788
|
+
option :padpath, type: :string
|
|
789
|
+
def encrypt(message = nil)
|
|
790
|
+
# Handle help request
|
|
791
|
+
if message == "help" || (message.nil? && options[:file].nil? && options[:agent].nil?)
|
|
792
|
+
help("encrypt")
|
|
793
|
+
return
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
ensure_config_loaded
|
|
797
|
+
|
|
798
|
+
# Validate agent if provided
|
|
799
|
+
if options[:agent]
|
|
800
|
+
agent = NumberStation.find_agent_by_name(options[:agent])
|
|
801
|
+
unless agent
|
|
802
|
+
raise Thor::Error, "Agent '#{options[:agent]}' not found"
|
|
803
|
+
end
|
|
804
|
+
unless agent["active"] == true
|
|
805
|
+
raise Thor::Error, "Agent '#{options[:agent]}' is inactive. Messages can only be encrypted for active agents."
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Determine message content: from --file option or from argument
|
|
810
|
+
if options[:file]
|
|
811
|
+
unless File.exist?(options[:file])
|
|
812
|
+
raise Thor::Error, "File not found: #{options[:file]}"
|
|
813
|
+
end
|
|
814
|
+
message_data = File.read(options[:file])
|
|
815
|
+
message_file_path = options[:file] # Store for filename generation
|
|
816
|
+
elsif message && !message.empty?
|
|
817
|
+
message_data = message
|
|
818
|
+
# If agent is specified, we'll save to file (even though input is string)
|
|
819
|
+
# Pass a flag to indicate we should save to file
|
|
820
|
+
message_file_path = options[:agent] ? "string_input" : nil
|
|
821
|
+
else
|
|
822
|
+
raise Thor::Error, "Either provide a message string as argument or use --file option"
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Auto-detect pad if not provided
|
|
826
|
+
if options[:padpath].nil? || options[:numpad].nil?
|
|
827
|
+
agent_name = options[:agent]
|
|
828
|
+
pad_info = NumberStation.find_next_available_pad(nil, message_data.size, true, agent_name)
|
|
829
|
+
pad_path = options[:padpath] || pad_info[:pad_path]
|
|
830
|
+
pad_num = options[:numpad] || pad_info[:pad_num]
|
|
831
|
+
|
|
832
|
+
agent_info = agent_name ? " for agent '#{agent_name}'" : ""
|
|
833
|
+
NumberStation.log.info "Using pad#{agent_info}: #{File.basename(pad_path)}, pad number: #{pad_num}"
|
|
834
|
+
else
|
|
835
|
+
pad_path = options[:padpath]
|
|
836
|
+
pad_num = options[:numpad]
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
encrypted = NumberStation.encrypt_message(message_data, pad_path, pad_num, message_file_path)
|
|
840
|
+
NumberStation.log.debug "encrypted_message: #{encrypted}"
|
|
841
|
+
|
|
842
|
+
# Output encrypted message to stdout if encrypting from string without agent
|
|
843
|
+
# (If agent is specified, we save to file instead)
|
|
844
|
+
unless message_file_path
|
|
845
|
+
puts encrypted
|
|
846
|
+
end
|
|
167
847
|
end
|
|
168
848
|
|
|
169
849
|
|
|
170
|
-
|
|
171
|
-
desc "decrypt_message [MESSAGE --numpad NUMPAD --padpath PADPATH]", "Decrypt a message using the key: NUMPAD in one time pad PADPATH"
|
|
850
|
+
desc "decrypt [MESSAGE --file FILE --numpad NUMPAD --padpath PADPATH]", "Decrypt a message using the key: NUMPAD in one time pad PADPATH"
|
|
172
851
|
long_desc <<-DECRYPT_MESSAGE_LONG_DESC
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
--
|
|
177
|
-
--
|
|
852
|
+
Decrypt a message using key NUMPAD in one-time-pad PADPATH
|
|
853
|
+
|
|
854
|
+
Parameters:
|
|
855
|
+
MESSAGE - Encrypted message string to decrypt (if --file is not provided)
|
|
856
|
+
--file FILE - Path to encrypted message file (alternative to passing message as argument)
|
|
857
|
+
--numpad NUMPAD (optional, will try to auto-detect if not provided)
|
|
858
|
+
--padpath PADPATH (optional, will try to auto-detect if not provided)
|
|
178
859
|
|
|
860
|
+
If --numpad or --padpath are not specified, the system will attempt to find a matching pad.
|
|
861
|
+
Note: Auto-detection may be slower as it tries multiple pads.
|
|
862
|
+
|
|
863
|
+
Examples:
|
|
864
|
+
number_station decrypt "a1b2c3d4e5" --padpath ~/number_station/pads/Shadow/Shadow-2024-01-15.json --numpad 0
|
|
865
|
+
number_station decrypt --file encrypted.txt --padpath ~/number_station/pads/Shadow/Shadow-2024-01-15.json --numpad 0
|
|
866
|
+
number_station decrypt --file encrypted.txt
|
|
179
867
|
DECRYPT_MESSAGE_LONG_DESC
|
|
180
|
-
option :
|
|
181
|
-
option :
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
868
|
+
option :file, type: :string
|
|
869
|
+
option :numpad, type: :string
|
|
870
|
+
option :padpath, type: :string
|
|
871
|
+
def decrypt(message = nil)
|
|
872
|
+
# Handle help request
|
|
873
|
+
if message == "help" || (message.nil? && options[:file].nil?)
|
|
874
|
+
help("decrypt")
|
|
875
|
+
return
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
ensure_config_loaded
|
|
879
|
+
|
|
880
|
+
# Determine message content: from --file option or from argument
|
|
881
|
+
if options[:file]
|
|
882
|
+
unless File.exist?(options[:file])
|
|
883
|
+
raise Thor::Error, "File not found: #{options[:file]}"
|
|
884
|
+
end
|
|
885
|
+
message_data = File.read(options[:file])
|
|
886
|
+
message_file_path = options[:file] # Store for filename generation
|
|
887
|
+
elsif message && !message.empty?
|
|
888
|
+
message_data = message
|
|
889
|
+
message_file_path = nil
|
|
890
|
+
else
|
|
891
|
+
raise Thor::Error, "Either provide an encrypted message string as argument or use --file option"
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# Auto-detect pad if not provided
|
|
895
|
+
if options[:padpath].nil? || options[:numpad].nil?
|
|
896
|
+
# For decryption, we don't require unconsumed pads (can decrypt with consumed pads)
|
|
897
|
+
# Get cleaned message length for size check
|
|
898
|
+
cleaned_message = message_data.gsub(/[\s\n\r]/, '')
|
|
899
|
+
min_length = cleaned_message.length
|
|
900
|
+
|
|
901
|
+
begin
|
|
902
|
+
pad_info = NumberStation.find_next_available_pad(nil, min_length, false)
|
|
903
|
+
pad_path = options[:padpath] || pad_info[:pad_path]
|
|
904
|
+
pad_num = options[:numpad] || pad_info[:pad_num]
|
|
905
|
+
|
|
906
|
+
NumberStation.log.info "Attempting decryption with pad: #{File.basename(pad_path)}, pad number: #{pad_num}"
|
|
907
|
+
decrypted = NumberStation.decrypt_message(message_data, pad_path, pad_num, message_file_path)
|
|
908
|
+
NumberStation.log.debug "decrypted_message: #{decrypted}"
|
|
909
|
+
|
|
910
|
+
# Output decrypted message to stdout if decrypting from string
|
|
911
|
+
unless message_file_path
|
|
912
|
+
puts decrypted
|
|
913
|
+
end
|
|
914
|
+
rescue ArgumentError, StandardError => e
|
|
915
|
+
NumberStation.log.error "Failed to decrypt with auto-detected pad: #{e.message}"
|
|
916
|
+
NumberStation.log.error "Please specify --padpath and --numpad explicitly for accurate decryption"
|
|
917
|
+
raise
|
|
918
|
+
end
|
|
919
|
+
else
|
|
920
|
+
pad_path = options[:padpath]
|
|
921
|
+
pad_num = options[:numpad]
|
|
922
|
+
decrypted = NumberStation.decrypt_message(message_data, pad_path, pad_num, message_file_path)
|
|
923
|
+
NumberStation.log.debug "decrypted_message: #{decrypted}"
|
|
924
|
+
|
|
925
|
+
# Output decrypted message to stdout if decrypting from string
|
|
926
|
+
unless message_file_path
|
|
927
|
+
puts decrypted
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
end
|
|
189
931
|
|
|
190
|
-
NumberStation.log.debug "message: #{message}"
|
|
191
|
-
NumberStation.log.debug "numpad: #{numpad}" if options[:numpad]
|
|
192
|
-
NumberStation.log.debug "padpath: #{padpath}" if options[:padpath]
|
|
193
932
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
933
|
+
desc "agents SUBCOMMAND", "Manage and view agent information"
|
|
934
|
+
long_desc <<-AGENTS_LONG_DESC
|
|
935
|
+
Manage agents in your number station configuration.
|
|
936
|
+
|
|
937
|
+
Available subcommands:
|
|
938
|
+
create NAME [--location LOCATION] [--handler HANDLER]
|
|
939
|
+
Create a new agent
|
|
940
|
+
|
|
941
|
+
activate NAME [--start-date DATE]
|
|
942
|
+
Activate an agent
|
|
943
|
+
|
|
944
|
+
deactivate NAME [--end-date DATE]
|
|
945
|
+
Deactivate an agent
|
|
946
|
+
|
|
947
|
+
update-handler NAME HANDLER
|
|
948
|
+
Update handler codeword for an agent
|
|
949
|
+
|
|
950
|
+
list
|
|
951
|
+
Show condensed list of active agents
|
|
952
|
+
|
|
953
|
+
list-all
|
|
954
|
+
Show all active agents and inactive agents with end dates
|
|
955
|
+
|
|
956
|
+
stats
|
|
957
|
+
Show detailed statistics about active agents
|
|
958
|
+
|
|
959
|
+
Examples:
|
|
960
|
+
number_station agents create Shadow --location "Berlin" --handler "NIGHTFALL"
|
|
961
|
+
number_station agents activate Shadow
|
|
962
|
+
number_station agents list
|
|
963
|
+
number_station agents stats
|
|
964
|
+
|
|
965
|
+
For help on a specific subcommand:
|
|
966
|
+
number_station agents help SUBCOMMAND
|
|
967
|
+
AGENTS_LONG_DESC
|
|
968
|
+
subcommand "agents", Agents
|
|
197
969
|
|
|
970
|
+
desc "pad SUBCOMMAND", "Manage one-time pads"
|
|
971
|
+
long_desc <<-PAD_LONG_DESC
|
|
972
|
+
Manage one-time pads for encryption/decryption.
|
|
973
|
+
|
|
974
|
+
Available subcommands:
|
|
975
|
+
create [--name NAME --path PATH --numpads NUM --length LENGTH]
|
|
976
|
+
Generate a new one-time pad file
|
|
977
|
+
Use --name to create agent-specific pad directories
|
|
978
|
+
|
|
979
|
+
stats [--path PATH]
|
|
980
|
+
Show statistics about one-time pads in a directory
|
|
981
|
+
|
|
982
|
+
Examples:
|
|
983
|
+
number_station pad create --name Shadow --numpads 1000 --length 1000
|
|
984
|
+
number_station pad create --name Shadow
|
|
985
|
+
number_station pad create --path ~/number_station/pads --numpads 10 --length 500
|
|
986
|
+
number_station pad create
|
|
987
|
+
number_station pad stats
|
|
988
|
+
number_station pad stats --path ~/custom/pads
|
|
989
|
+
|
|
990
|
+
For help on a specific subcommand:
|
|
991
|
+
number_station pad help SUBCOMMAND
|
|
992
|
+
PAD_LONG_DESC
|
|
993
|
+
subcommand "pad", Pad
|
|
198
994
|
|
|
199
|
-
# version
|
|
200
995
|
desc "version", "Print the version of the Number Stations gem."
|
|
201
996
|
long_desc <<-VERSION_LONG_DESC
|
|
202
997
|
Prints the version of the Number Stations gem.
|
|
203
998
|
VERSION_LONG_DESC
|
|
204
|
-
def version
|
|
205
|
-
|
|
206
|
-
|
|
999
|
+
def version
|
|
1000
|
+
ensure_config_loaded
|
|
1001
|
+
puts NumberStation::VERSION
|
|
207
1002
|
end
|
|
208
1003
|
|
|
209
|
-
def self.exit_on_failure?
|
|
1004
|
+
def self.exit_on_failure?
|
|
210
1005
|
false
|
|
211
1006
|
end
|
|
1007
|
+
|
|
1008
|
+
# Remove the built-in 'tree' command
|
|
1009
|
+
def self.all_commands
|
|
1010
|
+
super.reject { |k, v| k == 'tree' }
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
private
|
|
1014
|
+
|
|
1015
|
+
def ensure_config_loaded
|
|
1016
|
+
NumberStation::ConfigReader.read_config unless NumberStation.data
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def load_message_component(type, override_path)
|
|
1020
|
+
resource_key = type.to_s
|
|
1021
|
+
config = NumberStation.data["resources"][resource_key]
|
|
1022
|
+
|
|
1023
|
+
content = if override_path
|
|
1024
|
+
# For intro, outro, and repeat, read as-is (not converted to phonetic)
|
|
1025
|
+
File.read(override_path)
|
|
1026
|
+
elsif config && config["enabled"]
|
|
1027
|
+
# For intro, outro, and repeat, read as-is (not converted to phonetic)
|
|
1028
|
+
File.read(config["template"])
|
|
1029
|
+
else
|
|
1030
|
+
""
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
# Strip trailing newlines to avoid extra blank lines when joining
|
|
1034
|
+
content.chomp
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
def find_config_template
|
|
1038
|
+
# Try multiple possible locations
|
|
1039
|
+
possible_paths = [
|
|
1040
|
+
File.expand_path(File.join(File.dirname(__FILE__), "../../resources/conf.yaml")),
|
|
1041
|
+
File.expand_path(File.join(File.dirname(__FILE__), "../../../resources/conf.yaml"))
|
|
1042
|
+
]
|
|
1043
|
+
|
|
1044
|
+
# Try gem datadir if available
|
|
1045
|
+
begin
|
|
1046
|
+
gem_path = File.join(Gem.datadir('number_station'), 'conf.yaml')
|
|
1047
|
+
possible_paths << gem_path if gem_path
|
|
1048
|
+
rescue
|
|
1049
|
+
# Gem.datadir not available, skip
|
|
1050
|
+
end
|
|
1051
|
+
|
|
1052
|
+
possible_paths.find { |path| path && File.exist?(path) }
|
|
1053
|
+
end
|
|
1054
|
+
|
|
1055
|
+
def find_resource_file(filename)
|
|
1056
|
+
# Try multiple possible locations for resource files
|
|
1057
|
+
possible_paths = [
|
|
1058
|
+
File.expand_path(File.join(File.dirname(__FILE__), "../../resources/#{filename}")),
|
|
1059
|
+
File.expand_path(File.join(File.dirname(__FILE__), "../../../resources/#{filename}"))
|
|
1060
|
+
]
|
|
1061
|
+
|
|
1062
|
+
# Try gem datadir if available
|
|
1063
|
+
begin
|
|
1064
|
+
gem_path = File.join(Gem.datadir('number_station'), filename)
|
|
1065
|
+
possible_paths << gem_path if gem_path
|
|
1066
|
+
rescue
|
|
1067
|
+
# Gem.datadir not available, skip
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
possible_paths.find { |path| path && File.exist?(path) }
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def copy_message_template_files(target_path)
|
|
1074
|
+
# Copy intro_message.txt, outro_message.txt, and repeat_message.txt
|
|
1075
|
+
template_files = ['intro_message.txt', 'outro_message.txt', 'repeat_message.txt']
|
|
1076
|
+
|
|
1077
|
+
template_files.each do |filename|
|
|
1078
|
+
source_file = find_resource_file(filename)
|
|
1079
|
+
target_file = File.join(target_path, filename)
|
|
1080
|
+
|
|
1081
|
+
unless File.exist?(target_file)
|
|
1082
|
+
if source_file && File.exist?(source_file)
|
|
1083
|
+
FileUtils.cp(source_file, target_file)
|
|
1084
|
+
NumberStation.log.info "Created template file: #{target_file}"
|
|
1085
|
+
else
|
|
1086
|
+
NumberStation.log.warn "Template file not found: #{filename}"
|
|
1087
|
+
end
|
|
1088
|
+
else
|
|
1089
|
+
NumberStation.log.debug "Template file already exists: #{target_file}"
|
|
1090
|
+
end
|
|
1091
|
+
end
|
|
1092
|
+
end
|
|
1093
|
+
|
|
1094
|
+
def create_default_config(target_file)
|
|
1095
|
+
default_config = <<~YAML
|
|
1096
|
+
logging:
|
|
1097
|
+
level: 0
|
|
1098
|
+
|
|
1099
|
+
server:
|
|
1100
|
+
host: "0.0.0.0"
|
|
1101
|
+
port: 8080
|
|
1102
|
+
|
|
1103
|
+
resources:
|
|
1104
|
+
intro:
|
|
1105
|
+
template: "intro_message.txt"
|
|
1106
|
+
enabled: true
|
|
1107
|
+
outro:
|
|
1108
|
+
template: "outro_message.txt"
|
|
1109
|
+
enabled: true
|
|
1110
|
+
repeat:
|
|
1111
|
+
template: "repeat_message.txt"
|
|
1112
|
+
enabled: false
|
|
1113
|
+
|
|
1114
|
+
agent_list:
|
|
1115
|
+
- name: Abyss
|
|
1116
|
+
location: null
|
|
1117
|
+
handler_codeword: null
|
|
1118
|
+
start_date: null
|
|
1119
|
+
end_date: null
|
|
1120
|
+
active: false
|
|
1121
|
+
- name: Ash
|
|
1122
|
+
location: null
|
|
1123
|
+
handler_codeword: null
|
|
1124
|
+
start_date: null
|
|
1125
|
+
end_date: null
|
|
1126
|
+
active: false
|
|
1127
|
+
- name: Blade
|
|
1128
|
+
location: null
|
|
1129
|
+
handler_codeword: null
|
|
1130
|
+
start_date: null
|
|
1131
|
+
end_date: null
|
|
1132
|
+
active: false
|
|
1133
|
+
- name: Blitz
|
|
1134
|
+
location: null
|
|
1135
|
+
handler_codeword: null
|
|
1136
|
+
start_date: null
|
|
1137
|
+
end_date: null
|
|
1138
|
+
active: false
|
|
1139
|
+
- name: Cipher
|
|
1140
|
+
location: null
|
|
1141
|
+
handler_codeword: null
|
|
1142
|
+
start_date: null
|
|
1143
|
+
end_date: null
|
|
1144
|
+
active: false
|
|
1145
|
+
- name: Cobra
|
|
1146
|
+
location: null
|
|
1147
|
+
handler_codeword: null
|
|
1148
|
+
start_date: null
|
|
1149
|
+
end_date: null
|
|
1150
|
+
active: false
|
|
1151
|
+
- name: Dagger
|
|
1152
|
+
location: null
|
|
1153
|
+
handler_codeword: null
|
|
1154
|
+
start_date: null
|
|
1155
|
+
end_date: null
|
|
1156
|
+
active: false
|
|
1157
|
+
- name: Drift
|
|
1158
|
+
location: null
|
|
1159
|
+
handler_codeword: null
|
|
1160
|
+
start_date: null
|
|
1161
|
+
end_date: null
|
|
1162
|
+
active: false
|
|
1163
|
+
- name: Dusk
|
|
1164
|
+
location: null
|
|
1165
|
+
handler_codeword: null
|
|
1166
|
+
start_date: null
|
|
1167
|
+
end_date: null
|
|
1168
|
+
active: false
|
|
1169
|
+
- name: Eclipse
|
|
1170
|
+
location: null
|
|
1171
|
+
handler_codeword: null
|
|
1172
|
+
start_date: null
|
|
1173
|
+
end_date: null
|
|
1174
|
+
active: false
|
|
1175
|
+
- name: Enigma
|
|
1176
|
+
location: null
|
|
1177
|
+
handler_codeword: null
|
|
1178
|
+
start_date: null
|
|
1179
|
+
end_date: null
|
|
1180
|
+
active: false
|
|
1181
|
+
- name: Fang
|
|
1182
|
+
location: null
|
|
1183
|
+
handler_codeword: null
|
|
1184
|
+
start_date: null
|
|
1185
|
+
end_date: null
|
|
1186
|
+
active: false
|
|
1187
|
+
- name: Flux
|
|
1188
|
+
location: null
|
|
1189
|
+
handler_codeword: null
|
|
1190
|
+
start_date: null
|
|
1191
|
+
end_date: null
|
|
1192
|
+
active: false
|
|
1193
|
+
- name: Frost
|
|
1194
|
+
location: null
|
|
1195
|
+
handler_codeword: null
|
|
1196
|
+
start_date: null
|
|
1197
|
+
end_date: null
|
|
1198
|
+
active: false
|
|
1199
|
+
- name: Frostbite
|
|
1200
|
+
location: null
|
|
1201
|
+
handler_codeword: null
|
|
1202
|
+
start_date: null
|
|
1203
|
+
end_date: null
|
|
1204
|
+
active: false
|
|
1205
|
+
- name: Ghost
|
|
1206
|
+
location: null
|
|
1207
|
+
handler_codeword: null
|
|
1208
|
+
start_date: null
|
|
1209
|
+
end_date: null
|
|
1210
|
+
active: false
|
|
1211
|
+
- name: Grim
|
|
1212
|
+
location: null
|
|
1213
|
+
handler_codeword: null
|
|
1214
|
+
start_date: null
|
|
1215
|
+
end_date: null
|
|
1216
|
+
active: false
|
|
1217
|
+
- name: Grimlock
|
|
1218
|
+
location: null
|
|
1219
|
+
handler_codeword: null
|
|
1220
|
+
start_date: null
|
|
1221
|
+
end_date: null
|
|
1222
|
+
active: false
|
|
1223
|
+
- name: Havoc
|
|
1224
|
+
location: null
|
|
1225
|
+
handler_codeword: null
|
|
1226
|
+
start_date: null
|
|
1227
|
+
end_date: null
|
|
1228
|
+
active: false
|
|
1229
|
+
- name: Helix
|
|
1230
|
+
location: null
|
|
1231
|
+
handler_codeword: null
|
|
1232
|
+
start_date: null
|
|
1233
|
+
end_date: null
|
|
1234
|
+
active: false
|
|
1235
|
+
- name: Hunter
|
|
1236
|
+
location: null
|
|
1237
|
+
handler_codeword: null
|
|
1238
|
+
start_date: null
|
|
1239
|
+
end_date: null
|
|
1240
|
+
active: false
|
|
1241
|
+
- name: Iron
|
|
1242
|
+
location: null
|
|
1243
|
+
handler_codeword: null
|
|
1244
|
+
start_date: null
|
|
1245
|
+
end_date: null
|
|
1246
|
+
active: false
|
|
1247
|
+
- name: Ironclad
|
|
1248
|
+
location: null
|
|
1249
|
+
handler_codeword: null
|
|
1250
|
+
start_date: null
|
|
1251
|
+
end_date: null
|
|
1252
|
+
active: false
|
|
1253
|
+
- name: Jinx
|
|
1254
|
+
location: null
|
|
1255
|
+
handler_codeword: null
|
|
1256
|
+
start_date: null
|
|
1257
|
+
end_date: null
|
|
1258
|
+
active: false
|
|
1259
|
+
- name: Kraken
|
|
1260
|
+
location: null
|
|
1261
|
+
handler_codeword: null
|
|
1262
|
+
start_date: null
|
|
1263
|
+
end_date: null
|
|
1264
|
+
active: false
|
|
1265
|
+
- name: Nexus
|
|
1266
|
+
location: null
|
|
1267
|
+
handler_codeword: null
|
|
1268
|
+
start_date: null
|
|
1269
|
+
end_date: null
|
|
1270
|
+
active: false
|
|
1271
|
+
- name: Nightfall
|
|
1272
|
+
location: null
|
|
1273
|
+
handler_codeword: null
|
|
1274
|
+
start_date: null
|
|
1275
|
+
end_date: null
|
|
1276
|
+
active: false
|
|
1277
|
+
- name: Nightshade
|
|
1278
|
+
location: null
|
|
1279
|
+
handler_codeword: null
|
|
1280
|
+
start_date: null
|
|
1281
|
+
end_date: null
|
|
1282
|
+
active: false
|
|
1283
|
+
- name: Nova
|
|
1284
|
+
location: null
|
|
1285
|
+
handler_codeword: null
|
|
1286
|
+
start_date: null
|
|
1287
|
+
end_date: null
|
|
1288
|
+
active: false
|
|
1289
|
+
- name: Nyx
|
|
1290
|
+
location: null
|
|
1291
|
+
handler_codeword: null
|
|
1292
|
+
start_date: null
|
|
1293
|
+
end_date: null
|
|
1294
|
+
active: false
|
|
1295
|
+
- name: Obsidian
|
|
1296
|
+
location: null
|
|
1297
|
+
handler_codeword: null
|
|
1298
|
+
start_date: null
|
|
1299
|
+
end_date: null
|
|
1300
|
+
active: false
|
|
1301
|
+
- name: Onyx
|
|
1302
|
+
location: null
|
|
1303
|
+
handler_codeword: null
|
|
1304
|
+
start_date: null
|
|
1305
|
+
end_date: null
|
|
1306
|
+
active: false
|
|
1307
|
+
- name: Phantom
|
|
1308
|
+
location: null
|
|
1309
|
+
handler_codeword: null
|
|
1310
|
+
start_date: null
|
|
1311
|
+
end_date: null
|
|
1312
|
+
active: false
|
|
1313
|
+
- name: Pulse
|
|
1314
|
+
location: null
|
|
1315
|
+
handler_codeword: null
|
|
1316
|
+
start_date: null
|
|
1317
|
+
end_date: null
|
|
1318
|
+
active: false
|
|
1319
|
+
- name: Raptor
|
|
1320
|
+
location: null
|
|
1321
|
+
handler_codeword: null
|
|
1322
|
+
start_date: null
|
|
1323
|
+
end_date: null
|
|
1324
|
+
active: false
|
|
1325
|
+
- name: Raven
|
|
1326
|
+
location: null
|
|
1327
|
+
handler_codeword: null
|
|
1328
|
+
start_date: null
|
|
1329
|
+
end_date: null
|
|
1330
|
+
active: false
|
|
1331
|
+
- name: Razor
|
|
1332
|
+
location: null
|
|
1333
|
+
handler_codeword: null
|
|
1334
|
+
start_date: null
|
|
1335
|
+
end_date: null
|
|
1336
|
+
active: false
|
|
1337
|
+
- name: Reaper
|
|
1338
|
+
location: null
|
|
1339
|
+
handler_codeword: null
|
|
1340
|
+
start_date: null
|
|
1341
|
+
end_date: null
|
|
1342
|
+
active: false
|
|
1343
|
+
- name: Revenant
|
|
1344
|
+
location: null
|
|
1345
|
+
handler_codeword: null
|
|
1346
|
+
start_date: null
|
|
1347
|
+
end_date: null
|
|
1348
|
+
active: false
|
|
1349
|
+
- name: Riven
|
|
1350
|
+
location: null
|
|
1351
|
+
handler_codeword: null
|
|
1352
|
+
start_date: null
|
|
1353
|
+
end_date: null
|
|
1354
|
+
active: false
|
|
1355
|
+
- name: Rogue
|
|
1356
|
+
location: null
|
|
1357
|
+
handler_codeword: null
|
|
1358
|
+
start_date: null
|
|
1359
|
+
end_date: null
|
|
1360
|
+
active: false
|
|
1361
|
+
- name: Saber
|
|
1362
|
+
location: null
|
|
1363
|
+
handler_codeword: null
|
|
1364
|
+
start_date: null
|
|
1365
|
+
end_date: null
|
|
1366
|
+
active: false
|
|
1367
|
+
- name: Scorch
|
|
1368
|
+
location: null
|
|
1369
|
+
handler_codeword: null
|
|
1370
|
+
start_date: null
|
|
1371
|
+
end_date: null
|
|
1372
|
+
active: false
|
|
1373
|
+
- name: Scorpion
|
|
1374
|
+
location: null
|
|
1375
|
+
handler_codeword: null
|
|
1376
|
+
start_date: null
|
|
1377
|
+
end_date: null
|
|
1378
|
+
active: false
|
|
1379
|
+
- name: Shade
|
|
1380
|
+
location: null
|
|
1381
|
+
handler_codeword: null
|
|
1382
|
+
start_date: null
|
|
1383
|
+
end_date: null
|
|
1384
|
+
active: false
|
|
1385
|
+
- name: Shadow
|
|
1386
|
+
location: null
|
|
1387
|
+
handler_codeword: null
|
|
1388
|
+
start_date: null
|
|
1389
|
+
end_date: null
|
|
1390
|
+
active: false
|
|
1391
|
+
- name: Shard
|
|
1392
|
+
location: null
|
|
1393
|
+
handler_codeword: null
|
|
1394
|
+
start_date: null
|
|
1395
|
+
end_date: null
|
|
1396
|
+
active: false
|
|
1397
|
+
- name: Slate
|
|
1398
|
+
location: null
|
|
1399
|
+
handler_codeword: null
|
|
1400
|
+
start_date: null
|
|
1401
|
+
end_date: null
|
|
1402
|
+
active: false
|
|
1403
|
+
- name: Specter
|
|
1404
|
+
location: null
|
|
1405
|
+
handler_codeword: null
|
|
1406
|
+
start_date: null
|
|
1407
|
+
end_date: null
|
|
1408
|
+
active: false
|
|
1409
|
+
- name: Storm
|
|
1410
|
+
location: null
|
|
1411
|
+
handler_codeword: null
|
|
1412
|
+
start_date: null
|
|
1413
|
+
end_date: null
|
|
1414
|
+
active: false
|
|
1415
|
+
- name: Striker
|
|
1416
|
+
location: null
|
|
1417
|
+
handler_codeword: null
|
|
1418
|
+
start_date: null
|
|
1419
|
+
end_date: null
|
|
1420
|
+
active: false
|
|
1421
|
+
- name: Surge
|
|
1422
|
+
location: null
|
|
1423
|
+
handler_codeword: null
|
|
1424
|
+
start_date: null
|
|
1425
|
+
end_date: null
|
|
1426
|
+
active: false
|
|
1427
|
+
- name: Talon
|
|
1428
|
+
location: null
|
|
1429
|
+
handler_codeword: null
|
|
1430
|
+
start_date: null
|
|
1431
|
+
end_date: null
|
|
1432
|
+
active: false
|
|
1433
|
+
- name: Tempest
|
|
1434
|
+
location: null
|
|
1435
|
+
handler_codeword: null
|
|
1436
|
+
start_date: null
|
|
1437
|
+
end_date: null
|
|
1438
|
+
active: false
|
|
1439
|
+
- name: Thorn
|
|
1440
|
+
location: null
|
|
1441
|
+
handler_codeword: null
|
|
1442
|
+
start_date: null
|
|
1443
|
+
end_date: null
|
|
1444
|
+
active: false
|
|
1445
|
+
- name: Titan
|
|
1446
|
+
location: null
|
|
1447
|
+
handler_codeword: null
|
|
1448
|
+
start_date: null
|
|
1449
|
+
end_date: null
|
|
1450
|
+
active: false
|
|
1451
|
+
- name: Vantage
|
|
1452
|
+
location: null
|
|
1453
|
+
handler_codeword: null
|
|
1454
|
+
start_date: null
|
|
1455
|
+
end_date: null
|
|
1456
|
+
active: false
|
|
1457
|
+
- name: Venom
|
|
1458
|
+
location: null
|
|
1459
|
+
handler_codeword: null
|
|
1460
|
+
start_date: null
|
|
1461
|
+
end_date: null
|
|
1462
|
+
active: false
|
|
1463
|
+
- name: Vex
|
|
1464
|
+
location: null
|
|
1465
|
+
handler_codeword: null
|
|
1466
|
+
start_date: null
|
|
1467
|
+
end_date: null
|
|
1468
|
+
active: false
|
|
1469
|
+
- name: Viper
|
|
1470
|
+
location: null
|
|
1471
|
+
handler_codeword: null
|
|
1472
|
+
start_date: null
|
|
1473
|
+
end_date: null
|
|
1474
|
+
active: false
|
|
1475
|
+
- name: Void
|
|
1476
|
+
location: null
|
|
1477
|
+
handler_codeword: null
|
|
1478
|
+
start_date: null
|
|
1479
|
+
end_date: null
|
|
1480
|
+
active: false
|
|
1481
|
+
- name: Voidwalker
|
|
1482
|
+
location: null
|
|
1483
|
+
handler_codeword: null
|
|
1484
|
+
start_date: null
|
|
1485
|
+
end_date: null
|
|
1486
|
+
active: false
|
|
1487
|
+
- name: Vortex
|
|
1488
|
+
location: null
|
|
1489
|
+
handler_codeword: null
|
|
1490
|
+
start_date: null
|
|
1491
|
+
end_date: null
|
|
1492
|
+
active: false
|
|
1493
|
+
- name: Wraith
|
|
1494
|
+
location: null
|
|
1495
|
+
handler_codeword: null
|
|
1496
|
+
start_date: null
|
|
1497
|
+
end_date: null
|
|
1498
|
+
active: false
|
|
1499
|
+
- name: Zephyr
|
|
1500
|
+
location: null
|
|
1501
|
+
handler_codeword: null
|
|
1502
|
+
start_date: null
|
|
1503
|
+
end_date: null
|
|
1504
|
+
active: false
|
|
1505
|
+
- name: Zero
|
|
1506
|
+
location: null
|
|
1507
|
+
handler_codeword: null
|
|
1508
|
+
start_date: null
|
|
1509
|
+
end_date: null
|
|
1510
|
+
active: false
|
|
1511
|
+
YAML
|
|
1512
|
+
|
|
1513
|
+
File.write(target_file, default_config)
|
|
1514
|
+
end
|
|
212
1515
|
end
|
|
213
1516
|
end
|