Software /
code /
prosody-modules
Comparison
mod_ircd/mod_ircd.lua @ 326:282abba844e8
mod_ircd: Partial rewrite, use verse for MUC
author | Kim Alvefur <zash@zash.se> |
---|---|
date | Wed, 02 Feb 2011 23:52:31 +0100 |
parent | 282:e5cc0fe5eec3 |
child | 329:febfb59502fc |
comparison
equal
deleted
inserted
replaced
325:4e50e591a7fc | 326:282abba844e8 |
---|---|
1 -- README | |
2 -- Squish verse into this dir, then squish them into one, which you move | |
3 -- and rename to mod_ircd.lua in your prosody modules/plugins dir. | |
4 -- | |
5 -- IRC spec: | |
6 -- http://tools.ietf.org/html/rfc2812 | |
7 local _module = module | |
8 module = _G.module | |
9 local module = _module | |
10 -- | |
11 local component_jid, component_secret, muc_server = | |
12 module.host, nil, module:get_option("conference_server"); | |
13 | |
14 package.loaded["util.sha1"] = require "util.encodings"; | |
15 local verse = require "verse" | |
16 require "verse.component" | |
17 require "socket" | |
18 c = verse.new();--verse.logger()) | |
19 c:add_plugin("groupchat"); | |
20 | |
21 local function verse2prosody(e) | |
22 return c:event("stanza", e.stanza) or true; | |
23 end | |
24 module:hook("message/bare", verse2prosody); | |
25 module:hook("message/full", verse2prosody); | |
26 module:hook("presence/bare", verse2prosody); | |
27 module:hook("presence/full", verse2prosody); | |
28 c.type = "component"; | |
29 c.send = core_post_stanza; | |
30 | |
31 -- This plugin is actually a verse based component, but that mode is currently commented out | |
32 | |
33 -- Add some hooks for debugging | |
34 --c:hook("opened", function () print("Stream opened!") end); | |
35 --c:hook("closed", function () print("Stream closed!") end); | |
36 --c:hook("stanza", function (stanza) print("Stanza:", stanza) end); | |
37 | |
38 -- This one prints all received data | |
39 --c:hook("incoming-raw", print, 1000); | |
40 --c:hook("stanza", print, 1000); | |
41 --c:hook("outgoing-raw", print, 1000); | |
42 | |
43 -- Print a message after authentication | |
44 --c:hook("authentication-success", function () print("Logged in!"); end); | |
45 --c:hook("authentication-failure", function (err) print("Failed to log in! Error: "..tostring(err.condition)); end); | |
46 | |
47 -- Print a message and exit when disconnected | |
48 --c:hook("disconnected", function () print("Disconnected!"); os.exit(); end); | |
49 | |
50 -- Now, actually start the connection: | |
51 --c.connect_host = "127.0.0.1" | |
52 --c:connect_component(component_jid, component_secret); | |
53 | |
54 local jid = require "util.jid"; | |
55 | |
56 local function irc2muc(channel, nick) | |
57 return jid.join(channel:gsub("^#", ""), muc_server, nick) | |
58 end | |
59 local function muc2irc(room) | |
60 local channel, _, nick = jid.split(room); | |
61 return "#"..channel, nick; | |
62 end | |
63 | |
1 local irc_listener = { default_port = 6667, default_mode = "*l" }; | 64 local irc_listener = { default_port = 6667, default_mode = "*l" }; |
2 | 65 |
3 local sessions = {}; | 66 local sessions = {}; |
67 local jids = {}; | |
4 local commands = {}; | 68 local commands = {}; |
5 | 69 |
6 local nicks = {}; | 70 local nicks = {}; |
7 | 71 |
8 local st = require "util.stanza"; | 72 local st = require "util.stanza"; |
9 | 73 |
10 local conference_server = module:get_option("conference_server") or "conference.jabber.org"; | 74 local conference_server = muc_server; |
11 | 75 |
12 local function irc_close_session(session) | 76 local function irc_close_session(session) |
13 session.conn:close(); | 77 session.conn:close(); |
14 end | 78 end |
15 | 79 |
16 function irc_listener.onincoming(conn, data) | 80 function irc_listener.onincoming(conn, data) |
17 local session = sessions[conn]; | 81 local session = sessions[conn]; |
18 if not session then | 82 if not session then |
19 session = { conn = conn, host = module.host, reset_stream = function () end, | 83 session = { conn = conn, host = component_jid, reset_stream = function () end, |
20 close = irc_close_session, log = logger.init("irc"..(conn.id or "1")), | 84 close = irc_close_session, log = logger.init("irc"..(conn.id or "1")), |
85 rooms = {}, | |
21 roster = {} }; | 86 roster = {} }; |
22 sessions[conn] = session; | 87 sessions[conn] = session; |
23 function session.data(data) | 88 function session.data(data) |
24 module:log("debug", "Received: %s", data); | |
25 local command, args = data:match("^%s*([^ ]+) *(.*)%s*$"); | 89 local command, args = data:match("^%s*([^ ]+) *(.*)%s*$"); |
26 if not command then | 90 if not command then |
27 module:log("warn", "Invalid command: %s", data); | |
28 return; | 91 return; |
29 end | 92 end |
30 command = command:upper(); | 93 command = command:upper(); |
31 module:log("debug", "Received command: %s", command); | |
32 if commands[command] then | 94 if commands[command] then |
33 local ret = commands[command](session, args); | 95 local ret = commands[command](session, args); |
34 if ret then | 96 if ret then |
35 session.send(ret.."\r\n"); | 97 session.send(ret.."\r\n"); |
36 end | 98 end |
99 else | |
100 module:log("debug", "Unknown command: %s", command); | |
37 end | 101 end |
38 end | 102 end |
39 function session.send(data) | 103 function session.send(data) |
40 module:log("debug", "sending: %s", data); | |
41 return conn:write(data.."\r\n"); | 104 return conn:write(data.."\r\n"); |
42 end | 105 end |
43 end | 106 end |
44 if data then | 107 if data then |
45 session.data(data); | 108 session.data(data); |
46 end | 109 end |
47 end | 110 end |
48 | 111 |
49 function irc_listener.ondisconnect(conn, error) | 112 function irc_listener.ondisconnect(conn, error) |
50 module:log("debug", "Client disconnected"); | 113 local session = sessions[conn]; |
114 for _, room in pairs(session.rooms) do | |
115 room:leave("Disconnected"); | |
116 end | |
51 sessions[conn] = nil; | 117 sessions[conn] = nil; |
52 end | 118 end |
53 | 119 |
54 function commands.NICK(session, nick) | 120 function commands.NICK(session, nick) |
121 if session.nick then | |
122 session.send(":"..session.host.." 484 * "..nick.." :I'm afraid I can't let you do that, "..nick); | |
123 return; | |
124 end | |
55 nick = nick:match("^[%w_]+"); | 125 nick = nick:match("^[%w_]+"); |
56 if nicks[nick] then | 126 if nicks[nick] then |
57 session.send(":"..session.host.." 433 * "..nick.." :The nickname "..nick.." is already in use"); | 127 session.send(":"..session.host.." 433 * "..nick.." :The nickname "..nick.." is already in use"); |
58 return; | 128 return; |
59 end | 129 end |
130 local full_jid = jid.join(nick, component_jid, "ircd"); | |
131 jids[full_jid] = session; | |
60 nicks[nick] = session; | 132 nicks[nick] = session; |
61 session.nick = nick; | 133 session.nick = nick; |
62 session.full_jid = nick.."@"..module.host.."/ircd"; | 134 session.full_jid = full_jid; |
63 session.type = "c2s"; | 135 session.type = "c2s"; |
64 module:log("debug", "Client bound to %s", session.full_jid); | |
65 session.send(":"..session.host.." 001 "..session.nick.." :Welcome to XMPP via the "..session.host.." gateway "..session.nick); | 136 session.send(":"..session.host.." 001 "..session.nick.." :Welcome to XMPP via the "..session.host.." gateway "..session.nick); |
66 end | 137 end |
67 | 138 |
68 local joined_mucs = {}; | 139 local joined_mucs = {}; |
69 function commands.JOIN(session, channel) | 140 function commands.JOIN(session, channel) |
141 if not session.nick then | |
142 return ":"..session.host.." 451 :You have not registered"; | |
143 end | |
144 | |
70 if not joined_mucs[channel] then | 145 if not joined_mucs[channel] then |
71 joined_mucs[channel] = { occupants = {}, sessions = {} }; | 146 joined_mucs[channel] = { occupants = {}, sessions = {} }; |
72 end | 147 end |
73 joined_mucs[channel].sessions[session] = true; | 148 joined_mucs[channel].sessions[session] = true; |
74 local join_stanza = st.presence({ from = session.full_jid, to = channel:gsub("^#", "").."@"..conference_server.."/"..session.nick }); | 149 local room_jid = irc2muc(channel); |
75 core_process_stanza(session, join_stanza); | 150 print(session.full_jid); |
151 local room, err = c:join_room(room_jid, session.nick, { source = session.full_jid } ); | |
152 if not room then | |
153 return ":"..session.host.." ERR :Could not join room: "..err | |
154 end | |
155 session.rooms[channel] = room; | |
156 room.channel = channel; | |
157 room.session = session; | |
76 session.send(":"..session.nick.." JOIN :"..channel); | 158 session.send(":"..session.nick.." JOIN :"..channel); |
77 session.send(":"..session.host.." 332 "..session.nick.." "..channel.." :Connection in progress..."); | 159 session.send(":"..session.host.." 332 "..session.nick.." "..channel.." :Connection in progress..."); |
78 local nicks = session.nick; | 160 room:hook("message", function(event) |
79 for nick in pairs(joined_mucs[channel].occupants) do | 161 if not event.body then return end |
80 nicks = nicks.." "..nick; | 162 local nick, body = event.nick, event.body; |
81 end | 163 if nick ~= session.nick then |
82 session.send(":"..session.host.." 353 "..session.nick.." = "..channel.." :"..nicks); | 164 if body:sub(1,4) == "/me " then |
165 body = "\1ACTION ".. body:sub(5) .. "\1" | |
166 end | |
167 session.send(":"..nick.." PRIVMSG "..channel.." :"..body); | |
168 end | |
169 end); | |
170 end | |
171 | |
172 c:hook("groupchat/joined", function(room) | |
173 local session = room.session or jids[room.opts.source]; | |
174 local channel = muc2irc(room.jid); | |
175 local nicks = session.nick; | |
176 -- TODO Break this out into commands.NAMES | |
177 for nick in pairs(room.occupants) do | |
178 if nick ~= session.nick then | |
179 nicks = nicks.." "..nick; | |
180 end | |
181 end | |
83 session.send(":"..session.host.." 366 "..session.nick.." "..channel.." :End of /NAMES list."); | 182 session.send(":"..session.host.." 366 "..session.nick.." "..channel.." :End of /NAMES list."); |
84 end | 183 session.send(":"..session.host.." 353 "..session.nick.." = "..channel.." :"..nicks); |
184 room:hook("occupant-joined", function(nick) | |
185 session.send(":"..nick.nick.."!"..nick.nick.." JOIN :"..room.channel); | |
186 end); | |
187 room:hook("occupant-left", function(nick) | |
188 session.send(":"..nick.nick.."!"..nick.nick.." PART "..room.channel.." :"); | |
189 end); | |
190 end); | |
85 | 191 |
86 function commands.PART(session, channel) | 192 function commands.PART(session, channel) |
87 local channel, part_message = channel:match("^([^:]+):?(.*)$"); | 193 local channel, part_message = channel:match("^([^:]+):?(.*)$"); |
88 channel = channel:match("^([%S]*)"); | 194 channel = channel:match("^([%S]*)"); |
89 core_process_stanza(session, st.presence{ type = "unavailable", from = session.full_jid, | 195 session.rooms[channel]:leave(part_message); |
90 to = channel:gsub("^#", "").."@"..conference_server.."/"..session.nick }:tag("status"):text(part_message)); | |
91 session.send(":"..session.nick.." PART :"..channel); | 196 session.send(":"..session.nick.." PART :"..channel); |
92 end | 197 end |
93 | 198 |
94 function commands.PRIVMSG(session, message) | 199 function commands.PRIVMSG(session, message) |
95 local who, message = message:match("^(%S+) :(.+)$"); | 200 local channel, message = message:match("^(%S+) :(.+)$"); |
96 if joined_mucs[who] then | 201 if message and #message > 0 and session.rooms[channel] then |
97 core_process_stanza(session, st.message{to=who:gsub("^#", "").."@"..conference_server, type="groupchat"}:tag("body"):text(message)); | 202 if message:sub(1,8) == "\1ACTION " then |
203 message = "/me ".. message:sub(9,-2) | |
204 end | |
205 module:log("debug", "%s sending PRIVMSG \"%s\" to %s", session.nick, message, channel); | |
206 session.rooms[channel]:send_message(message); | |
98 end | 207 end |
99 end | 208 end |
100 | 209 |
101 function commands.PING(session, server) | 210 function commands.PING(session, server) |
102 session.send(":"..session.host..": PONG "..server); | 211 session.send(":"..session.host..": PONG "..server); |
103 end | 212 end |
104 | 213 |
105 function commands.WHO(session, channel) | 214 function commands.WHO(session, channel) |
106 if joined_mucs[channel] then | 215 if session.rooms[channel] then |
107 for nick in pairs(joined_mucs[channel].occupants) do | 216 local room = session.rooms[channel] |
217 for nick in pairs(room.occupants) do | |
108 --n=MattJ 91.85.191.50 irc.freenode.net MattJ H :0 Matthew Wild | 218 --n=MattJ 91.85.191.50 irc.freenode.net MattJ H :0 Matthew Wild |
109 session.send(":"..session.host.." 352 "..session.nick.." "..channel.." "..nick.." "..nick.." "..session.host.." "..nick.." H :0 "..nick); | 219 session.send(":"..session.host.." 352 "..session.nick.." "..channel.." "..nick.." "..nick.." "..session.host.." "..nick.." H :0 "..nick); |
110 end | 220 end |
111 session.send(":"..session.host.." 315 "..session.nick.." "..channel.. " :End of /WHO list"); | 221 session.send(":"..session.host.." 315 "..session.nick.." "..channel.. " :End of /WHO list"); |
112 end | 222 end |
114 | 224 |
115 function commands.MODE(session, channel) | 225 function commands.MODE(session, channel) |
116 session.send(":"..session.host.." 324 "..session.nick.." "..channel.." +J"); | 226 session.send(":"..session.host.." 324 "..session.nick.." "..channel.." +J"); |
117 end | 227 end |
118 | 228 |
119 --- Component (handle stanzas from the server for IRC clients) | 229 |
120 function irc_component(origin, stanza) | 230 function commands.RAW(session, data) |
121 local from, from_bare = stanza.attr.from, jid.bare(stanza.attr.from); | 231 --c:send(data) |
122 local from_node = "#"..jid.split(stanza.attr.from); | 232 end |
123 | 233 |
124 if joined_mucs[from_node] and from_bare == from then | 234 --c:hook("ready", function () |
125 -- From room itself | 235 require "net.connlisteners".register("irc", irc_listener); |
126 local joined_muc = joined_mucs[from_node]; | 236 require "net.connlisteners".start("irc"); |
127 if stanza.name == "message" then | 237 --end); |
128 local subject = stanza:get_child("subject"); | 238 |
129 subject = subject and (subject:get_text() or ""); | 239 --print("Starting loop...") |
130 if subject then | 240 --verse.loop() |
131 for session in pairs(joined_muc.sessions) do | 241 |
132 session.send(":"..session.host.." 332 "..session.nick.." "..from_node.." :"..subject); | 242 --[[ TODO |
133 end | 243 |
134 end | 244 This is so close to working as a Prosody plugin you know ^^ |
135 end | 245 Zash: :D |
136 elseif joined_mucs[from_node] then | 246 MattJ: That component function can go |
137 -- From room occupant | 247 Prosody fires events now |
138 local joined_muc = joined_mucs[from_node]; | 248 but verse fires "message" where Prosody fires "message/bare" |
139 local nick = select(3, jid.split(from)):gsub(" ", "_"); | 249 [20:59:50] |
140 if stanza.name == "presence" then | 250 Easy... don't connect_component |
141 local what; | 251 hook "message/*" and presence, and whatever |
142 if not stanza.attr.type then | 252 and call c:event("message", ...) |
143 if joined_muc.occupants[nick] then | 253 module:hook("message/bare", function (e) c:event("message", e.stanza) end) |
144 return; | 254 as an example |
145 end | 255 That's so bad ^^ |
146 joined_muc.occupants[nick] = true; | 256 and override c:send() to core_post_stanza... |
147 what = "JOIN"; | 257 |
148 else | 258 --]] |
149 joined_muc.occupants[nick] = nil; | 259 |
150 what = "PART"; | |
151 end | |
152 for session in pairs(joined_muc.sessions) do | |
153 if nick ~= session.nick then | |
154 session.send(":"..nick.."!"..nick.." "..what.." :"..from_node); | |
155 end | |
156 end | |
157 elseif stanza.name == "message" then | |
158 local body = stanza:get_child("body"); | |
159 body = body and body:get_text() or ""; | |
160 local hasdelay = stanza:get_child("delay", "urn:xmpp:delay"); | |
161 if body ~= "" and nick then | |
162 local to_nick = jid.split(stanza.attr.to); | |
163 local session = nicks[to_nick]; | |
164 if nick ~= session.nick or hasdelay then | |
165 session.send(":"..nick.." PRIVMSG "..from_node.." :"..body); | |
166 end | |
167 end | |
168 if not nick then | |
169 module:log("error", "Invalid nick from JID: %s", from); | |
170 end | |
171 end | |
172 end | |
173 end | |
174 | |
175 local function stanza_handler(event) | |
176 irc_component(event.origin, event.stanza); | |
177 return true; | |
178 end | |
179 module:hook("iq/bare", stanza_handler, -1); | |
180 module:hook("message/bare", stanza_handler, -1); | |
181 module:hook("presence/bare", stanza_handler, -1); | |
182 module:hook("iq/full", stanza_handler, -1); | |
183 module:hook("message/full", stanza_handler, -1); | |
184 module:hook("presence/full", stanza_handler, -1); | |
185 module:hook("iq/host", stanza_handler, -1); | |
186 module:hook("message/host", stanza_handler, -1); | |
187 module:hook("presence/host", stanza_handler, -1); | |
188 require "core.componentmanager".register_component(module.host, function() end); -- COMPAT Prosody 0.7 | |
189 | |
190 prosody.events.add_handler("server-stopping", function (shutdown) | |
191 module:log("debug", "Closing IRC connections prior to shutdown"); | |
192 for channel, joined_muc in pairs(joined_mucs) do | |
193 for session in pairs(joined_muc.sessions) do | |
194 core_process_stanza(session, | |
195 st.presence{ type = "unavailable", from = session.full_jid, | |
196 to = channel:gsub("^#", "").."@"..conference_server.."/"..session.nick } | |
197 :tag("status") | |
198 :text("Connection closed: Server is shutting down"..(shutdown.reason and (": "..shutdown.reason) or ""))); | |
199 session:close(); | |
200 end | |
201 end | |
202 end); | |
203 | |
204 require "net.connlisteners".register("irc", irc_listener); | |
205 require "net.connlisteners".start("irc", { port = module:get_option("port") }); |