lijab 0.1.1

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