lijab 0.1.1

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.
@@ -0,0 +1,234 @@
1
+ require 'readline'
2
+ require 'readline/extra'
3
+
4
+ module Lijab
5
+
6
+ module InputHandler
7
+
8
+ DEFAULT_PROMPT = "> "
9
+
10
+ @prompt = DEFAULT_PROMPT
11
+ @last_to = ""
12
+ @last_typed = ""
13
+ @multiline = false
14
+ @multilines = []
15
+
16
+ module_function
17
+
18
+ def init
19
+ Readline::completer_word_break_characters = ""
20
+ Readline::completion_append_character = " "
21
+ Readline::completion_proc = method(:completer).to_proc
22
+ Readline::pre_input_proc = lambda do
23
+ print "#{ANSI.cleartoeol}" ; STDOUT.flush
24
+ unless @last_to.empty?
25
+ Readline::insert_text("#{@last_to}: ")
26
+ Readline::redisplay
27
+ end
28
+ end
29
+
30
+ if Config.opts[:ctrl_c_quits]
31
+ trap("SIGINT") { Main.quit }
32
+ else
33
+ trap("SIGINT") do
34
+ Readline::line_buffer = ""
35
+ puts
36
+ Out::make_infoline
37
+ print "#{@prompt}"
38
+ STDOUT.flush
39
+ end
40
+ end
41
+
42
+ read_typed_history()
43
+
44
+ init_char_input_stuff()
45
+
46
+ @input_thread = Thread.new { read_input() }
47
+ end
48
+
49
+ def prompt(p=nil)
50
+ return @prompt unless p
51
+ @prompt = p
52
+ end
53
+
54
+ def reset_prompt
55
+ @prompt = DEFAULT_PROMPT
56
+ end
57
+
58
+ def init_char_input_stuff
59
+ # i'm surprised this doesn't make typing fucking unbearable
60
+
61
+ @on_char_input_blocks = []
62
+
63
+ @on_char_input_blocks << lambda do |c|
64
+ to, msg = Readline::line_buffer.split(":", 2).strip
65
+ if to && msg && Main.contacts.key?(to)
66
+ # TODO: try to see if a thread improves things
67
+ Main.contacts[to].typed_stuff
68
+ end
69
+ c
70
+ end
71
+
72
+ Readline::char_input_proc = lambda do |c|
73
+ ret = c
74
+ @on_char_input_blocks.each do |block|
75
+ ret = block.call(c)
76
+ break if ret != c
77
+ end
78
+ ret
79
+ end
80
+ end
81
+
82
+ def on_char_input(&block)
83
+ @on_char_input_blocks << block
84
+ end
85
+
86
+ #def composing_watcher
87
+ # timer = nil
88
+ # loop do
89
+ # sleep(1)
90
+
91
+ # buf = Readline::line_buffer
92
+ # next unless buf != @last_line
93
+
94
+ # @last_line = buf
95
+ # to, msg = buf.split(":", 2).strip
96
+
97
+ # next unless to && msg && Main.contacts.key?(to)
98
+ # end
99
+ #end
100
+
101
+ def read_input
102
+ loop do
103
+ Out::make_infoline
104
+
105
+ t = Readline::readline(@prompt, true)
106
+
107
+ @last_typed = t || ""
108
+
109
+ if !t
110
+ if @multiline
111
+ @last_typed = @multilines
112
+ process_input(@multilines.join("\n"))
113
+ multiline(false)
114
+ else
115
+ if Config.opts[:ctrl_c_quits]
116
+ puts ; next
117
+ else
118
+ Main.quit
119
+ end
120
+ end
121
+ elsif !@multiline && t =~ /^\s*$/
122
+ Readline::HISTORY.pop
123
+ else
124
+ Readline::HISTORY.pop if Readline::HISTORY.to_a[-2] == t
125
+
126
+ if @multiline
127
+ @multilines.push(t)
128
+ @last_typed = @multilines
129
+ else
130
+ process_input(t)
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ def process_input(text)
137
+ return if text.empty?
138
+
139
+ if !Main.connected
140
+ # FIXME: brute force ftw!
141
+ Out::error("not connected :-(", false)
142
+ return
143
+ end
144
+
145
+ if text[0] == ?/
146
+ Commands::run(*text[1..-1].split(" ", 2))
147
+ @last_to = ""
148
+ else
149
+ to, msg = text.split(":", 2)
150
+ return unless to && msg && !msg.empty? && Main.contacts.key?(to)
151
+ msg = msg[1..-1] if msg[0].chr == " " # goddammit, whitespace will be the death of me
152
+
153
+ @last_to = to
154
+ jid = Jabber::JID.new(to)
155
+ jid = nil unless jid.resource
156
+ Main.contacts[to].send_message(msg, jid)
157
+ end
158
+ end
159
+
160
+ def delete_last_typed
161
+ if @last_typed.is_a?(Array)
162
+ @last_typed.each do |line|
163
+ # line length + multiline prompt + \n
164
+ # FIXME: put the multiline prompt somewhere
165
+ print "\b" * (line.length + 6)
166
+ print "#{ANSIMove.up(1)}"
167
+ end
168
+ print "#{ANSIMove.down(1)}" if @last_typed.length > 0
169
+ else
170
+ print "\b" * @last_typed.length
171
+ end
172
+ end
173
+
174
+ def delete_typed
175
+ if @multiline
176
+ delete_last_typed()
177
+ else
178
+ print "\b" * Readline::line_buffer.length
179
+ end
180
+ end
181
+
182
+ def redisplay_input()
183
+ if @multiline && !@multilines.empty?
184
+ puts "#{ANSI.clearline}#{DEFAULT_PROMPT}#{@multilines[0]}"
185
+ @multilines[1..-1].each do |line|
186
+ puts "#{ANSI.clearline}#{@prompt}#{line}"
187
+ end
188
+ end
189
+
190
+ Out::make_infoline()
191
+ print "#{@prompt}#{Readline::line_buffer}"
192
+ STDOUT.flush
193
+ end
194
+
195
+
196
+ def completer(line)
197
+ return if !Main.connected
198
+
199
+ if line[0] == ?/
200
+ Commands::completer(line)
201
+ else
202
+ Main.contacts.completer(line)
203
+ end
204
+ end
205
+
206
+ def save_typed_history
207
+ File.open(Config.account[:typed], 'w') do |f|
208
+ f.puts(Readline::HISTORY.to_a[-300..-1] || Readline::HISTORY.to_a)
209
+ end
210
+ end
211
+
212
+ def read_typed_history
213
+ path = Config.account[:typed]
214
+ File.read(path).each { |l| Readline::HISTORY.push(l.chomp) } if File.file?(path)
215
+ end
216
+
217
+ def multiline?
218
+ @multiline
219
+ end
220
+
221
+ def multiline(enable, first_line="")
222
+ @multiline = enable
223
+ @multilines = []
224
+ if enable
225
+ @multilines.push(first_line) unless first_line.empty?
226
+ prompt("---> ")
227
+ else
228
+ reset_prompt()
229
+ end
230
+ end
231
+ end
232
+
233
+ end
234
+
@@ -0,0 +1,248 @@
1
+ require 'date'
2
+ require 'file/tail'
3
+ require 'monitor'
4
+ require 'optparse'
5
+ require 'term/ansicolor'
6
+ require 'xmpp4r'
7
+ require 'xmpp4r/roster'
8
+ require 'yaml'
9
+
10
+ require 'lijab/commands'
11
+ require 'lijab/config'
12
+ require 'lijab/contacts'
13
+ require 'lijab/history'
14
+ require 'lijab/hooks'
15
+ require 'lijab/input'
16
+ require 'lijab/out'
17
+ require 'lijab/version'
18
+ require 'lijab/xmpp4r/message'
19
+
20
+ include Term
21
+
22
+
23
+ class String
24
+ include ANSIColor
25
+
26
+ def colored(*colors)
27
+ s = self
28
+ colors.each { |c| s = s.send(c) }
29
+ s
30
+ end
31
+ end
32
+
33
+ class Array
34
+ def strip
35
+ self.map { |s| s.strip }
36
+ end
37
+ end
38
+
39
+ Thread.abort_on_exception = true
40
+
41
+ module Lijab
42
+
43
+ module Main
44
+ module_function
45
+
46
+ @monitor = Monitor.new
47
+
48
+ def run!
49
+ args = parse_args()
50
+ Jabber::debug = args[:debug]
51
+
52
+ Config::init(args)
53
+ read_saved_session()
54
+
55
+ @connected = false
56
+
57
+ print ANSI.title("lijab -- #{Config.jid.strip}") ; STDOUT.flush
58
+
59
+ begin
60
+ setup_client()
61
+ rescue SystemCallError, SocketError
62
+ Out::error("couldn't connect", false)
63
+ reconnect()
64
+ end
65
+
66
+ Commands::init
67
+ InputHandler::init
68
+ end
69
+
70
+ def setup_after_connect
71
+ HooksHandler::init
72
+ end
73
+
74
+ def setup_client
75
+ return unless @monitor.try_enter
76
+ begin
77
+ @client = Jabber::Client.new(Config.jid)
78
+
79
+ @client.add_message_callback do |msg|
80
+ if Main.contacts.key?(msg.from)
81
+ Main.contacts[msg.from].handle_message(msg)
82
+ else
83
+ Main.contacts.handle_non_contact_message(msg)
84
+ end
85
+ end
86
+
87
+ @client.use_ssl = Config.account[:use_ssl]
88
+
89
+ Out::put("connecting...".yellow, false)
90
+
91
+ loop do
92
+ begin
93
+ @client.connect(Config.account[:server], Config.account[:port])
94
+
95
+ if !Config.account[:password]
96
+ print "#{ANSI.clearline}#{Config.account[:name]} account password: "
97
+ system("stty -echo") # FIXME
98
+ STDIN.read_nonblock(9999999) rescue nil
99
+ Config.account[:password] = gets.chomp
100
+ system("stty echo")
101
+ puts
102
+ end
103
+
104
+ @client.auth(Config.account[:password])
105
+ break
106
+ rescue Jabber::ClientAuthenticationFailure
107
+ Out::error("couldn't authenticate: wrong password?", false)
108
+ Config.account[:password] = nil
109
+ end
110
+ end
111
+
112
+ @client.on_exception do |e,stream,from|
113
+ @connected = false
114
+
115
+ case from
116
+ when :disconnected
117
+ Out::error("disconnected", false)
118
+ HooksHandler::handle_disconnect
119
+ reconnect()
120
+ else
121
+ # death before lost messages!
122
+ raise e || "exception raised from #{from}"
123
+ end
124
+ end
125
+
126
+ @contacts = Contacts::Contacts.new(Jabber::Roster::Helper.new(@client))
127
+ @client.send(@presence)
128
+ @connected = true
129
+
130
+ setup_after_connect()
131
+ HooksHandler::handle_connect
132
+
133
+ Out::put("connected!".green)
134
+ ensure
135
+ @monitor.exit
136
+ end
137
+ end
138
+
139
+ def reconnect
140
+ do_sleep = 1
141
+ loop do
142
+ do_sleep.downto(1) do |i|
143
+ Out::make_infoline
144
+ Out::infoline("trying reconnect in #{i*5} seconds...")
145
+ sleep(5)
146
+ end
147
+ do_sleep = [do_sleep*2, 10].min
148
+
149
+ begin
150
+ setup_client()
151
+ Out::clear_infoline
152
+ break
153
+ rescue SystemCallError, SocketError
154
+ end
155
+ end
156
+ end
157
+
158
+ def set_status(status, msg=nil)
159
+ type = status == :invisible ? :unavailable : nil
160
+ priority = Config.opts[:status_priorities][status]
161
+ status = nil if [:available, :invisible].include?(status)
162
+
163
+ @presence.set_type(type).set_show(status).set_status(msg).set_priority(priority)
164
+
165
+ @client.send(@presence)
166
+ end
167
+
168
+ def clear_status_message
169
+ set_status(@status)
170
+ end
171
+
172
+ def set_priority(priority)
173
+ @client.send(@presence.set_priority(priority))
174
+ end
175
+
176
+ def parse_args
177
+ options = {:debug => false}
178
+ begin
179
+ op = OptionParser.new do |opts|
180
+ opts.banner = "usage: lijab [-h | -V | [-a ACCOUNTNAME] [-d BASEDIR] [-D]]\n\n"
181
+ opts.on("-D", "--[no-]debug",
182
+ "output xmpp debug information to stderr") { |v| options[:debug] = v }
183
+ opts.on("-d", "--basedir BASEDIR",
184
+ "configs base directory") { |v| options[:basedir] = v }
185
+ opts.on("-a", "--account ACCOUNTNAME",
186
+ "the name of the account to connect to") { |v| options[:account] = v }
187
+ opts.on("-V", "--version", "print version information") do |v|
188
+ puts "lijab #{Lijab::VERSION}"
189
+ exit(0)
190
+ end
191
+ end
192
+ begin
193
+ op.parse!
194
+ rescue OptionParser::ParseError => e
195
+ puts "#{e}\n\n#{op.banner.chomp}"
196
+ exit(1)
197
+ end
198
+ rescue OptionParser::MissingArgument
199
+ puts "lijab: error: #{$!}\n\n#{op}"
200
+ exit 1
201
+ end
202
+ options
203
+ end
204
+
205
+ def save_session
206
+ return unless @presence
207
+
208
+ o = {:status => {:type => @presence.type,
209
+ :show => @presence.show,
210
+ :status => @presence.status,
211
+ :priority => @presence.priority}}
212
+ File.open(File.join(Config.account[:dir], "session_data.yml"), 'w') do |f|
213
+ f.puts(YAML.dump(o))
214
+ end
215
+ end
216
+
217
+ def read_saved_session
218
+ path = File.join(Config.account[:dir], "session_data.yml")
219
+
220
+ if File.file?(path)
221
+ o = YAML.load_file(path)
222
+ else
223
+ o = {:status => {:type => :available, :priority => 51}}
224
+ end
225
+
226
+ @presence = Jabber::Presence.new.set_type(o[:status][:type]) \
227
+ .set_show(o[:status][:show]) \
228
+ .set_status(o[:status][:status]) \
229
+ .set_priority(o[:status][:priority])
230
+ end
231
+
232
+ def quit
233
+ begin
234
+ @client.close if @connected
235
+ rescue
236
+ end
237
+ InputHandler::save_typed_history
238
+ Config::dump_config_file(false, true)
239
+ save_session()
240
+ puts "\nexiting..."
241
+ exit 0
242
+ end
243
+
244
+ attr_reader :contacts, :client, :connected, :presence
245
+ module_function :contacts, :client, :connected, :presence
246
+ end
247
+ end
248
+