palbo-lijab 0.1.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.
@@ -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
+