lijab 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,101 @@
1
+
2
+ module Lijab
3
+ module Commands
4
+
5
+ module ContactsCommandMixin
6
+ SORTBY = ["status", "alpha"]
7
+
8
+ def completer(line)
9
+ sortby = line.split[1] || ""
10
+ if SORTBY.grep(sortby).empty?
11
+ SORTBY.grep(/^#{Regexp.escape(sortby)}/)
12
+ end
13
+ end
14
+
15
+ def group_contacts(contacts)
16
+ grouped = {}
17
+ contacts.each do |jid,contact|
18
+ groups = contact.roster_item.groups
19
+ if groups.empty?
20
+ (grouped["<no group>"] ||= []) << contact
21
+ else
22
+ groups.each { |g| (grouped[g] ||= []) << contact }
23
+ end
24
+ end
25
+
26
+ grouped = grouped.sort_by { |g,c| g }
27
+ end
28
+
29
+ def print_contacts(sort_by_status=false, online_only=false)
30
+ if sort_by_status
31
+ contacts = Main.contacts.sort { |a, b| -(a[1].presence <=> b[1].presence) }
32
+ else
33
+ contacts = Main.contacts.sort_by { |j,c| c.simple_name }
34
+ end
35
+
36
+ if Config.opts[:show_groups_in_contact_list]
37
+ grouped = group_contacts(contacts)
38
+ else
39
+ grouped = {nil => contacts.map { |j,c| c }}
40
+ end
41
+
42
+ s = []
43
+ grouped.each do |group, contactz|
44
+ if online_only
45
+ next unless contactz.any? { |c| c.online? }
46
+ end
47
+
48
+ s << " #{group} ".on_blue if group
49
+ contactz.each do |contact|
50
+ unless online_only && !contact.online?
51
+ main = contact.presence
52
+ s << "* #{contact.simple_name} #{main.pretty(true)} " \
53
+ "(#{main.priority || 0}) [#{main.from || contact.jid}]"
54
+
55
+ if online_only && contact.roster_item.presences.length > 1
56
+ contact.roster_item.presences.each do |p|
57
+ if p.from != main.from
58
+ s << " #{p.from} #{p.pretty(true)} (#{p.priority || 0})"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ Out::put(s.join("\n")) unless s.empty?
67
+ end
68
+
69
+ end
70
+
71
+ Command.define :contacts do
72
+ usage "/contacts [status|alpha]"
73
+ description "Show a list of all contacts. Sorted alphabetically or by status."
74
+
75
+ SORTBY = ["status", "alpha"]
76
+
77
+ def run(args)
78
+ print_contacts(args.split[0] == "status")
79
+ end
80
+
81
+ class << self
82
+ include ContactsCommandMixin
83
+ end
84
+ end
85
+
86
+ Command.define :who do
87
+ usage "/who [status|alpha]"
88
+ description "Show a list of online contacts. Sorted alphabetically or by status."
89
+
90
+ def run(args)
91
+ print_contacts(args.split[0] == "status", true)
92
+ end
93
+
94
+ class << self
95
+ include ContactsCommandMixin
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ end
@@ -0,0 +1,49 @@
1
+
2
+ module Lijab
3
+ module Commands
4
+
5
+ Command.define :set do
6
+ usage "/set <option> [<value>]"
7
+ description "Modify the options. Print the current value if no <value> is given.\n" \
8
+ "See #{Config.files[:config]} for the available options."
9
+ def run(args)
10
+ option, value = args.split(nil, 2).strip
11
+
12
+ option = option.to_sym if option
13
+
14
+ if !(Config.opts[option].is_a?(String) ||
15
+ Config.opts[option].is_a?(Numeric) ||
16
+ [true, false, nil].include?(Config.opts[option])) &&
17
+ Config.opts.key?(option)
18
+ raise CommandError, %{can't change "#{option} with /set"}
19
+ elsif !Config.opts.key?(option)
20
+ raise CommandError, %{no such option "#{option}"}
21
+ end
22
+
23
+ if value && !value.empty?
24
+ begin
25
+ val = YAML.load(value)
26
+ #raise TypeError unless val.is_a?(Config.opts[option].class)
27
+ Config.opts[option] = val
28
+ rescue
29
+ Out::error("invalid value", false)
30
+ end
31
+ else
32
+ Out::put(YAML.dump(Config.opts[option])[4..-1].chomp)
33
+ end
34
+
35
+ end
36
+
37
+ def completer(line)
38
+ option = line.split(nil, 2).strip[1] || ""
39
+ Config.opts.keys.find_all do |k|
40
+ k.to_s =~ /^#{Regexp.escape(option)}/ &&
41
+ (Config.opts[k].is_a?(String) ||
42
+ Config.opts[k].is_a?(Numeric) ||
43
+ [true, false, nil].include?(Config.opts[k]))
44
+ end
45
+ end
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,132 @@
1
+
2
+ module Lijab
3
+ module Commands
4
+
5
+ Command.define :help do
6
+ usage "/help [<command> | commands]"
7
+ description "Get some help."
8
+
9
+ def run(args)
10
+ if args.empty?
11
+ s = %Q{
12
+ When in doubt, hit <tab>.
13
+
14
+ Some general hints:
15
+
16
+ Run "lijab -a <name>" to connect to the account named <name>.
17
+
18
+ Tab on an empty line will try to complete online contacts.
19
+ If there are no online contact matches for what you typed, offline contacts will also be
20
+ considered.
21
+
22
+ You can tab-complete specific resources of a contact by typing the contact name
23
+ followed by an @ character, e.g. somecontact@<tab> will complete all the available
24
+ resources for the contact and a message can be sent to that specific resource.
25
+
26
+ Config/logs folder is at #{Config.basedir}
27
+
28
+ Put your custom commands in #{Config.dirs[:commands]}
29
+ Check out the files in <install-path>/lib/lijab/commands/ for some examples.
30
+
31
+ Put your custom hooks in #{Config.dirs[:hooks]}
32
+
33
+ Send mails to quuxbaz@gmail.com to complain about the lack of documentation :-)
34
+
35
+ }.gsub!(/^ */, '')
36
+ Out::put(s)
37
+ else
38
+ if args == "commands"
39
+ Out::put
40
+ Commands::registered.each do |name, cmd|
41
+ Out::put(%{#{cmd.usage || "/#{name}"}}.magenta)
42
+ Out::put("#{cmd.description}\n\n")
43
+ end
44
+ else
45
+ cmd = Commands::get(args)
46
+ if cmd
47
+ s = "usage: #{cmd.usage}\n\n" if cmd.usage
48
+ s = "#{s}#{cmd.description}"
49
+ Out::put(s)
50
+ else
51
+ raise CommandError, %(No such command "#{args}")
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ def completer(line)
58
+ help_cmd, rest = line.split(" ", 2)
59
+ rest ||= ""
60
+
61
+ m = "commands" =~ /^#{Regexp.escape(rest)}/ ? ["commands"] : []
62
+
63
+ rest = "/#{rest}"
64
+
65
+ m += Commands::completer(rest).map { |c| c[1..-1] } if rest.split(" ", 2).length == 1
66
+ end
67
+ end
68
+
69
+ Command.define :history do
70
+ usage "/history [<contact>] [<limit>]"
71
+ description "Show the message history with a <contact>, or all the contacts."
72
+
73
+ def run(args)
74
+ contact, limit = args.split(" ", 2).strip
75
+ limit ||= 10
76
+
77
+ if contact
78
+ raise CommandError, %(No contact named "#{contact}) unless Main.contacts.key?(contact)
79
+ m = Main.contacts[contact].history.last(limit.to_i)
80
+ else
81
+ m = HistoryHandler::last(limit.to_i)
82
+ end
83
+ Out::history(*m)
84
+ end
85
+
86
+ class << self
87
+ include ContactCompleterMixin
88
+ end
89
+ end
90
+
91
+ Command.define :multiline do
92
+ usage "/multiline <contact> [<first_line>]"
93
+ description "Enter multiline mode, meaning, send a multiline message to a contact.\n" \
94
+ "Ctrl-d in an empty line exits multiline mode and sends the message."
95
+
96
+ def run(args)
97
+ contact, first_line = args.split(" ", 2).strip
98
+ first_line = "#{contact}: #{first_line}"
99
+ InputHandler::multiline(true, first_line)
100
+ end
101
+
102
+ class << self
103
+ include ContactCompleterMixin
104
+ end
105
+ end
106
+
107
+ Command.define :quit do
108
+ usage "/quit"
109
+ description "Quit lijab"
110
+
111
+ def run(args)
112
+ Main.quit
113
+ end
114
+ end
115
+
116
+ # TODO: make a generic option changer?
117
+ Command.define :show_status_changes do
118
+ usage "/show_status_changes yes|no"
119
+ description "Enable/disable printing the contacts' status changes. Can get quite spammish."
120
+
121
+ def run(args)
122
+ if !args || args.empty?
123
+ Out::put(Config.opts[:show_status_changes] ? "yes" : "no")
124
+ else
125
+ Config.opts[:show_status_changes] = args.strip == "yes"
126
+ end
127
+ end
128
+ end
129
+
130
+ end
131
+ end
132
+
@@ -0,0 +1,63 @@
1
+
2
+ module Lijab
3
+ module Commands
4
+
5
+ Command.define :priority do
6
+ usage "/priority [<priority>]"
7
+ description "Change the jabber priority. Number must be between -127 and 127.\n" \
8
+ "Show current priority if no argument is given."
9
+
10
+ def run(args)
11
+ if args.strip.empty?
12
+ Out::put("Current priority is #{Main.presence.priority}")
13
+ return
14
+ end
15
+
16
+ begin
17
+ p = Integer(args)
18
+ rescue ArgumentError
19
+ raise Commands::CommandError, %{"#{args}" is not a valid integer}
20
+ end
21
+
22
+ raise Commands::CommandError, "priority must be between -127 and 127" unless (-127..127).include?(p)
23
+
24
+ Main.set_priority(p)
25
+ end
26
+ end
27
+
28
+ Command.define :status do
29
+ usage "/status [available|away|chat|xa|dnd|invisible] [<message>]"
30
+ description "Set your status.\n" \
31
+ "If no status is given, keep the current and set the status message.\n" \
32
+ "If no message is given, keep the current status and clear the message.\n" \
33
+ "If no arguments are given, print the current status."
34
+
35
+ STATUSES = ["available", "away", "chat", "xa", "dnd", "invisible"]
36
+
37
+ def run(args)
38
+ status, message = args.split(" ", 2).strip
39
+
40
+ unless status
41
+ p = Main.presence
42
+ Out::put("#{Config.jid} (#{p.priority || 0}) #{p.pretty(true)}")
43
+ return
44
+ end
45
+
46
+ unless STATUSES.include?(status)
47
+ message = "#{status} #{message}".strip
48
+ status = nil
49
+ end
50
+
51
+ Main.set_status(status && status.to_sym, message)
52
+ end
53
+
54
+ def completer(line)
55
+ status = line.split[1] || ""
56
+ if STATUSES.grep(status).empty?
57
+ STATUSES.grep(/^#{Regexp.escape(status)}/)
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,78 @@
1
+
2
+ module Lijab
3
+ module Commands
4
+ Command.define :add do
5
+ usage "/add <user@server>"
6
+ description "Add a user to your roster."
7
+
8
+ def run(args)
9
+ Main.contacts.add(args)
10
+ Out::put("subscription request sent to #{args}")
11
+ end
12
+ end
13
+
14
+ Command.define :remove do
15
+ usage "/remove <user@server>"
16
+ description "Remove a user from your roster."
17
+
18
+ def run(args)
19
+ unless Main.contacts.remove(args)
20
+ raise CommandError, "no contact found for #{args}"
21
+ end
22
+ end
23
+
24
+ class << self
25
+ include ContactCompleterMixin
26
+ end
27
+ end
28
+
29
+ # TODO: <user@server | all>
30
+ Command.define :requests do
31
+ usage "/requests [accept|accept_and_add|decline <user@server>]"
32
+ description "Accept/decline a user's request to see your status.\n" \
33
+ "Print pending requests if no argument given." \
34
+
35
+ ACTIONS = ["accept", "accept_and_add", "decline"]
36
+
37
+ def run(args)
38
+ action, addr = args.split(nil, 2).strip
39
+
40
+ if action
41
+ unless ACTIONS.include?(action)
42
+ raise CommandError, "action must be accept, accept_and_add or decline"
43
+ end
44
+ raise CommandError, "need the user's address" unless addr and !addr.empty?
45
+
46
+ if ["accept", "accept_and_add"].include?(action)
47
+ success = Main.contacts.process_request(addr, :accept)
48
+ Main.contacts.add(addr) if success && action == "accept_and_add"
49
+ else
50
+ success = Main.contacts.process_request(addr, :decline)
51
+ end
52
+
53
+ raise CommandError, "no pending request from #{addr}" unless success
54
+ else
55
+ if Main.contacts.has_subscription_requests?
56
+ Out::put("pending requests from:")
57
+ Out::put(Main.contacts.subscription_requests.join("\n"))
58
+ else
59
+ Out::put("no pending requests")
60
+ end
61
+ end
62
+ end
63
+
64
+ def completer(line)
65
+ _, action, addr = line.split(nil, 3)
66
+
67
+ if !addr
68
+ ACTIONS.grep(/^#{Regexp.escape(action)}/)
69
+ elsif addr && ACTIONS.include?(action)
70
+ Main.contacts.subscription_requests.grep(/^#{Regexp.escape(addr)}/).map do |c|
71
+ "#{action} #{c}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ end
78
+ end
@@ -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
+