palbo-lijab 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,122 @@
1
+ require 'time'
2
+
3
+ class File
4
+ include File::Tail
5
+ end
6
+
7
+ module Lijab
8
+
9
+ module HistoryHandler
10
+ class DummyHistory
11
+ def log(*a)
12
+ end
13
+
14
+ def last(*a)
15
+ Out::put("warning: logs are disabled")
16
+ []
17
+ end
18
+ end
19
+
20
+ class History
21
+ MEMORY_LOG_LENGTH = 50
22
+
23
+ def initialize(path, target=nil, log_to_session=false)
24
+ @path, @target, @log_to_session = path, target, log_to_session
25
+ @m = []
26
+ end
27
+
28
+ def init_logfile
29
+ @w = File.open(@path, 'a')
30
+ @r = File.open(@path, 'r')
31
+ @r.return_if_eof = true
32
+ end
33
+
34
+ def log(msg, direction, target=nil)
35
+ init_logfile() unless @w
36
+
37
+ time = Time.now.utc
38
+ target ||= @target
39
+ arrow = direction == :from ? "<-" : "->"
40
+ quoted = [msg].pack("M").gsub(/=?\n/) { |m| m[0] == ?= ? "" : "=0A" }
41
+
42
+ @w.puts("#{time.iso8601} #{target} #{arrow} #{quoted}")
43
+ @w.flush
44
+
45
+ @m.push({:time=>time.localtime, :target=>target, :direction=>direction, :msg=>msg})
46
+ @m.shift if @m.length > MEMORY_LOG_LENGTH
47
+
48
+ @m = []
49
+
50
+ HistoryHandler::log(msg, direction, target) if @log_to_session
51
+
52
+ self
53
+ end
54
+
55
+ def last(n)
56
+ return [] if n <= 0
57
+
58
+ init_logfile() unless @r
59
+
60
+ if n <= @m.length
61
+ @m[-n..-1]
62
+ else
63
+ ret = []
64
+ @r.seek(0, File::SEEK_END)
65
+ @r.backward(n)
66
+ @r.tail(n-@m.length) do |l|
67
+ time, target, direction, msg = l.split(" ", 4)
68
+ ret << {:time => Time.parse(time).localtime,
69
+ :target => target,
70
+ :direction => direction == "<-" ? :from : :to,
71
+ :msg => msg.strip.unpack("M").first}
72
+ end
73
+ ret += @m
74
+ if @m.length < MEMORY_LOG_LENGTH
75
+ @m = (ret[0...n-@m.length] + @m)
76
+ @m = @m[-MEMORY_LOG_LENGTH..-1] if @m.length > MEMORY_LOG_LENGTH
77
+ end
78
+ ret
79
+ end
80
+ end
81
+ end
82
+
83
+ module_function
84
+
85
+ @histories = {}
86
+
87
+ def get(jid)
88
+ name = jid.strip.to_s
89
+ if Config.account[:log]
90
+ path = File.join(Config.account[:log_dir], "#{name}.log")
91
+ @histories[name] ||= History.new(path, name, true)
92
+ else
93
+ @dummy ||= DummyHistory.new
94
+ end
95
+ end
96
+
97
+ def log(msg, direction, target)
98
+ return unless Config.account[:log]
99
+
100
+ init_session_log() unless @session
101
+ @session.log(msg, direction, target)
102
+ end
103
+
104
+ def last(n)
105
+ unless Config.account[:log]
106
+ Out::put("warning: logs are disabled")
107
+ return []
108
+ end
109
+
110
+ init_session_log() unless @session
111
+ @session.last(n)
112
+ end
113
+
114
+ def init_session_log
115
+ return unless Config.account[:log]
116
+
117
+ @session = History.new(path = File.join(Config.account[:log_dir], "session.log"))
118
+ end
119
+ end
120
+
121
+ end
122
+
@@ -0,0 +1,109 @@
1
+
2
+ module Lijab
3
+
4
+ module HooksHandler
5
+ module_function
6
+
7
+ @on_connect = []
8
+ @on_disconnect = []
9
+ @on_incoming_message = []
10
+ @on_presence = []
11
+ @on_pre_send_message = []
12
+ @on_post_send_message = []
13
+
14
+ def init
15
+ @on_connect, @on_disconnect, @on_incoming_message, @on_presence = [], [], [], []
16
+ @on_pre_send_message, @on_post_send_message = [], []
17
+
18
+ Dir[File.join(Config.dirs[:hooks], '**', '*.rb')].each { |f| load f }
19
+
20
+ Main.client.add_message_callback(&method(:handle_message))
21
+ Main.contacts.roster.add_presence_callback(&method(:handle_presence))
22
+ end
23
+
24
+ def handle_message(msg)
25
+ return unless msg.body && !msg.body.empty?
26
+
27
+ @on_incoming_message.each do |b|
28
+ b.call(Main.contacts[msg.from.strip], msg.body)
29
+ end
30
+ end
31
+
32
+ def handle_presence(roster_item, old_p, new_p)
33
+ @on_presence.each do |b|
34
+ b.call(Main.contacts[roster_item.jid.strip], old_p, new_p)
35
+ end
36
+ end
37
+
38
+ def handle_pre_send_message(contact, msg)
39
+ return msg if @on_pre_send_message.empty? || !msg.body || msg.body.empty?
40
+
41
+ @on_pre_send_message.inject(msg) do |ret_msg, block|
42
+ args = [contact, ret_msg.body]
43
+ args.push(msg) if block.arity == 3
44
+
45
+ m = block.call(*args)
46
+ break if !m
47
+
48
+ if m.is_a?(Jabber::Message)
49
+ m
50
+ else
51
+ ret_msg.body = m.to_s
52
+ ret_msg
53
+ end
54
+ end
55
+ end
56
+
57
+ def handle_post_send_message(contact, msg)
58
+ return if !msg.body || msg.body.empty?
59
+
60
+ @on_post_send_message.each do |block|
61
+ args = [contact, msg.body]
62
+ args.push(msg) if block.arity == 3
63
+ block.call(*args)
64
+ end
65
+ end
66
+
67
+ def handle_connect
68
+ @on_connect.each { |b| b.call }
69
+ end
70
+
71
+ def handle_disconnect
72
+ @on_disconnect.each { |b| b.call }
73
+ end
74
+
75
+ attr_reader :on_connect, :on_disconnect, :on_incoming_message, :on_presence,
76
+ :on_pre_send_message, :on_post_send_message
77
+ module_function :on_connect, :on_disconnect, :on_incoming_message,:on_presence,
78
+ :on_pre_send_message, :on_post_send_message
79
+ end
80
+
81
+ module Hooks
82
+ module_function
83
+
84
+ def on_incoming_message(&block)
85
+ HooksHandler::on_incoming_message.push(block)
86
+ end
87
+
88
+ def on_presence(&block)
89
+ HooksHandler::on_presence.push(block)
90
+ end
91
+
92
+ def on_pre_send_message(&block)
93
+ HooksHandler::on_pre_send_message.push(block)
94
+ end
95
+
96
+ def on_post_send_message(&block)
97
+ HooksHandler::on_post_send_message.push(block)
98
+ end
99
+
100
+ def on_connect(&block)
101
+ HooksHandler::on_connect.push(block)
102
+ end
103
+
104
+ def on_disconnect(&block)
105
+ HooksHandler::on_disconnect.push(block)
106
+ end
107
+ end
108
+
109
+ end
@@ -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
+
data/lib/lijab/main.rb ADDED
@@ -0,0 +1,246 @@
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")
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.on_exception do |e,stream,from|
80
+ @connected = false
81
+
82
+ case from
83
+ when :disconnected
84
+ Out::error("disconnected")
85
+ HooksHandler::handle_disconnect
86
+ reconnect()
87
+ else
88
+ # death before lost messages!
89
+ raise e || "exception raised from #{from}"
90
+ end
91
+ end
92
+
93
+ @client.add_message_callback do |msg|
94
+ if Main.contacts.key?(msg.from)
95
+ Main.contacts[msg.from].handle_message(msg)
96
+ else
97
+ Main.contacts.handle_non_contact_message(msg)
98
+ end
99
+ end
100
+
101
+ Out::put("connecting...".yellow, true)
102
+
103
+ @client.use_ssl = Config.account[:use_ssl]
104
+ @client.connect(Config.account[:server], Config.account[:port])
105
+
106
+ loop do
107
+ begin
108
+ if !Config.account[:password]
109
+ print "#{Config.account[:name]} account password: "
110
+ system("stty -echo") # FIXME
111
+ STDIN.read_nonblock(9999999) rescue nil
112
+ Config.account[:password] = gets.chomp
113
+ system("stty echo")
114
+ puts
115
+ end
116
+
117
+ @client.auth(Config.account[:password])
118
+ break
119
+ rescue Jabber::ClientAuthenticationFailure
120
+ Out::error("couldn't authenticate: wrong password?", false)
121
+ Config.account[:password] = nil
122
+ end
123
+ end
124
+
125
+ @contacts = Contacts::Contacts.new(Jabber::Roster::Helper.new(@client))
126
+ @client.send(@presence)
127
+ @connected = true
128
+
129
+ setup_after_connect()
130
+ HooksHandler::handle_connect
131
+
132
+ Out::put("connected!".green, true)
133
+ ensure
134
+ @monitor.exit
135
+ end
136
+ end
137
+
138
+ def reconnect
139
+ do_sleep = 1
140
+ loop do
141
+ do_sleep.downto(1) do |i|
142
+ Out::infoline("trying reconnect in #{i*5} seconds...")
143
+ sleep(5)
144
+ end
145
+ do_sleep = [do_sleep*2, 10].min
146
+
147
+ begin
148
+ setup_client()
149
+ Out::clear_infoline
150
+ break
151
+ rescue SystemCallError, SocketError
152
+ end
153
+ end
154
+ end
155
+
156
+ def set_status(status, msg=nil)
157
+ type = status == :invisible ? :unavailable : nil
158
+ priority = Config.opts[:status_priorities][status]
159
+ status = nil if [:available, :invisible].include?(status)
160
+
161
+ @presence.set_type(type).set_show(status).set_status(msg).set_priority(priority)
162
+
163
+ @client.send(@presence)
164
+ end
165
+
166
+ def clear_status_message
167
+ set_status(@status)
168
+ end
169
+
170
+ def set_priority(priority)
171
+ @client.send(@presence.set_priority(priority))
172
+ end
173
+
174
+ def parse_args
175
+ options = {:debug => false}
176
+ begin
177
+ op = OptionParser.new do |opts|
178
+ opts.banner = "usage: lijab [-h | -V | [-a ACCOUNTNAME] [-d BASEDIR] [-D]]\n\n"
179
+ opts.on("-D", "--[no-]debug",
180
+ "output xmpp debug information to stderr") { |v| options[:debug] = v }
181
+ opts.on("-d", "--basedir BASEDIR",
182
+ "configs base directory") { |v| options[:basedir] = v }
183
+ opts.on("-a", "--account ACCOUNTNAME",
184
+ "the name of the account to connect to") { |v| options[:account] = v }
185
+ opts.on("-V", "--version", "print version information") do |v|
186
+ puts "lijab #{Lijab::VERSION}"
187
+ exit(0)
188
+ end
189
+ end
190
+ begin
191
+ op.parse!
192
+ rescue OptionParser::ParseError => e
193
+ puts "#{e}\n\n#{op.banner.chomp}"
194
+ exit(1)
195
+ end
196
+ rescue OptionParser::MissingArgument
197
+ puts "lijab: error: #{$!}\n\n#{op}"
198
+ exit 1
199
+ end
200
+ options
201
+ end
202
+
203
+ def save_session
204
+ return unless @presence
205
+
206
+ o = {:status => {:type => @presence.type,
207
+ :show => @presence.show,
208
+ :status => @presence.status,
209
+ :priority => @presence.priority}}
210
+ File.open(File.join(Config.account[:dir], "session_data.yml"), 'w') do |f|
211
+ f.puts(YAML.dump(o))
212
+ end
213
+ end
214
+
215
+ def read_saved_session
216
+ path = File.join(Config.account[:dir], "session_data.yml")
217
+
218
+ if File.file?(path)
219
+ o = YAML.load_file(path)
220
+ else
221
+ o = {:status => {:type => :available, :priority => 51}}
222
+ end
223
+
224
+ @presence = Jabber::Presence.new.set_type(o[:status][:type]) \
225
+ .set_show(o[:status][:show]) \
226
+ .set_status(o[:status][:status]) \
227
+ .set_priority(o[:status][:priority])
228
+ end
229
+
230
+ def quit
231
+ begin
232
+ @client.close if @connected
233
+ rescue
234
+ end
235
+ InputHandler::save_typed_history
236
+ Config::dump_config_file(false, true)
237
+ save_session()
238
+ puts "\nexiting..."
239
+ exit 0
240
+ end
241
+
242
+ attr_reader :contacts, :client, :connected, :presence
243
+ module_function :contacts, :client, :connected, :presence
244
+ end
245
+ end
246
+