number_station 0.2.0 → 0.4.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.
@@ -21,37 +21,72 @@
21
21
  require "securerandom"
22
22
  require "json"
23
23
  require "time"
24
+ require "date"
24
25
 
25
26
  module NumberStation
26
- def self.make_otp(pad_path, length, num_pads)
27
+ def self.make_otp(pad_path, length, num_pads, agent_name = nil)
27
28
  path = pad_path || Dir.pwd
28
- len = length || 250
29
- num = num_pads || 5
29
+ len = length || 500
30
+ num = num_pads || 500
30
31
 
31
- NumberStation.log.debug "make_otp"
32
- pads = {}
33
- id = rand(0..99999).to_s.rjust(5, "0")
34
- file_name = File.join(path, "one_time_pad_#{id}.json")
35
- NumberStation.log.debug "file_name: #{file_name}"
32
+ # Round length up to nearest multiple of 5
33
+ len = round_up_to_multiple_of_5(len.to_i)
36
34
 
37
- 0.upto(num.to_i - 1) do |i|
38
- pads[i] = {
39
- "key"=>SecureRandom.hex(len.to_i),
40
- "epoch_date"=>nil,
41
- "consumed"=>false
42
- }
35
+ # Generate date-based filename with uniqueness component
36
+ date_str = Date.today.strftime("%Y-%m-%d")
37
+ filename_base = if agent_name && !agent_name.empty?
38
+ "#{agent_name}-#{date_str}"
39
+ else
40
+ "one_time_pad-#{date_str}"
41
+ end
42
+
43
+ # Ensure uniqueness: if file exists, add a counter
44
+ filename = File.join(path, "#{filename_base}.json")
45
+ counter = 1
46
+ while File.exist?(filename)
47
+ filename = File.join(path, "#{filename_base}-#{counter.to_s.rjust(3, '0')}.json")
48
+ counter += 1
49
+ # Safety check to prevent infinite loop
50
+ raise StandardError, "Too many pad files with same date prefix: #{filename_base}" if counter > 999
43
51
  end
44
- one_time_pads = {
45
- :id=> id,
46
- :pads=> pads
52
+
53
+ # Generate a unique pad_id for internal use (using epoch timestamp + random)
54
+ pad_id = generate_pad_id
55
+
56
+ pads = generate_pads(num.to_i, len)
57
+ pad_data = {
58
+ id: pad_id,
59
+ pads: pads
47
60
  }
48
61
 
49
- unless File.file?(file_name)
50
- f = File.open(file_name, "w")
51
- f.write(one_time_pads.to_json)
52
- f.close
53
- else
54
- raise Exception.new("Exception #{file_name} already exists")
62
+ File.write(filename, pad_data.to_json)
63
+ NumberStation.log.debug "Created one-time pad: #{filename}"
64
+ filename
65
+ end
66
+
67
+ private
68
+
69
+ def self.round_up_to_multiple_of_5(number)
70
+ # Round up to nearest multiple of 5
71
+ # Examples: 3 -> 5, 7 -> 10, 12 -> 15, 15 -> 15, 500 -> 500
72
+ ((number + 4) / 5) * 5
73
+ end
74
+
75
+ def self.generate_pad_id
76
+ # Generate a unique ID using epoch timestamp and random component
77
+ # This ensures uniqueness even if multiple pads are created in the same second
78
+ "#{Time.now.to_i}-#{rand(1000..9999)}"
79
+ end
80
+
81
+ def self.generate_pads(num_pads, length)
82
+ pads = {}
83
+ 0.upto(num_pads - 1) do |i|
84
+ pads[i] = {
85
+ "key" => SecureRandom.hex(length),
86
+ "epoch_date" => nil,
87
+ "consumed" => false
88
+ }
55
89
  end
90
+ pads
56
91
  end
57
92
  end
@@ -21,7 +21,7 @@
21
21
 
22
22
  module NumberStation
23
23
 
24
- ALPHABET ={
24
+ ALPHABET = {
25
25
  '0' => "zero",
26
26
  '1' => "one",
27
27
  '2' => "two",
@@ -58,80 +58,69 @@ module NumberStation
58
58
  'x' => "xray",
59
59
  'y' => "yankee",
60
60
  'z' => "zulu"
61
- }
61
+ }.freeze
62
62
 
63
-
64
- def self.lookup_phonetic(c)
65
- begin
66
- return NumberStation::ALPHABET[c] + ' ' || ' '
67
- rescue Exception => e
68
- return ' '
69
- end
63
+ def self.lookup_phonetic(char)
64
+ phonetic = ALPHABET[char.downcase]
65
+ phonetic ? phonetic : nil
70
66
  end
71
67
 
72
68
 
73
69
  def self.espeak_word_template(word)
74
- return "<prosody pitch=\"#{randomsign() + rand(0..200).to_s}\">#{word}</prosody>"
70
+ pitch = "#{random_sign}#{rand(0..200)}"
71
+ "<prosody pitch=\"#{pitch}\">#{word}</prosody>"
75
72
  end
76
73
 
77
-
78
- def self.randomsign()
79
- return rand(0..1) == 0 ? "-" : "+"
74
+ def self.random_sign
75
+ rand(0..1) == 0 ? "-" : "+"
80
76
  end
81
77
 
82
-
83
78
  def self.generate_sentence(message)
84
- sentence = ""
85
- message.split(" ").each {|i| sentence += espeak_word_template(i)}
86
- return "<speak version=\"1.0\" xmlns=\"\" xmlns:xsi=\"\" xsi:schemaLocation=\"\" xml:lang=\"\"><voice gender=\"female\">#{sentence}</voice></speak>"
79
+ words = message.split(" ")
80
+ prosody_words = words.map { |word| espeak_word_template(word) }.join
81
+ "<speak version=\"1.0\" xmlns=\"\" xmlns:xsi=\"\" xsi:schemaLocation=\"\" xml:lang=\"\"><voice gender=\"female\">#{prosody_words}</voice></speak>"
87
82
  end
88
83
 
89
-
90
84
  def self.write_espeak_template_file(filename, sentence)
91
- f = File.open(filename, "w")
92
- f.write(sentence)
93
- f.close
85
+ File.write(filename, sentence)
94
86
  end
95
87
 
96
88
 
97
89
  def self.call_espeak(input_file_path, output_file_path)
98
- if NumberStation.data["resources"]["espeak"]["glados"]
99
- cmd = "espeak -ven+f3 -m -p 60 -s 180 -f #{input_file_path} --stdout | ffmpeg -i - -ar 44100 -ac 2 -ab 192k -f mp3 #{output_file_path}"
100
- else
101
- cmd = "espeak -m -p 60 -s 180 -f #{input_file_path} --stdout | ffmpeg -i - -ar 44100 -ac 2 -ab 192k -f mp3 #{output_file_path}"
102
- end
103
-
104
- unless NumberStation.command?('espeak') || NumberStation.command?('ffmpeg')
90
+ unless NumberStation.command?('espeak') && NumberStation.command?('ffmpeg')
105
91
  NumberStation.log.error "number_station requires the espeak and ffmpeg utilities are installed in order to output an mp3 file."
106
- else
107
- `#{cmd}`
92
+ return
108
93
  end
109
- end
110
94
 
95
+ # Use GLaDOS voice settings by default
96
+ voice_flag = "-ven+f3"
97
+ cmd = "espeak #{voice_flag} -m -p 60 -s 180 -f #{input_file_path} --stdout | ffmpeg -i - -ar 44100 -ac 2 -ab 192k -f mp3 #{output_file_path}"
98
+ `#{cmd}`
99
+ end
111
100
 
112
101
  def self.write_mp3(message, output_file_path)
113
- filename = NumberStation.data["resources"]["espeak"]["sentence_template"]
114
- if NumberStation.data["resources"]["espeak"]["glados"]
115
- sentence = NumberStation.generate_sentence(message)
116
- else
117
- sentence = message
118
- end
119
- NumberStation.write_espeak_template_file(filename, sentence)
120
- NumberStation.call_espeak(filename, output_file_path)
102
+ # Use temporary file for espeak template
103
+ template_file = "/tmp/espeak_tmp.xml"
104
+ # Generate GLaDOS-style sentence
105
+ sentence = generate_sentence(message)
106
+
107
+ write_espeak_template_file(template_file, sentence)
108
+ call_espeak(template_file, output_file_path)
121
109
  end
122
110
 
123
-
124
111
  def self.to_phonetic(file_name)
125
- message = ''
126
- puts file_name
127
- f = File.open(file_name)
128
- raw_message = f.read()
129
- f.close()
130
-
131
- raw_message.each_char do |c|
132
- message += NumberStation.lookup_phonetic(c)
112
+ raw_message = File.read(file_name)
113
+ # Remove all whitespace from input
114
+ cleaned_message = raw_message.gsub(/[\s\n\r]/, '')
115
+
116
+ # Process in groups of 5 characters
117
+ groups = cleaned_message.chars.each_slice(5).map do |group|
118
+ # Convert each character in the group to phonetic
119
+ group.map { |char| lookup_phonetic(char) }.compact.join(' ')
133
120
  end
134
- return message
121
+
122
+ # Join groups with double space for readability
123
+ groups.join(' ')
135
124
  end
136
125
 
137
126
 
@@ -20,5 +20,5 @@
20
20
  =end
21
21
 
22
22
  module NumberStation
23
- VERSION = "0.2.0"
23
+ VERSION = "0.4.0"
24
24
  end
@@ -18,7 +18,6 @@
18
18
  You should have received a copy of the GNU General Public License
19
19
  along with this program. If not, see <https://www.gnu.org/licenses/>.
20
20
  =end
21
- require 'pastel'
22
21
  require 'json'
23
22
  require 'logger'
24
23
  require 'number_station/cli'
@@ -27,11 +26,12 @@ require 'number_station/encrypt_message'
27
26
  require 'number_station/decrypt_message'
28
27
  require 'number_station/make_onetime_pad'
29
28
  require 'number_station/phonetic_conversion'
29
+ require 'number_station/examine_pads'
30
+ require 'number_station/GLaDOS_espeak'
30
31
  require 'number_station/version'
31
32
 
32
33
 
33
34
  module NumberStation
34
-
35
35
  def self.command?(name)
36
36
  `which #{name}`
37
37
  $?.success?
@@ -41,16 +41,46 @@ module NumberStation
41
41
  @log = log
42
42
  end
43
43
 
44
- def self.log()
45
- return @log
44
+ def self.log
45
+ @log
46
46
  end
47
47
 
48
48
  def self.set_data(data)
49
49
  @data = data
50
50
  end
51
51
 
52
- def self.data()
53
- return @data
52
+ def self.data
53
+ @data
54
+ end
55
+
56
+ def self.agent_list
57
+ return [] unless @data && @data["agent_list"]
58
+
59
+ # Handle backward compatibility: if agent_list contains strings, convert them
60
+ agents = @data["agent_list"]
61
+ if agents.is_a?(Array) && agents.first.is_a?(String)
62
+ # Old format: array of strings, convert to hashes
63
+ agents.map do |name|
64
+ {
65
+ "name" => name,
66
+ "location" => nil,
67
+ "handler_codeword" => nil,
68
+ "start_date" => nil,
69
+ "end_date" => nil,
70
+ "active" => false
71
+ }
72
+ end
73
+ else
74
+ # New format: array of hashes
75
+ agents
76
+ end
54
77
  end
55
78
 
79
+ def self.find_agent_by_name(name)
80
+ agent_list.find { |agent| agent["name"] == name }
81
+ end
82
+
83
+ def self.active_agents
84
+ agent_list.select { |agent| agent["active"] == true }
85
+ end
56
86
  end
@@ -53,8 +53,8 @@ Gem::Specification.new do |spec|
53
53
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
54
54
  spec.require_paths = ["lib"]
55
55
 
56
- spec.add_dependency "pastel"
57
- spec.add_dependency "thor"
56
+ spec.add_dependency "thor", "~> 1.5"
58
57
 
59
- spec.add_development_dependency "rake"
58
+ spec.add_development_dependency "bundler", "~> 2.0"
59
+ spec.add_development_dependency "rake", "~> 13.0"
60
60
  end