clix.lua

1
#!/usr/bin/env lua
2
-- Clix -- Command-line XMPP
3
-- Copyright (C) 2008-2010 Matthew Wild
4
--
5
-- This project is MIT/X11 licensed. Please see the
6
-- COPYING file in the source package for more information.
7
--
8
local verse = require "verse".init "client"
9
 
10
-- Global to allow commands to add to it
11
short_opts = { v = "verbose", q = "quiet", t = "to", f = "from", e = "type",
12
	a = "account", p = "password", r = "resource", o = "presence", c = "chatroom", i = "interactive",
13
    f = "file" }
14
 
15
 
16
local commands = "adhoc archive avatar bounce export mirror moderate ping\
17
presence publish_atom raw receive roster send sendfile sendfilecontent vcard\
18
version watch_pep"
19
if #arg < 1 then
20
	print("Command Line XMPP, available commands:");
21
	for command in commands:gmatch("%S+") do
22
		local m = require("clix."..command);
23
		io.write("\t", command, ": ");
24
		m({ short_help = true }, {});
25
	end
26
	return 0;
27
end
28
 
29
local accounts = { default = {} };
30
local current_account;
31
 
32
local config_path = (os.getenv("XDG_CONFIG_HOME") or (os.getenv("HOME").."/.config"));
33
local config_file, err = io.open(config_path.."/.clixrc") or io.open(config_path.."/.clix");
34
 
35
if not config_file then
36
	print("Unable to open config file... looked for "..config_path.."/.clixrc");
37
	if err then print(err); end
38
	os.exit(1);
39
end
40
 
41
for line in config_file:lines() do
42
	line = line:match("^%s*(.-)%s*$");
43
	if line:match("^%[") then
44
		current_account = line:match("^%[(.-)%]");
45
		accounts[current_account] = {};
46
		if not current_account then -- This is the first defined account
47
			accounts.default = accounts[current_account];
48
		end
49
	elseif current_account then
50
		local k,v = line:match("^(%w+)%s*[:=]%s*(.+)$");
51
		if k then
52
			accounts[current_account or "default"][k] = v;
53
		end
54
	end
55
end
56
 
57
local function read_password()
58
	io.write("Password: ")
59
	local stty_ret = os.execute("stty -echo 2>/dev/null");
60
	if stty_ret ~= 0 then
61
		io.write("\027[08m"); -- ANSI 'hidden' text attribute
62
	end
63
	local ok, pass = pcall(io.read, "*l");
64
	if stty_ret == 0 then
65
		os.execute("stty sane");
66
	else
67
		io.write("\027[00m");
68
	end
69
	io.write("\n");
70
	if ok then return pass; end
71
end
72
 
73
function clix_connect(opts, on_connect, plugins)
74
	local account = accounts[opts.account or "default"];
75
	if not (account and account.jid) then
76
		io.stderr:write("The specified account (", opts.account or "default", ") wasn't found in the config file\n");
77
		return nil;
78
	end
79
	verse.set_log_handler(io.stderr, opts.quiet and {} or not opts.verbose and {"info", "warn", "error"});
80
	local conn = verse.new(verse.new_logger("clix"));
81
	if opts.raw then
82
		conn:hook("incoming-raw", print, 1000);
83
		conn:hook("outgoing-raw", print, 1000);
84
	end
85
	for _, plugin in ipairs(plugins or {}) do
86
		conn:add_plugin(plugin);
87
	end
88
	conn:hook("authentication-failure", function (err)
89
		conn:error("Authentication failure ("..(err.condition or "unknown error")..")"..(err.text and (": "..err.text) or ""));
90
		conn:close();
91
	end);
92
	conn:hook("ready", function ()
93
		conn:info("Connected as %s", conn.jid);
94
		if opts.chatroom then
95
			conn:send(verse.presence{to=opts.to.."/"..(opts.nick or "clix")});
96
		end
97
		if opts.presence then
98
			conn:send(verse.presence());
99
		end
100
		return on_connect(conn);
101
	end);
102
	conn:hook("bind-failure", function (err)
103
		conn:error("Resource binding failure ("..(err.condition or "unknown error")..")"..(err.text and (": "..err.text) or ""));
104
		conn:close();
105
	end);
106
	conn:hook("disconnected", function (info)
107
		if info and info.reason and info.reason ~= "stream closed"  then
108
			conn:warn("Disconnecting: %s", tostring(info.reason));
109
		end
110
		verse.quit();
111
	end);
112
	-- Optional config parameters
113
	conn.connect_host = account.address;
114
	conn.connect_port = account.port;
115
 
116
	if not account.password and not (account.clientkey and account.serverkey) and opts.interactive then
117
		account.password = read_password()
118
	end
119
 
120
	local clientkey, serverkey = account.clientkey, account.serverkey;
121
	if clientkey and serverkey then
122
		local hex = require "prosody.util.hex";
123
		clientkey = hex.from(clientkey);
124
		serverkey = hex.from(serverkey);
125
	elseif clientkey or serverkey then
126
		conn:warn("Only one of 'clientkey' and 'serverkey' available, both reqired for SCRAM")
127
		clientkey, serverkey = nil, nil;
128
	end
129
	
130
	-- Connect!
131
	conn:connect_client(account.jid, account.password, clientkey, serverkey);
132
	if type(opts.resource) == "string" then
133
		conn.resource = opts.resource;
134
	end
135
	
136
	local ok, ret = pcall(verse.loop);
137
	if not ok and not ret:match("interrupted!$") then
138
		io.stderr:write("Fatal error: ", ret, "\n");
139
		return 1;
140
	end
141
	return err or 0;
142
end
143
 
144
local opts = {};
145
 
146
local command, args_handled_up_to;
147
for i, opt in ipairs(arg) do
148
	if opt:match("^%-") and opt ~= "--" then
149
		local name = opt:match("^%-%-?([^%s=]+)()")
150
		name = (short_opts[name] or name):gsub("%-+", "_");
151
		if name:match("^no_") then
152
			name = name:sub(4, -1);
153
			opts[name] = false;
154
		else
155
			opts[name] = opt:match("=(.*)$") or true;
156
		end
157
	elseif not command then
158
		command = arg[i];
159
		args_handled_up_to = i-1;
160
	else
161
		args_handled_up_to = i-1;
162
		break;
163
	end
164
end
165
 
166
-- Remove all the handled args from the arg array
167
for n=((args_handled_up_to > 0) and args_handled_up_to or #arg),1,-1 do
168
	table.remove(arg, n);
169
end
170
 
171
local ok, m = pcall(require, "clix."..command);
172
if not ok then
173
	local is_debug;
174
	for i=1,#arg do
175
		if arg[i] == "--debug" then
176
			is_debug = true; break;
177
		end
178
	end
179
	print("Error running command '"..command..(is_debug and "" or "' (run with --debug to see full error)"));
180
	if is_debug then
181
		print(m);
182
	end
183
	return 1;
184
end
185
 
186
if type(m) ~= "function" then
187
	print(command.." is not a valid command");
188
	return 1;
189
end
190
 
191
return m(opts, arg) or 0;