init
This commit is contained in:
commit
883f31932e
169 changed files with 5676 additions and 0 deletions
18
ansible/roles/prosody/templates/docker-compose.yml.j2
Normal file
18
ansible/roles/prosody/templates/docker-compose.yml.j2
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
services:
|
||||
prosody:
|
||||
image: prosodyim/prosody:{{ version }}
|
||||
network_mode: host
|
||||
volumes:
|
||||
- prosody_data:/var/lib/prosody
|
||||
- ./prosody.cfg.lua:/etc/prosody/prosody.cfg.lua:ro
|
||||
{% if oauth_client_id is defined or default_contacts is defined %}
|
||||
- ./modules:/usr/lib/prosody/custom-modules:ro
|
||||
{% endif %}
|
||||
{% if ssl_cert is defined %}
|
||||
- {{ ssl_cert }}:/etc/prosody/certs/fullchain.pem:ro
|
||||
- {{ ssl_key }}:/etc/prosody/certs/privkey.pem:ro
|
||||
{% endif %}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
prosody_data:
|
||||
40
ansible/roles/prosody/templates/mod_default_contacts.lua.j2
Normal file
40
ansible/roles/prosody/templates/mod_default_contacts.lua.j2
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
local rostermanager = require "core.rostermanager";
|
||||
local host = module.host;
|
||||
|
||||
local default_contacts = module:get_option("default_contacts", {});
|
||||
|
||||
module:hook("resource-bind", function(event)
|
||||
local user = event.session.username;
|
||||
local user_jid = user .. "@" .. host;
|
||||
local roster = rostermanager.load_roster(user, host);
|
||||
|
||||
for _, contact in ipairs(default_contacts) do
|
||||
local contact_user = contact.jid:match("^([^@]+)@");
|
||||
if contact_user ~= user and not roster[contact.jid] then
|
||||
-- Add contact to this user's roster
|
||||
local groups = {};
|
||||
for _, gname in ipairs(contact.groups or {}) do
|
||||
groups[gname] = true;
|
||||
end
|
||||
roster[contact.jid] = {
|
||||
subscription = "both";
|
||||
name = contact.name;
|
||||
groups = groups;
|
||||
};
|
||||
rostermanager.save_roster(user, host);
|
||||
rostermanager.roster_push(user, host, contact.jid);
|
||||
|
||||
-- Add this user to the contact's roster (for bidirectional presence)
|
||||
local contact_roster = rostermanager.load_roster(contact_user, host);
|
||||
if contact_roster and not contact_roster[user_jid] then
|
||||
contact_roster[user_jid] = {
|
||||
subscription = "both";
|
||||
name = user;
|
||||
groups = {};
|
||||
};
|
||||
rostermanager.save_roster(contact_user, host);
|
||||
rostermanager.roster_push(contact_user, host, user_jid);
|
||||
end
|
||||
end
|
||||
end
|
||||
end);
|
||||
144
ansible/roles/prosody/templates/mod_offline_email.lua.j2
Normal file
144
ansible/roles/prosody/templates/mod_offline_email.lua.j2
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
local socket = require "socket";
|
||||
local ssl = require "ssl";
|
||||
local b64 = require "prosody.util.encodings".base64.encode;
|
||||
|
||||
local smtp_server = module:get_option_string("offline_email_smtp_server");
|
||||
local smtp_port = module:get_option_number("offline_email_smtp_port", 587);
|
||||
local smtp_username = module:get_option_string("offline_email_smtp_username");
|
||||
local smtp_password = module:get_option_string("offline_email_smtp_password");
|
||||
local smtp_from = module:get_option_string("offline_email_smtp_from", smtp_username);
|
||||
|
||||
local accounts = module:open_store("accounts");
|
||||
|
||||
local function read_response(sock)
|
||||
local lines = {};
|
||||
while true do
|
||||
local line, err = sock:receive("*l");
|
||||
if not line then return nil, err; end
|
||||
table.insert(lines, line);
|
||||
if line:sub(4, 4) == " " then break; end
|
||||
end
|
||||
local code = tonumber(lines[1]:sub(1, 3));
|
||||
return code, table.concat(lines, "\n");
|
||||
end
|
||||
|
||||
local function send_command(sock, cmd)
|
||||
sock:send(cmd .. "\r\n");
|
||||
return read_response(sock);
|
||||
end
|
||||
|
||||
local function send_email(to_email, subject, body_text)
|
||||
local sock = socket.tcp();
|
||||
sock:settimeout(10);
|
||||
|
||||
local ok, err = sock:connect(smtp_server, smtp_port);
|
||||
if not ok then return nil, "connect: " .. tostring(err); end
|
||||
|
||||
local code, resp = read_response(sock);
|
||||
if not code or code ~= 220 then
|
||||
sock:close();
|
||||
return nil, "greeting: " .. tostring(resp);
|
||||
end
|
||||
|
||||
code = send_command(sock, "EHLO localhost");
|
||||
if code ~= 250 then sock:close(); return nil, "EHLO failed"; end
|
||||
|
||||
code = send_command(sock, "STARTTLS");
|
||||
if code ~= 220 then sock:close(); return nil, "STARTTLS rejected"; end
|
||||
|
||||
sock = ssl.wrap(sock, { mode = "client", protocol = "any", verify = "none" });
|
||||
ok = sock:dohandshake();
|
||||
if not ok then sock:close(); return nil, "TLS handshake failed"; end
|
||||
|
||||
code = send_command(sock, "EHLO localhost");
|
||||
if code ~= 250 then sock:close(); return nil, "EHLO after TLS failed"; end
|
||||
|
||||
local auth_str = b64("\0" .. smtp_username .. "\0" .. smtp_password);
|
||||
code = send_command(sock, "AUTH PLAIN " .. auth_str);
|
||||
if code ~= 235 then sock:close(); return nil, "AUTH failed"; end
|
||||
|
||||
code = send_command(sock, "MAIL FROM:<" .. smtp_from .. ">");
|
||||
if code ~= 250 then sock:close(); return nil, "MAIL FROM failed"; end
|
||||
|
||||
code = send_command(sock, "RCPT TO:<" .. to_email .. ">");
|
||||
if code ~= 250 then sock:close(); return nil, "RCPT TO failed"; end
|
||||
|
||||
code = send_command(sock, "DATA");
|
||||
if code ~= 354 then sock:close(); return nil, "DATA failed"; end
|
||||
|
||||
local message = "From: " .. smtp_from .. "\r\n" ..
|
||||
"To: " .. to_email .. "\r\n" ..
|
||||
"Subject: " .. subject .. "\r\n" ..
|
||||
"Content-Type: text/plain; charset=UTF-8\r\n" ..
|
||||
"\r\n" ..
|
||||
body_text .. "\r\n.\r\n";
|
||||
sock:send(message);
|
||||
code = read_response(sock);
|
||||
if code ~= 250 then sock:close(); return nil, "message rejected"; end
|
||||
|
||||
send_command(sock, "QUIT");
|
||||
sock:close();
|
||||
return true;
|
||||
end
|
||||
|
||||
local function notify_offline(to_user, stanza)
|
||||
local body = stanza:get_child_text("body");
|
||||
local is_encrypted = stanza:get_child("encrypted", "eu.siacs.conversations.axolotl") ~= nil;
|
||||
|
||||
if not is_encrypted and (not body or body == "") then return; end
|
||||
|
||||
local account = accounts:get(to_user);
|
||||
if not account or not account.email then return; end
|
||||
|
||||
local from_jid = stanza.attr.from or "unknown";
|
||||
local from_name = from_jid:match("^([^@]+)") or from_jid;
|
||||
local subject = "New message from " .. from_name;
|
||||
local email_body;
|
||||
if is_encrypted then
|
||||
email_body = "Ahoy,\r\n\r\n" ..
|
||||
"while offline, you've received an encrypted message from " .. from_name .. ".\r\n\r\n" ..
|
||||
"This message was sent using end-to-end encryption (OMEMO).\r\n" ..
|
||||
"To view it, log in from the same device where your encryption keys are stored. " ..
|
||||
"The message cannot be read on a different device or client.\r\n\r\n" ..
|
||||
"Kind regards,\r\n\r\n" ..
|
||||
"Tiara";
|
||||
else
|
||||
email_body = "Ahoy,\r\n\r\n" ..
|
||||
"while offline, you received a plain-text message from " .. from_name .. ":\r\n\r\n" ..
|
||||
">> " .. body .. "\r\n\r\n" ..
|
||||
"Kind regards,\r\n\r\n" ..
|
||||
"Tiara";
|
||||
end
|
||||
|
||||
local ok, err = send_email(account.email, subject, email_body);
|
||||
if ok then
|
||||
module:log("info", "Sent offline email notification to %s for user %s", account.email, to_user);
|
||||
else
|
||||
module:log("warn", "Failed to send offline email to %s: %s", account.email, tostring(err));
|
||||
end
|
||||
end
|
||||
|
||||
local bare_sessions = prosody.bare_sessions;
|
||||
local jid = require "util.jid";
|
||||
|
||||
local function has_hibernating_session(username)
|
||||
local bare = username .. "@" .. module.host;
|
||||
local user = bare_sessions[bare];
|
||||
if not user then return false; end
|
||||
for _, session in pairs(user.sessions) do
|
||||
if session.hibernating then return true; end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
-- Fired when user is offline (no sessions) or when smacks gives up on a hibernating session
|
||||
module:hook("message/offline/handle", function(event)
|
||||
local to_user = event.username or (event.stanza.attr.to and event.stanza.attr.to:match("^([^@]+)"));
|
||||
if not to_user then return; end
|
||||
-- Skip if smacks is still hibernating — we'll get called again when it expires
|
||||
if has_hibernating_session(to_user) then
|
||||
module:log("debug", "Skipping email for %s — smacks session still hibernating", to_user);
|
||||
return;
|
||||
end
|
||||
notify_offline(to_user, event.stanza);
|
||||
end, 1);
|
||||
111
ansible/roles/prosody/templates/prosody.cfg.lua.j2
Normal file
111
ansible/roles/prosody/templates/prosody.cfg.lua.j2
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
admins = { "{{ admin_jid }}" }
|
||||
|
||||
{% if oauth_client_id is defined or default_contacts is defined or smtp_host is defined or session_timeout is defined %}
|
||||
plugin_paths = { "/usr/lib/prosody/custom-modules" }
|
||||
{% endif %}
|
||||
|
||||
modules_enabled = {
|
||||
"roster";
|
||||
"saslauth";
|
||||
"tls";
|
||||
"dialback";
|
||||
"disco";
|
||||
"carbons";
|
||||
"pep";
|
||||
"private";
|
||||
"blocklist";
|
||||
"vcard4";
|
||||
"vcard_legacy";
|
||||
"version";
|
||||
"uptime";
|
||||
"time";
|
||||
"ping";
|
||||
"register";
|
||||
"admin_adhoc";
|
||||
"bosh";
|
||||
"websocket";
|
||||
"smacks";
|
||||
"csi_simple";
|
||||
"mam";
|
||||
{% if oauth_client_id is defined %}
|
||||
"sasl_oauthbearer_only_bosh";
|
||||
{% endif %}
|
||||
{% if session_timeout is defined %}
|
||||
"session_timeout";
|
||||
{% endif %}
|
||||
{% if smtp_host is defined %}
|
||||
"offline";
|
||||
"offline_email";
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
allow_registration = false
|
||||
|
||||
http_ports = { 5280 }
|
||||
http_interfaces = { "127.0.0.1" }
|
||||
|
||||
https_ports = {}
|
||||
|
||||
proxy65_ports = { {{ proxy65_port }} }
|
||||
|
||||
consider_bosh_secure = true
|
||||
consider_websocket_secure = true
|
||||
|
||||
VirtualHost "{{ domain }}"
|
||||
{% if oauth_client_id is defined %}
|
||||
authentication = "oauth_external"
|
||||
oauth_external_validation_endpoint = "{{ oauth_userinfo_url }}"
|
||||
oauth_external_username_field = "preferred_username"
|
||||
oauth_external_client_id = "{{ oauth_ropc_client_id | default(oauth_client_id) }}"
|
||||
{% if oauth_ropc_client_secret is defined %}
|
||||
oauth_external_client_secret = "{{ oauth_ropc_client_secret }}"
|
||||
oauth_external_token_endpoint = "{{ oauth_token_url }}"
|
||||
oauth_external_resource_owner_password = true
|
||||
oauth_external_scope = "openid profile email"
|
||||
{% endif %}
|
||||
{% else %}
|
||||
authentication = "internal_hashed"
|
||||
{% endif %}
|
||||
{% if session_timeout is defined %}
|
||||
session_timeout = {{ session_timeout }}
|
||||
{% endif %}
|
||||
{% if smtp_host is defined %}
|
||||
offline_email_smtp_server = "{{ smtp_host }}"
|
||||
offline_email_smtp_port = {{ smtp_port | default(587) }}
|
||||
offline_email_smtp_username = "{{ smtp_username }}"
|
||||
offline_email_smtp_password = "{{ smtp_password }}"
|
||||
offline_email_smtp_from = "{{ smtp_from | default(smtp_username) }}"
|
||||
{% endif %}
|
||||
{% if default_contacts is defined %}
|
||||
modules_enabled = { "default_contacts" }
|
||||
default_contacts = {
|
||||
{% for contact in default_contacts %}
|
||||
{ jid = "{{ contact.jid }}"; name = "{{ contact.name }}"; groups = { "{{ contact.group | default('Contacts') }}" } };
|
||||
{% endfor %}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if ssl_cert is defined %}
|
||||
ssl = {
|
||||
certificate = "/etc/prosody/certs/fullchain.pem";
|
||||
key = "/etc/prosody/certs/privkey.pem";
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
Component "conference.{{ domain }}" "muc"
|
||||
modules_enabled = { "muc_mam" }
|
||||
restrict_room_creation = true
|
||||
muc_room_default_public = false
|
||||
muc_room_default_members_only = true
|
||||
muc_room_default_change_subject = true
|
||||
muc_room_default_history_length = 50
|
||||
muc_room_locking = false
|
||||
|
||||
Component "upload.{{ domain }}" "http_file_share"
|
||||
http_file_share_size_limit = {{ http_upload_file_size_limit }}
|
||||
http_file_share_expires_after = {{ http_upload_expire_after }}
|
||||
http_host = "upload.{{ domain }}"
|
||||
http_external_url = "https://upload.{{ domain }}"
|
||||
|
||||
Component "proxy.{{ domain }}" "proxy65"
|
||||
proxy65_address = "{{ proxy65_address }}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue