This commit is contained in:
Tiara Rodney 2026-03-14 05:38:45 +01:00
commit 883f31932e
No known key found for this signature in database
GPG key ID: 5CD8EC1D46106723
169 changed files with 5676 additions and 0 deletions

View 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:

View 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);

View 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);

View 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 }}"