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,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