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,139 @@
1
+
2
+ module Lijab
3
+
4
+ module Commands
5
+ @registered = {}
6
+ @overloaded = {}
7
+
8
+ module CommandMixin
9
+ def define_meta(*names)
10
+ class_eval do
11
+ names.each do |name|
12
+ define_method(name) do |*args|
13
+ if args.size == 0
14
+ instance_variable_get("@#{name}")
15
+ else
16
+ instance_variable_set("@#{name}", *args)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ module ContactCompleterMixin
25
+ def completer(line)
26
+ _, contact = line.split(nil, 2)
27
+ Main.contacts.completer(contact, false)
28
+ end
29
+ end
30
+
31
+ class CommandError < RuntimeError
32
+ end
33
+
34
+ class Command
35
+ class << self
36
+ private :new
37
+ end
38
+
39
+ def self.define(name, &block)
40
+ c = new
41
+ c.instance_eval(&block)
42
+ Commands::register(name.to_sym, c)
43
+ end
44
+
45
+ def completer(s)
46
+ end
47
+
48
+ def run(args)
49
+ end
50
+
51
+ extend CommandMixin
52
+ define_meta :usage, :description
53
+ end
54
+
55
+ module_function
56
+
57
+ def init
58
+ files = Dir[File.join(File.dirname(File.expand_path(__FILE__)), 'commands', '*.rb')] + \
59
+ Dir[File.join(Config.dirs[:commands], '**', '*.rb')]
60
+
61
+ files.each { |f| load f }
62
+ Config.opts[:aliases].each do |a, c|
63
+ register_alias(a, c)
64
+ end
65
+ end
66
+
67
+ def register(name, cmd)
68
+ name = name.to_sym
69
+ @overloaded[name] = @registered[name] if @registered.key?(name)
70
+ @registered[name] = cmd
71
+ end
72
+
73
+ def register_alias(name, s)
74
+ alias_cmd, alias_args = s.split(" ", 2)
75
+ alias_cmd.strip!
76
+ alias_cmd = alias_cmd[1..-1] if alias_cmd[0] == ?/
77
+ name = name[1..-1] if name[0] == ?/
78
+
79
+ Command.define name.to_sym do
80
+ description %{Alias for "#{s}"}
81
+ @alias_cmd = alias_cmd
82
+ @alias_args = alias_args
83
+ @name = name
84
+
85
+ def run(args)
86
+ Commands::run(@alias_cmd, [@alias_args, args].join(" "), true)
87
+ end
88
+
89
+ def completer(line)
90
+ args = line.split(" ", 2)[1]
91
+ Commands::completer("/#{@alias_cmd} #{@alias_args} #{args}").map do |r|
92
+ r.gsub(/\/#{@alias_cmd}\s?/, "")
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ def get(name)
99
+ @registered[name.to_sym]
100
+ end
101
+
102
+ def registered?(name)
103
+ @registered.key?(name.to_sym)
104
+ end
105
+
106
+ def run(cmd, args="", is_alias=false)
107
+ cmd = cmd.strip.to_sym
108
+ command = @overloaded[cmd] if is_alias
109
+ command ||= @registered[cmd]
110
+ if command
111
+ begin
112
+ command.run(args.strip)
113
+ rescue CommandError => e
114
+ Out::error("#{cmd}: #{e}", false)
115
+ end
116
+ else
117
+ Out::error("no such command: /#{cmd}", false)
118
+ end
119
+ end
120
+
121
+ def completer(line)
122
+ cmd, args = line[1..-1].split(" ", 2).strip
123
+ cmd ||= ""
124
+
125
+ matches = @registered.keys.find_all { |c| c.to_s.match(/^#{Regexp.escape(cmd)}/) }
126
+
127
+ if !cmd.empty? && (matches.length == 1 || args) && registered?(cmd)
128
+ (@registered[cmd.to_sym].completer(line) || []).map { |s| "/#{cmd} #{s}" }
129
+ else
130
+ matches.map { |k| "/#{k}" }
131
+ end
132
+ end
133
+
134
+ attr_reader :registered
135
+ module_function :registered
136
+ end
137
+
138
+ end
139
+
@@ -0,0 +1,174 @@
1
+
2
+ module Lijab
3
+
4
+ module Config
5
+ module_function
6
+
7
+ def init(args)
8
+ @opts = {}
9
+ @dirs = {}
10
+ @files = {}
11
+ @accounts = []
12
+ @account = nil
13
+
14
+ setup_basedir(args[:basedir])
15
+ read_accounts(args[:account])
16
+ read_options()
17
+
18
+ @jid = Jabber::JID.new("#{@account[:jabberid]}")
19
+ @jid.resource ||= "lijab#{(0...5).map{rand(10).to_s}.join}"
20
+ @account[:server] ||= @jid.domain
21
+
22
+ create_account_dirs()
23
+ end
24
+
25
+ def setup_basedir(basedir)
26
+ # xdg? meh
27
+ #xdg = ENV["XDG_CONFIG_HOME"]
28
+ #xdg && File.join(xdg, "lijab") ||
29
+
30
+ @basedir = basedir ||
31
+ ENV["LIJABDIR"] ||
32
+ "~/.lijab"
33
+ @basedir = File.expand_path(@basedir)
34
+
35
+ unless File.directory?(@basedir)
36
+ puts "Creating #{@basedir} with the default configs"
37
+ end
38
+
39
+ %w{accounts commands hooks}.each do |d|
40
+ @dirs[d.to_sym] = path = File.join(@basedir, d)
41
+ FileUtils.mkdir_p(path)
42
+ end
43
+
44
+ @files[:accounts] = path = File.join(@basedir, "accounts.yml")
45
+ File.open(path, 'w') { |f| f.puts(DEFAULT_ACCOUNTS_FILE) } unless File.file?(path)
46
+
47
+ @files[:config] = File.join(@basedir, "config.yml")
48
+ dump_config_file(true)
49
+ end
50
+
51
+ def dump_config_file(default=false, clobber=false)
52
+ if !File.file?(@files[:config]) || clobber
53
+ File.open(@files[:config], 'w') do |f|
54
+ DEFAULT_OPTIONS.each do |a|
55
+ if a[2]
56
+ f.puts
57
+ a[2].each { |l| f.puts("# #{l}") }
58
+ end
59
+ v = default ? a[1] : @opts[a[0]]
60
+ f.puts(YAML.dump({a[0] => v})[5..-1].chomp)
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def read_accounts(account)
67
+ File.open(@files[:accounts]) do |f|
68
+ YAML.load_documents(f) { |a| @accounts << a }
69
+ end
70
+
71
+ errors = []
72
+ errors << "need at least one account!" if @accounts.empty?
73
+
74
+ @accounts.each do |a|
75
+ a[:port] ||= a[:use_ssl] ? 5223 : 5222
76
+
77
+ errors << "account #{a} needs a name" unless a.key?(:name)
78
+ errors << "account #{a[:name] || a} needs a jabberid" unless a.key?(:jabberid)
79
+ end
80
+
81
+ @account = account ? @accounts.find { |a| a[:name] == account} : @accounts[0]
82
+
83
+ errors << "no account with name #{account} in #{@accounts_file}" if account && !@account
84
+
85
+ errors.each do |e|
86
+ STDERR.puts("#{File.basename($0)}: error: #{e}")
87
+ end
88
+
89
+ exit(1) unless errors.empty?
90
+ end
91
+
92
+ def read_options
93
+ # FIXME: error check / validate
94
+
95
+ @opts = Hash[*DEFAULT_OPTIONS.collect { |a| [a[0], a[1]] }.flatten]
96
+ @opts.merge!(YAML.load_file(@files[:config]))
97
+ end
98
+
99
+ def create_account_dirs
100
+ @accounts.each do |a|
101
+ a[:dir] = File.join(@dirs[:accounts], @jid.strip.to_s)
102
+ a[:log_dir] = File.join(a[:dir], "logs")
103
+ a[:typed] = File.join(a[:dir], "typed_history")
104
+
105
+ [:dir, :log_dir].each { |s| FileUtils.mkdir_p(a[s]) }
106
+ end
107
+ end
108
+
109
+ DEFAULT_OPTIONS = [
110
+ [:datetime_format, "%H:%M:%S", ["Time formatting (leave empty to disable timestamps)"]],
111
+ [:history_datetime_format, "%Y-%b-%d %H:%M:%S"],
112
+ [:autocomplete_online_first, true,
113
+ ["When completing contacts try to find matches for online contacts, and if none",
114
+ "is found try to find matches on all of them. Otherwise always match every",
115
+ "contact."]],
116
+ [:show_status_changes, true, ["Show changes in contacts' status"]],
117
+ [:show_groups_in_contact_list, false, ["Group contacts in contact list"]],
118
+ [:ctrl_c_quits, false,
119
+ ["ctrl+c quits the program if enabled, otherwise ctrl+c ignores whatever is",
120
+ "typed and you get a clean prompt, and ctrl+d on a clean line exits lijab,",
121
+ "terminal style."]],
122
+ [:terminal_bell_on_message, true,
123
+ ["Ring the terminal bell on incoming message.",
124
+ "Useful for setting the urgent hint on the terminal window:",
125
+ "Set as so in your ~/.Xdefaults, might have to run xrdb -merge ~/.Xdefaults afterwards",
126
+ "XTerm*bellIsUrgent: true",
127
+ "or",
128
+ "URxvt*urgentOnBell: true",
129
+ "or just look it up on your terminal's man page, don't be lazy."]],
130
+ [:status_priorities,
131
+ {:chat => 55, :available => 50, :away => 40, :xa => 30, :dnd => 20},
132
+ ["Default priority for each status"]],
133
+ [:aliases, {"/h" => "/history", "/exit" => "/quit"},
134
+ ["Command aliases.",
135
+ "<command_alias> : <existing_command>",
136
+ "Commands can be overloaded.",
137
+ "For instance /who could be redefined like so to sort by status by default.",
138
+ "/who : /who status"]]
139
+ ]
140
+
141
+ DEFAULT_ACCOUNTS_FILE = %Q{
142
+ # Accounts go here. Separate each one with ---
143
+ # First one is the default.
144
+
145
+ #---
146
+ #:name : an_account # the account name
147
+ #:jabberid : fisk@example.com/lijab # the resource is optional
148
+ #:password : frosk # optional, will prompt if not present
149
+ #:server : localhost # optional, will use the jid domain if not present
150
+ #:port : 5222 # optional
151
+ #:use_ssl : no # deprecated in jabber, but might help sometimes
152
+ #:log : yes # yes|no ; default no
153
+
154
+ #---
155
+ #:name : another_account
156
+ #:jabberid : another_user@example.com/lijab
157
+
158
+ #---
159
+ #:name : gmail_account
160
+ #:jabberid : blah@gmail.com/lijab
161
+ #:server : talk.google.com
162
+ ## might wanna try use_ssl if the normal settings don't work (e.g. in ubuntu afaik)
163
+ ##:port : 5223
164
+ ##:use_ssl : yes
165
+ #:log : yes
166
+
167
+ }.gsub!(/^ */, '')
168
+
169
+ attr_reader :jid, :account, :basedir, :dirs, :files, :opts
170
+ module_function :jid, :account, :basedir, :dirs, :files, :opts
171
+ end
172
+
173
+ end
174
+
@@ -0,0 +1,318 @@
1
+
2
+ class Jabber::Presence
3
+ PRETTY = Hash.new(["", []]).update(
4
+ { :available => ["available", [:green, :bold]],
5
+ :away => ["away", [:magenta]],
6
+ :chat => ["chatty", [:green]],
7
+ :dnd => ["busy", [:red, :bold]],
8
+ :xa => ["not available", [:red]],
9
+ :offline => ["offline", [:blue]]
10
+ })
11
+
12
+ def pretty_show
13
+ case type()
14
+ when nil
15
+ show() || :available
16
+ when :unavailable
17
+ :offline
18
+ end
19
+ end
20
+
21
+ def pretty(colorize=false)
22
+ sh = pretty_show()
23
+
24
+ s, colors = PRETTY[sh]
25
+ s = s.colored(*colors) if colorize
26
+ message = status() && !status().empty? ? " [#{status()}]" : ""
27
+ "#{s}#{message}"
28
+ end
29
+ end
30
+
31
+ module Lijab
32
+ module Contacts
33
+
34
+ class Contact
35
+ attr_accessor :simple_name, :history
36
+ attr_writer :color
37
+ attr_reader :roster_item
38
+
39
+ COLORS = [:red, :blue, :yellow, :green, :magenta, :cyan].sort_by { rand() }
40
+ @@cur_color = 0
41
+
42
+ def initialize(simple_name, roster_item)
43
+ @simple_name = simple_name
44
+ @roster_item = roster_item
45
+ @resource_jid = @roster_item.jid
46
+ @history = HistoryHandler::get(jid())
47
+ end
48
+
49
+ def presence(jid=nil)
50
+ if jid
51
+ p = @roster_item.presences.select { |p| p.jid == jid }.first
52
+ else
53
+ p = @roster_item.presences.inject(nil) do |max, presence|
54
+ !max.nil? && max.priority.to_i > presence.priority.to_i ? max : presence
55
+ end
56
+ end
57
+ p || Jabber::Presence.new.set_type(:unavailable)
58
+ end
59
+
60
+ def handle_message(msg)
61
+ @thread = msg.thread
62
+
63
+ if msg.body && !msg.body.empty?
64
+ @resource_jid = msg.from
65
+ Out::message_in(@simple_name, msg.body, [color(), :bold])
66
+ @history.log(msg.body, :from)
67
+ end
68
+
69
+ if msg.chat_state
70
+ s = ""
71
+ case msg.chat_state
72
+ when :composing
73
+ s = "is typing"
74
+ when :active
75
+ Out::clear_infoline
76
+ when :gone
77
+ s = "went away"
78
+ when :paused
79
+ s = "paused typing"
80
+ end
81
+ Out::infoline("* #{@simple_name} #{s}".red) unless s.empty?
82
+ end
83
+ end
84
+
85
+ def send_message(msg, jid=nil)
86
+ if msg.kind_of?(Jabber::Message)
87
+ msg.thread = @thread unless msg.thread
88
+ message = msg
89
+ elsif msg.kind_of?(String) && !msg.empty?
90
+ # TODO: send chat_state only in the first message
91
+ if jid
92
+ @resource_jid = jid
93
+ else
94
+ jid = @resource_jid
95
+ end
96
+ Out::message_out(@simple_name, msg, color())
97
+ message = Jabber::Message.new(jid, msg).set_type(:chat) \
98
+ .set_chat_state(:active) \
99
+ .set_thread(@thread)
100
+
101
+ @chat_state_timer.kill if @chat_state_timer && @chat_state_timer.alive?
102
+ end
103
+
104
+ message = HooksHandler::handle_pre_send_message(self, message)
105
+ return unless message
106
+
107
+ Main.client.send(message)
108
+
109
+ HooksHandler::handle_post_send_message(self, message)
110
+
111
+ @history.log(message.body, :to)
112
+ @chat_state = :active
113
+ end
114
+
115
+ def send_chat_state(state)
116
+ return if state == @chat_state
117
+ msg = Jabber::Message.new(jid(), nil).set_type(:chat).set_chat_state(state)
118
+ Main.client.send(msg)
119
+ @chat_state = state
120
+ end
121
+
122
+ def typed_stuff
123
+ send_chat_state(:composing)
124
+
125
+ @chat_state_timer.kill if @chat_state_timer && @chat_state_timer.alive?
126
+ @chat_state_timer = Thread.new do
127
+ sleep(3); return if !Main.connected
128
+ send_chat_state(:paused)
129
+ sleep(10); return if !Main.connected
130
+ send_chat_state(:active)
131
+ end
132
+ end
133
+
134
+ def presence_changed(old_p, new_p)
135
+ @resource_jid = @roster_item.jid if old_p && old_p.from == @resource_jid
136
+ end
137
+
138
+ def jid
139
+ @roster_item.jid
140
+ end
141
+
142
+ def online?
143
+ @roster_item.online?
144
+ end
145
+
146
+ def color
147
+ @color = COLORS[(@@cur_color = (@@cur_color + 1) % COLORS.length)] unless @color
148
+ @color
149
+ end
150
+
151
+ def to_s;
152
+ @simple_name
153
+ end
154
+ end
155
+
156
+ class Contacts < Hash
157
+ attr_reader :roster, :subscription_requests
158
+
159
+ def initialize(roster)
160
+ super()
161
+ @roster = roster
162
+
163
+ # why does everything always has to be so hackish?
164
+ self_ri = Jabber::Roster::Helper::RosterItem.new(Main.client)
165
+ self_ri.jid = Config.jid.strip
166
+ @roster.items[self_ri.jid] = self_ri
167
+
168
+ @roster.add_presence_callback(&method(:handle_presence))
169
+ @roster.add_subscription_callback(&method(:handle_subscription))
170
+ @roster.add_subscription_request_callback(&method(:handle_subscription))
171
+ @roster.wait_for_roster
172
+
173
+ @subscription_requests = {}
174
+ @short = {}
175
+
176
+ @roster.items.each do |jid, item|
177
+ add(jid, Contact.new(jid.node, item))
178
+ end
179
+ end
180
+
181
+ def [](k)
182
+ return @short[k] if @short.key?(k)
183
+
184
+ k = Jabber::JID.new(k) unless k.is_a?(Jabber::JID)
185
+
186
+ super(k) || super(k.strip)
187
+ end
188
+
189
+ def key?(k)
190
+ return true if @short.key?(k)
191
+
192
+ k = Jabber::JID.new(k) unless k.is_a?(Jabber::JID)
193
+
194
+ super(k) || super(k.strip)
195
+ end
196
+
197
+ def add(jid, contact=nil)
198
+ if contact
199
+ self[jid] = contact
200
+ if @short.key?(jid.node)
201
+ self[@short[jid.node].jid].simple_name = jid.strip.to_s
202
+ @short[@short[jid.node].jid.strip.to_s] = @short.delete(jid.node)
203
+ @short[jid.strip.to_s] = self[jid]
204
+ else
205
+ @short[jid.node] = self[jid]
206
+ end
207
+ else
208
+ jid = Jabber::JID.new(jid) unless jid.is_a?(Jabber::JID)
209
+ jid.strip!
210
+
211
+ @roster.add(jid, nil, true)
212
+ end
213
+
214
+ contact || self[jid]
215
+ end
216
+
217
+ def remove(jid)
218
+ return false unless key?(jid)
219
+
220
+ contact = self[jid]
221
+ contact.roster_item.remove()
222
+ @short.delete(contact.simple_name)
223
+ self.delete(contact.jid)
224
+
225
+ true
226
+ end
227
+
228
+ def process_request(jid, action)
229
+ jid = Jabber::JID.new(jid) unless jid.is_a?(Jabber::JID)
230
+ jid.strip!
231
+ if @subscription_requests.include?(jid)
232
+
233
+ @subscription_requests.delete(jid)
234
+ case action
235
+ when :accept
236
+ Main.contacts.roster.accept_subscription(jid)
237
+ when :decline
238
+ Main.contacts.roster.decline_subscription(jid)
239
+ end
240
+
241
+ true
242
+ else
243
+ false
244
+ end
245
+ end
246
+
247
+ def has_subscription_requests?
248
+ !@subscription_requests.empty?
249
+ end
250
+
251
+ def subscription_requests
252
+ @subscription_requests.keys.map { |jid| jid.to_s }
253
+ end
254
+
255
+ def completer(line, end_with_colon=true)
256
+ if line.include?(?@)
257
+ matches = @roster.items.values.collect { |ri| ri.presences }.flatten.select do |p|
258
+ p.from.to_s =~ /^#{Regexp.escape(line)}/
259
+ end.map { |p| p.from.to_s }
260
+ else
261
+ if Config.opts[:autocomplete_online_first]
262
+ matches = @short.keys.find_all do |name|
263
+ @short[name].online? && name =~ /^#{Regexp.escape(line)}/
264
+ end
265
+ end
266
+ if matches.empty? || !Config.opts[:autocomplete_online_first]
267
+ matches = @short.keys.find_all { |name| name =~ /^#{Regexp.escape(line)}/ }
268
+ end
269
+ end
270
+ end_with_colon && matches.length == 1 ? "#{matches.first}:" : matches
271
+ end
272
+
273
+ def handle_non_contact_message(msg)
274
+ # TODO: improve this, maybe show the contact differentiated in /contacts
275
+
276
+ jid = msg.from.strip
277
+
278
+ ri = Jabber::Roster::Helper::RosterItem.new(Main.client)
279
+ ri.jid = jid
280
+
281
+ self[jid] = Contact.new(jid.to_s, ri)
282
+ self[jid].handle_message(msg)
283
+ #add(jid, Contact.new(jid.to_s, ri)).handle_message(msg)
284
+ end
285
+
286
+ def handle_presence(roster_item, old_p, new_p)
287
+ contact = self[new_p.from]
288
+ if Config.opts[:show_status_changes]
289
+ type = new_p.type
290
+ if type == nil || type == :unavailable && (!contact || !contact.online?)
291
+ Out::presence(new_p.from.to_s, new_p)
292
+ end
293
+ end
294
+
295
+ if contact
296
+ contact.roster_item.presences.delete_if { |p| p.type == :unavailable } rescue nil
297
+ contact.presence_changed(old_p, new_p)
298
+ end
299
+ end
300
+
301
+ def handle_subscription(roster_item, presence)
302
+ show = true
303
+ if presence.type == :subscribe
304
+ show = !@subscription_requests.key?(presence.from.strip)
305
+ @subscription_requests[presence.from.strip] = presence
306
+ elsif presence.type == :subscribed
307
+ jid = presence.from.strip
308
+
309
+ @roster.add(jid)
310
+ add(jid, Contact.new(jid.node, @roster[jid]))
311
+ #p = Jabber::Presence.new.set_type(:probe)
312
+ end
313
+
314
+ Out::subscription(presence.from.to_s, presence.type, presence.status) if show
315
+ end
316
+ end
317
+ end
318
+ end