diff --git a/initdb/02_3_1_signalk_public_tables.sql b/initdb/02_3_1_signalk_public_tables.sql new file mode 100644 index 0000000..dad9132 --- /dev/null +++ b/initdb/02_3_1_signalk_public_tables.sql @@ -0,0 +1,210 @@ +--------------------------------------------------------------------------- +-- singalk db public schema tables +-- + +-- List current database +select current_database(); + +-- connect to the DB +\c signalk + +CREATE SCHEMA IF NOT EXISTS public; +COMMENT ON SCHEMA public IS 'backend functions'; + +--------------------------------------------------------------------------- +-- Table geocoders +-- +-- https://github.com/CartoDB/labs-postgresql/blob/master/workshop/plpython.md +-- +CREATE TABLE IF NOT EXISTS geocoders( + name TEXT UNIQUE, + url TEXT, + reverse_url TEXT +); +-- Description +COMMENT ON TABLE + public.geocoders + IS 'geo service nominatim url'; + +INSERT INTO geocoders VALUES +('nominatim', + NULL, + 'https://nominatim.openstreetmap.org/reverse'); + +--------------------------------------------------------------------------- +-- Tables for message template email/pushover/telegram +-- +CREATE TABLE IF NOT EXISTS email_templates( + name TEXT UNIQUE, + email_subject TEXT, + email_content TEXT, + pushover_title TEXT, + pushover_message TEXT +); +-- Description +COMMENT ON TABLE + public.email_templates + IS 'email/message templates for notifications'; + +-- with escape value, eg: E'A\nB\r\nC' +-- https://stackoverflow.com/questions/26638615/insert-line-break-in-postgresql-when-updating-text-field +-- TODO Update notification subject for log entry to 'logbook #NB ...' +INSERT INTO email_templates VALUES +('logbook', + 'New Logbook Entry', + E'Hello __RECIPIENT__,\n\nWe just wanted to let you know that you have a new entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\n\r\nSee more details at __APP_URL__/log/__LOGBOOK_LINK__\n\nHappy sailing!\nThe PostgSail Team', + 'New Logbook Entry', + E'We just wanted to let you know that you have a new entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\n\r\nSee more details at __APP_URL__/log/__LOGBOOK_LINK__\n\nHappy sailing!\nThe PostgSail Team'), +('user', + 'Welcome', + E'Hello __RECIPIENT__,\nCongratulations!\nYou successfully created an account.\nKeep in mind to register your vessel.\nHappy sailing!', + 'Welcome', + E'Hi!\nYou successfully created an account\nKeep in mind to register your vessel.\nHappy sailing!'), +('vessel', + 'New vessel', + E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.', + 'New vessel', + E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.'), +('monitor_offline', + 'Offline', + E'__BOAT__ has been offline for more than an hour\r\nFind more details at __APP_URL__/boats/\n', + 'Offline', + E'__BOAT__ has been offline for more than an hour\r\nFind more details at __APP_URL__/boats/\n'), +('monitor_online', + 'Online', + E'__BOAT__ just came online\nFind more details at __APP_URL__/boats/\n', + 'Online', + E'__BOAT__ just came online\nFind more details at __APP_URL__/boats/\n'), +('badge', + 'New Badge!', + E'Hello __RECIPIENT__,\nCongratulations! You have just unlocked a new badge: __BADGE_NAME__\nSee more details at __APP_URL__/badges\nHappy sailing!\nThe PostgSail Team', + 'New Badge!', + E'Congratulations!\nYou have just unlocked a new badge: __BADGE_NAME__\nSee more details at __APP_URL__/badges\nHappy sailing!\nThe PostgSail Team'), +('pushover', + 'Pushover integration', + E'Hello __RECIPIENT__,\nCongratulations! You have just connect your account to pushover.\n\nThe PostgSail Team', + 'Pushover integration!', + E'Congratulations!\nYou have just connect your account to pushover.\n\nThe PostgSail Team'), +('email_otp', + 'Email verification', + E'Hello __RECIPIENT__,\nPlease active your account using the following code: __OTP_CODE__.\nThe code is valid 15 minutes.\nThe PostgSail Team', + 'Email verification', + E'Congratulations!\nPlease validate your account. Check your email!'), +('telegram_otp', + 'Telegram bot', + E'Hello __RECIPIENT__,\nTo connect your account to a @postgsail_bot. Please type this verification code __OTP_CODE__ back to the bot.\nThe code is valid 15 minutes.\nThe PostgSail Team', + 'Telegram bot', + E'Congratulations!\nTo connect your account to a @postgsail_bot. Check your email!'), +('telegram_valid', + 'Telegram bot', + E'Hello __RECIPIENT__,\nCongratulations! You have just connect your account to a @postgsail_bot.\n\nThe PostgSail Team', + 'Telegram bot!', + E'Congratulations!\nYou have just connect your account to a @postgsail_bot.\n\nHappy sailing!\nThe PostgSail Team'); + +--------------------------------------------------------------------------- +-- Queue handling +-- +-- https://gist.github.com/kissgyorgy/beccba1291de962702ea9c237a900c79 +-- https://www.depesz.com/2012/06/13/how-to-send-mail-from-database/ + +-- Listen/Notify way +--create function new_logbook_entry() returns trigger as $$ +--begin +-- perform pg_notify('new_logbook_entry', NEW.id::text); +-- return NEW; +--END; +--$$ language plpgsql; + +-- table way +CREATE TABLE IF NOT EXISTS public.process_queue ( + id SERIAL PRIMARY KEY, + channel TEXT NOT NULL, + payload TEXT NOT NULL, + stored TIMESTAMP WITHOUT TIME ZONE NOT NULL, + processed TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL +); +-- Description +COMMENT ON TABLE + public.process_queue + IS 'process queue for async job'; +-- Index +CREATE INDEX ON public.process_queue (channel); +CREATE INDEX ON public.process_queue (stored); +CREATE INDEX ON public.process_queue (processed); + +-- Function process_queue helpers +create function new_account_entry_fn() returns trigger as $new_account_entry$ +begin + insert into process_queue (channel, payload, stored) values ('new_account', NEW.email, now()); + return NEW; +END; +$new_account_entry$ language plpgsql; + +create function new_account_otp_validation_entry_fn() returns trigger as $new_account_otp_validation_entry$ +begin + insert into process_queue (channel, payload, stored) values ('new_account_otp', NEW.email, now()); + return NEW; +END; +$new_account_otp_validation_entry$ language plpgsql; + +create function new_vessel_entry_fn() returns trigger as $new_vessel_entry$ +begin + insert into process_queue (channel, payload, stored) values ('new_vessel', NEW.owner_email, now()); + return NEW; +END; +$new_vessel_entry$ language plpgsql; + +--------------------------------------------------------------------------- +-- Tables Application Settings +-- https://dba.stackexchange.com/questions/27296/storing-application-settings-with-different-datatypes#27297 +-- https://stackoverflow.com/questions/6893780/how-to-store-site-wide-settings-in-a-database +-- http://cvs.savannah.gnu.org/viewvc/*checkout*/gnumed/gnumed/gnumed/server/sql/gmconfiguration.sql + +CREATE TABLE IF NOT EXISTS public.app_settings ( + name TEXT NOT NULL UNIQUE, + value TEXT NOT NULL +); +-- Description +COMMENT ON TABLE public.app_settings IS 'application settings'; +COMMENT ON COLUMN public.app_settings.name IS 'application settings name key'; +COMMENT ON COLUMN public.app_settings.value IS 'application settings value'; + +--------------------------------------------------------------------------- +-- Badges descriptions +-- TODO add contiditions +-- +CREATE TABLE IF NOT EXISTS badges( + name TEXT UNIQUE, + description TEXT +); +-- Description +COMMENT ON TABLE + public.badges + IS 'Badges descriptions'; + +INSERT INTO badges VALUES +('Helmsman', + 'Nice work logging your first sail! You are officially a helmsman now!'), +('Wake Maker', + 'Yowzers! Welcome to the 15 knot+ club ya speed demon skipper!'), +('Explorer', + 'It looks like home is where the helm is. Cheers to 10 days away from home port!'), +('Mooring Pro', + 'It takes a lot of skill to "thread that floating needle" but seems like you have mastered mooring with 10 nights on buoy!'), +('Anchormaster', + 'Hook, line and sinker, you have this anchoring thing down! 25 days on the hook for you!'), +('Traveler', + 'Who needs to fly when one can sail! You are an international sailor. À votre santé!'), +('Stormtrooper', + 'Just like the elite defenders of the Empire, here you are, our braving your own hydro-empire in windspeeds above 30kts. Nice work trooper! '), +('Club Alaska', + 'Home to the bears, glaciers, midnight sun and high adventure. Welcome to the Club Alaska Captain!'), +('Tropical Traveler', + 'Look at you with your suntan, tropical drink and southern latitude!'), +('Aloha Award', + 'Ticking off over 2300 NM across the great blue Pacific makes you the rare recipient of the Aloha Award. Well done and Aloha sailor!'), +('Tyee', + 'You made it to the Tyee Outstation, the friendliest dock in Pacific Northwest!'), +-- TODO the sea is big and the world is not limited to the US +('Mediterranean Traveler', + 'You made it trought the Mediterranean!'); diff --git a/initdb/02_3_signalk_public.sql b/initdb/02_3_2_signalk_public_functions.sql similarity index 61% rename from initdb/02_3_signalk_public.sql rename to initdb/02_3_2_signalk_public_functions.sql index 8162e2e..7a72644 100644 --- a/initdb/02_3_signalk_public.sql +++ b/initdb/02_3_2_signalk_public_functions.sql @@ -9,392 +9,10 @@ select current_database(); \c signalk CREATE SCHEMA IF NOT EXISTS public; -COMMENT ON SCHEMA public IS 'backend functions'; - ---------------------------------------------------------------------------- --- python reverse_geocode --- --- https://github.com/CartoDB/labs-postgresql/blob/master/workshop/plpython.md --- -CREATE TABLE IF NOT EXISTS geocoders( - name TEXT UNIQUE, - url TEXT, - reverse_url TEXT -); --- Description -COMMENT ON TABLE - public.geocoders - IS 'geo service nominatim url'; - -INSERT INTO geocoders VALUES -('nominatim', - NULL, - 'https://nominatim.openstreetmap.org/reverse'); - -DROP FUNCTION IF EXISTS reverse_geocode_py_fn; -CREATE OR REPLACE FUNCTION reverse_geocode_py_fn(IN geocoder TEXT, IN lon NUMERIC, IN lat NUMERIC, - OUT geo_name TEXT) -AS $reverse_geocode_py$ - import requests - - # Use the shared cache to avoid preparing the geocoder metadata - if geocoder in SD: - plan = SD[geocoder] - # A prepared statement from Python - else: - plan = plpy.prepare("SELECT reverse_url AS url FROM geocoders WHERE name = $1", ["text"]) - SD[geocoder] = plan - - # Execute the statement with the geocoder param and limit to 1 result - rv = plpy.execute(plan, [geocoder], 1) - url = rv[0]['url'] - - # Validate input - if not lon or not lat: - plpy.notice('reverse_geocode_py_fn Parameters [{}] [{}]'.format(lon, lat)) - plpy.error('Error missing parameters') - return None - - # Make the request to the geocoder API - payload = {"lon": lon, "lat": lat, "format": "jsonv2", "zoom": 18} - r = requests.get(url, params=payload) - - # Return the full address or nothing if not found - if r.status_code == 200 and "name" in r.json(): - return r.json()["name"] - else: - plpy.error('Failed to received a geo full address %s', r.json()) - return 'unknow' -$reverse_geocode_py$ LANGUAGE plpython3u; --- Description -COMMENT ON FUNCTION - public.reverse_geocode_py_fn - IS 'query reverse geo service to return location name'; - ---------------------------------------------------------------------------- --- python template email/pushover --- -CREATE TABLE IF NOT EXISTS email_templates( - name TEXT UNIQUE, - email_subject TEXT, - email_content TEXT, - pushover_title TEXT, - pushover_message TEXT -); --- Description -COMMENT ON TABLE - public.email_templates - IS 'email/message templates for notifications'; - --- with escape value, eg: E'A\nB\r\nC' --- https://stackoverflow.com/questions/26638615/insert-line-break-in-postgresql-when-updating-text-field --- TODO Update notification subject for log entry to 'logbook #NB ...' -INSERT INTO email_templates VALUES -('logbook', - 'New Logbook Entry', - E'Hello __RECIPIENT__,\n\nWe just wanted to let you know that you have a new entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\n\r\nSee more details at __APP_URL__/log/__LOGBOOK_LINK__\n\nHappy sailing!\nThe PostgSail Team', - 'New Logbook Entry', - E'We just wanted to let you know that you have a new entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\n\r\nSee more details at __APP_URL__/log/__LOGBOOK_LINK__\n\nHappy sailing!\nThe PostgSail Team'), -('user', - 'Welcome', - E'Hello __RECIPIENT__,\nCongratulations!\nYou successfully created an account.\nKeep in mind to register your vessel.\nHappy sailing!', - 'Welcome', - E'Hi!\nYou successfully created an account\nKeep in mind to register your vessel.\nHappy sailing!'), -('vessel', - 'New vessel', - E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.', - 'New vessel', - E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.'), -('monitor_offline', - 'Offline', - E'__BOAT__ has been offline for more than an hour\r\nFind more details at __APP_URL__/boats/\n', - 'Offline', - E'__BOAT__ has been offline for more than an hour\r\nFind more details at __APP_URL__/boats/\n'), -('monitor_online', - 'Online', - E'__BOAT__ just came online\nFind more details at __APP_URL__/boats/\n', - 'Online', - E'__BOAT__ just came online\nFind more details at __APP_URL__/boats/\n'), -('badge', - 'New Badge!', - E'Hello __RECIPIENT__,\nCongratulations! You have just unlocked a new badge: __BADGE_NAME__\nSee more details at __APP_URL__/badges\nHappy sailing!\nThe PostgSail Team', - 'New Badge!', - E'Congratulations!\nYou have just unlocked a new badge: __BADGE_NAME__\nSee more details at __APP_URL__/badges\nHappy sailing!\nThe PostgSail Team'), -('pushover', - 'Pushover integration', - E'Hello __RECIPIENT__,\nCongratulations! You have just connect your account to pushover.\n\nThe PostgSail Team', - 'Pushover integration!', - E'Congratulations!\nYou have just connect your account to pushover.\n\nThe PostgSail Team'), -('email_otp', - 'Email verification', - E'Hello __RECIPIENT__,\nPlease active your account using the following code: __OTP_CODE__.\nThe code is valid 15 minutes.\nThe PostgSail Team', - 'Email verification', - E'Congratulations!\nPlease validate your account. Check your email!'), -('telegram_otp', - 'Telegram bot', - E'Hello __RECIPIENT__,\nTo connect your account to a @postgsail_bot. Please type this verification code __OTP_CODE__ back to the bot.\nThe code is valid 15 minutes.\nThe PostgSail Team', - 'Telegram bot', - E'Congratulations!\nTo connect your account to a @postgsail_bot. Check your email!'), -('telegram_valid', - 'Telegram bot', - E'Hello __RECIPIENT__,\nCongratulations! You have just connect your account to a @postgsail_bot.\n\nThe PostgSail Team', - 'Telegram bot!', - E'Congratulations!\nYou have just connect your account to a @postgsail_bot.\n\nHappy sailing!\nThe PostgSail Team'); - ---------------------------------------------------------------------------- --- python send email --- --- https://www.programcreek.com/python/example/3684/email.utils.formatdate -DROP FUNCTION IF EXISTS send_email_py_fn; -CREATE OR REPLACE FUNCTION send_email_py_fn(IN email_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void -AS $send_email_py$ - # Import smtplib for the actual sending function - import smtplib - - # Import the email modules we need - #from email.message import EmailMessage - from email.utils import formatdate,make_msgid - from email.mime.text import MIMEText - - # Use the shared cache to avoid preparing the email metadata - if email_type in SD: - plan = SD[email_type] - # A prepared statement from Python - else: - plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"]) - SD[email_type] = plan - - # Execute the statement with the email_type param and limit to 1 result - rv = plpy.execute(plan, [email_type], 1) - email_subject = rv[0]['email_subject'] - email_content = rv[0]['email_content'] - - # Replace fields using input jsonb obj - if not _user or not app: - plpy.notice('send_email_py_fn Parameters [{}] [{}]'.format(_user, app)) - plpy.error('Error missing parameters') - return None - if 'logbook_name' in _user and _user['logbook_name']: - email_content = email_content.replace('__LOGBOOK_NAME__', _user['logbook_name']) - if 'logbook_link' in _user and _user['logbook_link']: - email_content = email_content.replace('__LOGBOOK_LINK__', str(_user['logbook_link'])) - if 'recipient' in _user and _user['recipient']: - email_content = email_content.replace('__RECIPIENT__', _user['recipient']) - if 'boat' in _user and _user['boat']: - email_content = email_content.replace('__BOAT__', _user['boat']) - if 'badge' in _user and _user['badge']: - email_content = email_content.replace('__BADGE_NAME__', _user['badge']) - if 'otp_code' in _user and _user['otp_code']: - email_content = email_content.replace('__OTP_CODE__', _user['otp_code']) - - if 'app.url' in app and app['app.url']: - email_content = email_content.replace('__APP_URL__', app['app.url']) - - email_from = 'root@localhost' - if 'app.email_from' in app and app['app.email_from']: - email_from = 'PostgSail <' + app['app.email_from'] + '>' - #plpy.notice('Sending email from [{}] [{}]'.format(email_from, app['app.email_from'])) - - email_to = 'root@localhost' - if 'email' in _user and _user['email']: - email_to = _user['email'] - #plpy.notice('Sending email to [{}] [{}]'.format(email_to, _user['email'])) - else: - plpy.error('Error email to') - return None - - msg = MIMEText(email_content, 'plain', 'utf-8') - msg["Subject"] = email_subject - msg["From"] = email_from - msg["To"] = email_to - msg["Date"] = formatdate() - msg["Message-ID"] = make_msgid() - - server_smtp = 'localhost' - if 'app.email_server' in app and app['app.email_server']: - server_smtp = app['app.email_server'] - - # Send the message via our own SMTP server. - try: - # send your message with credentials specified above - with smtplib.SMTP(server_smtp, 25) as server: - if 'app.email_user' in app and app['app.email_user'] \ - and 'app.email_pass' in app and app['app.email_pass']: - server.starttls() - server.login(app['app.email_user'], app['app.email_pass']) - #server.send_message(msg) - server.sendmail(msg["From"], msg["To"], msg.as_string()) - server.quit() - # tell the script to report if your message was sent or which errors need to be fixed - plpy.notice('Sent email successfully to [{}] [{}]'.format(msg["To"], msg["Subject"])) - return None - except OSError as error: - plpy.error(error) - except smtplib.SMTPConnectError: - plpy.error('Failed to connect to the server. Bad connection settings?') - except smtplib.SMTPServerDisconnected: - plpy.error('Failed to connect to the server. Wrong user/password?') - except smtplib.SMTPException as e: - plpy.error('SMTP error occurred: ' + str(e)) -$send_email_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u; --- Description -COMMENT ON FUNCTION - public.send_email_py_fn - IS 'Send email notification using plpython3u'; - ---------------------------------------------------------------------------- --- python send pushover message --- https://pushover.net/ -DROP FUNCTION IF EXISTS send_pushover_py_fn; -CREATE OR REPLACE FUNCTION send_pushover_py_fn(IN message_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void -AS $send_pushover_py$ - import requests - - # Use the shared cache to avoid preparing the email metadata - if message_type in SD: - plan = SD[message_type] - # A prepared statement from Python - else: - plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"]) - SD[message_type] = plan - - # Execute the statement with the message_type param and limit to 1 result - rv = plpy.execute(plan, [message_type], 1) - pushover_title = rv[0]['pushover_title'] - pushover_message = rv[0]['pushover_message'] - - # Replace fields using input jsonb obj - if 'logbook_name' in _user and _user['logbook_name']: - pushover_message = pushover_message.replace('__LOGBOOK_NAME__', _user['logbook_name']) - if 'logbook_link' in _user and _user['logbook_link']: - pushover_message = pushover_message.replace('__LOGBOOK_LINK__', str(_user['logbook_link'])) - if 'recipient' in _user and _user['recipient']: - pushover_message = pushover_message.replace('__RECIPIENT__', _user['recipient']) - if 'boat' in _user and _user['boat']: - pushover_message = pushover_message.replace('__BOAT__', _user['boat']) - if 'badge' in _user and _user['badge']: - pushover_message = pushover_message.replace('__BADGE_NAME__', _user['badge']) - - if 'app.url' in app and app['app.url']: - pushover_message = pushover_message.replace('__APP_URL__', app['app.url']) - - pushover_token = None - if 'app.pushover_app_token' in app and app['app.pushover_app_token']: - pushover_token = app['app.pushover_app_token'] - else: - plpy.error('Error no pushover token defined, check app settings') - return None - pushover_user = None - if 'pushover_user_key' in _user and _user['pushover_user_key']: - pushover_user = _user['pushover_user_key'] - else: - plpy.error('Error no pushover user token defined, check user settings') - return None - - # requests - r = requests.post("https://api.pushover.net/1/messages.json", data = { - "token": pushover_token, - "user": pushover_user, - "title": pushover_title, - "message": pushover_message - }) - - #print(r.text) - # Return ?? or None if not found - plpy.notice('Sent pushover successfully to [{}] [{}]'.format(r.text, r.status_code)) - if r.status_code == 200: - plpy.notice('Sent pushover successfully to [{}] [{}] [{}]'.format("__USER__", pushover_title, r.text)) - else: - plpy.error('Failed to send pushover') - return None -$send_pushover_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u; --- Description -COMMENT ON FUNCTION - public.send_pushover_py_fn - IS 'Send pushover notification using plpython3u'; - ---------------------------------------------------------------------------- --- python send telegram message --- https://core.telegram.org/ -DROP FUNCTION IF EXISTS send_telegram_py_fn; -CREATE OR REPLACE FUNCTION send_telegram_py_fn(IN message_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void -AS $send_telegram_py$ - """ - Send a message to a telegram user or group specified on chatId - chat_id must be a number! - """ - import requests - import json - - # Use the shared cache to avoid preparing the email metadata - if message_type in SD: - plan = SD[message_type] - # A prepared statement from Python - else: - plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"]) - SD[message_type] = plan - - # Execute the statement with the message_type param and limit to 1 result - rv = plpy.execute(plan, [message_type], 1) - telegram_title = rv[0]['pushover_title'] - telegram_message = rv[0]['pushover_message'] - - # Replace fields using input jsonb obj - if 'logbook_name' in _user and _user['logbook_name']: - telegram_message = telegram_message.replace('__LOGBOOK_NAME__', _user['logbook_name']) - if 'logbook_link' in _user and _user['logbook_link']: - telegram_message = telegram_message.replace('__LOGBOOK_LINK__', str(_user['logbook_link'])) - if 'recipient' in _user and _user['recipient']: - telegram_message = telegram_message.replace('__RECIPIENT__', _user['recipient']) - if 'boat' in _user and _user['boat']: - telegram_message = telegram_message.replace('__BOAT__', _user['boat']) - if 'badge' in _user and _user['badge']: - telegram_message = telegram_message.replace('__BADGE_NAME__', _user['badge']) - - if 'app.url' in app and app['app.url']: - telegram_message = telegram_message.replace('__APP_URL__', app['app.url']) - - telegram_token = None - if 'app.telegram_bot_token' in app and app['app.telegram_bot_token']: - telegram_token = app['app.telegram_bot_token'] - else: - plpy.error('Error no telegram token defined, check app settings') - return None - telegram_chat_id = None - if 'telegram_chat_id' in _user and _user['telegram_chat_id']: - telegram_chat_id = _user['telegram_chat_id'] - else: - plpy.error('Error no telegram user token defined, check user settings') - return None - - # requests - headers = {'Content-Type': 'application/json', - 'Proxy-Authorization': 'Basic base64'} - data_dict = {'chat_id': telegram_chat_id, - 'text': telegram_message, - 'parse_mode': 'HTML', - 'disable_notification': False} - data = json.dumps(data_dict) - url = f'https://api.telegram.org/bot{telegram_token}/sendMessage' - r = requests.post(url, - data=data, - headers=headers) - print(r.text) - # Return the full address or None if not found - plpy.notice('Sent telegram successfully to [{}] [{}]'.format(r.text, r.status_code)) - if r.status_code == 200: - plpy.notice('Sent telegram successfully to [{}] [{}] [{}]'.format("__USER__", telegram_title, r.text)) - else: - plpy.error('Failed to send telegram') - return None -$send_telegram_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u; --- Description -COMMENT ON FUNCTION - public.send_telegram_py_fn - IS 'Send a message to a telegram user or group specified on chatId using plpython3u'; --------------------------------------------------------------------------- -- Functions public schema +-- process single cron event, process_[logbook|stay|moorage|badge]_queue_fn() -- -- Update a logbook with avg data @@ -986,114 +604,6 @@ COMMENT ON FUNCTION public.set_vessel_settings_from_clientid_fn IS 'set_vessel settings details from a clientid, initiate for process queue functions'; ---------------------------------------------------------------------------- --- Queue handling --- --- https://gist.github.com/kissgyorgy/beccba1291de962702ea9c237a900c79 --- https://www.depesz.com/2012/06/13/how-to-send-mail-from-database/ - --- Listen/Notify way ---create function new_logbook_entry() returns trigger as $$ ---begin --- perform pg_notify('new_logbook_entry', NEW.id::text); --- return NEW; ---END; ---$$ language plpgsql; - --- table way -CREATE TABLE IF NOT EXISTS public.process_queue ( - id SERIAL PRIMARY KEY, - channel TEXT NOT NULL, - payload TEXT NOT NULL, - stored TIMESTAMP WITHOUT TIME ZONE NOT NULL, - processed TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL -); --- Description -COMMENT ON TABLE - public.process_queue - IS 'process queue for async job'; --- Index -CREATE INDEX ON public.process_queue (channel); -CREATE INDEX ON public.process_queue (stored); -CREATE INDEX ON public.process_queue (processed); - --- Function process_queue helpers -create function new_account_entry_fn() returns trigger as $new_account_entry$ -begin - insert into process_queue (channel, payload, stored) values ('new_account', NEW.email, now()); - return NEW; -END; -$new_account_entry$ language plpgsql; - -create function new_account_otp_validation_entry_fn() returns trigger as $new_account_otp_validation_entry$ -begin - insert into process_queue (channel, payload, stored) values ('new_account_otp', NEW.email, now()); - return NEW; -END; -$new_account_otp_validation_entry$ language plpgsql; - -create function new_vessel_entry_fn() returns trigger as $new_vessel_entry$ -begin - insert into process_queue (channel, payload, stored) values ('new_vessel', NEW.owner_email, now()); - return NEW; -END; -$new_vessel_entry$ language plpgsql; - ---------------------------------------------------------------------------- --- App settings --- https://dba.stackexchange.com/questions/27296/storing-application-settings-with-different-datatypes#27297 --- https://stackoverflow.com/questions/6893780/how-to-store-site-wide-settings-in-a-database --- http://cvs.savannah.gnu.org/viewvc/*checkout*/gnumed/gnumed/gnumed/server/sql/gmconfiguration.sql - -CREATE TABLE IF NOT EXISTS public.app_settings ( - name TEXT NOT NULL UNIQUE, - value TEXT NOT NULL -); --- Description -COMMENT ON TABLE public.app_settings IS 'application settings'; -COMMENT ON COLUMN public.app_settings.name IS 'application settings name key'; -COMMENT ON COLUMN public.app_settings.value IS 'application settings value'; - ---------------------------------------------------------------------------- --- Badges descriptions --- TODO add contiditions --- -CREATE TABLE IF NOT EXISTS badges( - name TEXT UNIQUE, - description TEXT -); --- Description -COMMENT ON TABLE - public.badges - IS 'Badges descriptions'; - -INSERT INTO badges VALUES -('Helmsman', - 'Nice work logging your first sail! You are officially a helmsman now!'), -('Wake Maker', - 'Yowzers! Welcome to the 15 knot+ club ya speed demon skipper!'), -('Explorer', - 'It looks like home is where the helm is. Cheers to 10 days away from home port!'), -('Mooring Pro', - 'It takes a lot of skill to "thread that floating needle" but seems like you have mastered mooring with 10 nights on buoy!'), -('Anchormaster', - 'Hook, line and sinker, you have this anchoring thing down! 25 days on the hook for you!'), -('Traveler', - 'Who needs to fly when one can sail! You are an international sailor. À votre santé!'), -('Stormtrooper', - 'Just like the elite defenders of the Empire, here you are, our braving your own hydro-empire in windspeeds above 30kts. Nice work trooper! '), -('Club Alaska', - 'Home to the bears, glaciers, midnight sun and high adventure. Welcome to the Club Alaska Captain!'), -('Tropical Traveler', - 'Look at you with your suntan, tropical drink and southern latitude!'), -('Aloha Award', - 'Ticking off over 2300 NM across the great blue Pacific makes you the rare recipient of the Aloha Award. Well done and Aloha sailor!'), -('Tyee', - 'You made it to the Tyee Outstation, the friendliest dock in Pacific Northwest!'), --- TODO the sea is big and the world is not limited to the US -('Mediterranean Traveler', - 'You made it trought the Mediterranean!'); - create function public.process_badge_queue_fn() RETURNS void AS $process_badge_queue$ declare badge_rec record; @@ -1139,6 +649,7 @@ $process_badge_queue$ language plpgsql; -- TODO add alert monitoring for Battery --------------------------------------------------------------------------- +-- PostgREST API pre-request check -- TODO db-pre-request = "public.check_jwt" -- Prevent unregister user or unregister vessel access CREATE OR REPLACE FUNCTION public.check_jwt() RETURNS void AS $$ @@ -1243,6 +754,7 @@ BEGIN END $$ language plpgsql security definer; +--------------------------------------------------------------------------- -- Function to trigger cron_jobs using API for tests. -- Todo limit access and permision -- Run con jobs diff --git a/initdb/02_3_3_signalk_public_functions_py.sql b/initdb/02_3_3_signalk_public_functions_py.sql new file mode 100644 index 0000000..9591a3f --- /dev/null +++ b/initdb/02_3_3_signalk_public_functions_py.sql @@ -0,0 +1,308 @@ +--------------------------------------------------------------------------- +-- singalk db public schema +-- + +-- List current database +select current_database(); + +-- connect to the DB +\c signalk + +CREATE SCHEMA IF NOT EXISTS public; + +--------------------------------------------------------------------------- +-- python reverse_geocode +-- +-- https://github.com/CartoDB/labs-postgresql/blob/master/workshop/plpython.md +-- +DROP FUNCTION IF EXISTS reverse_geocode_py_fn; +CREATE OR REPLACE FUNCTION reverse_geocode_py_fn(IN geocoder TEXT, IN lon NUMERIC, IN lat NUMERIC, + OUT geo_name TEXT) +AS $reverse_geocode_py$ + import requests + + # Use the shared cache to avoid preparing the geocoder metadata + if geocoder in SD: + plan = SD[geocoder] + # A prepared statement from Python + else: + plan = plpy.prepare("SELECT reverse_url AS url FROM geocoders WHERE name = $1", ["text"]) + SD[geocoder] = plan + + # Execute the statement with the geocoder param and limit to 1 result + rv = plpy.execute(plan, [geocoder], 1) + url = rv[0]['url'] + + # Validate input + if not lon or not lat: + plpy.notice('reverse_geocode_py_fn Parameters [{}] [{}]'.format(lon, lat)) + plpy.error('Error missing parameters') + return None + + # Make the request to the geocoder API + payload = {"lon": lon, "lat": lat, "format": "jsonv2", "zoom": 18} + r = requests.get(url, params=payload) + + # Return the full address or nothing if not found + if r.status_code == 200 and "name" in r.json(): + return r.json()["name"] + else: + plpy.error('Failed to received a geo full address %s', r.json()) + return 'unknow' +$reverse_geocode_py$ LANGUAGE plpython3u; +-- Description +COMMENT ON FUNCTION + public.reverse_geocode_py_fn + IS 'query reverse geo service to return location name using plpython3u'; + +--------------------------------------------------------------------------- +-- python send email +-- +-- https://www.programcreek.com/python/example/3684/email.utils.formatdate +DROP FUNCTION IF EXISTS send_email_py_fn; +CREATE OR REPLACE FUNCTION send_email_py_fn(IN email_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void +AS $send_email_py$ + # Import smtplib for the actual sending function + import smtplib + + # Import the email modules we need + #from email.message import EmailMessage + from email.utils import formatdate,make_msgid + from email.mime.text import MIMEText + + # Use the shared cache to avoid preparing the email metadata + if email_type in SD: + plan = SD[email_type] + # A prepared statement from Python + else: + plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"]) + SD[email_type] = plan + + # Execute the statement with the email_type param and limit to 1 result + rv = plpy.execute(plan, [email_type], 1) + email_subject = rv[0]['email_subject'] + email_content = rv[0]['email_content'] + + # Replace fields using input jsonb obj + if not _user or not app: + plpy.notice('send_email_py_fn Parameters [{}] [{}]'.format(_user, app)) + plpy.error('Error missing parameters') + return None + if 'logbook_name' in _user and _user['logbook_name']: + email_content = email_content.replace('__LOGBOOK_NAME__', _user['logbook_name']) + if 'logbook_link' in _user and _user['logbook_link']: + email_content = email_content.replace('__LOGBOOK_LINK__', str(_user['logbook_link'])) + if 'recipient' in _user and _user['recipient']: + email_content = email_content.replace('__RECIPIENT__', _user['recipient']) + if 'boat' in _user and _user['boat']: + email_content = email_content.replace('__BOAT__', _user['boat']) + if 'badge' in _user and _user['badge']: + email_content = email_content.replace('__BADGE_NAME__', _user['badge']) + if 'otp_code' in _user and _user['otp_code']: + email_content = email_content.replace('__OTP_CODE__', _user['otp_code']) + + if 'app.url' in app and app['app.url']: + email_content = email_content.replace('__APP_URL__', app['app.url']) + + email_from = 'root@localhost' + if 'app.email_from' in app and app['app.email_from']: + email_from = 'PostgSail <' + app['app.email_from'] + '>' + #plpy.notice('Sending email from [{}] [{}]'.format(email_from, app['app.email_from'])) + + email_to = 'root@localhost' + if 'email' in _user and _user['email']: + email_to = _user['email'] + #plpy.notice('Sending email to [{}] [{}]'.format(email_to, _user['email'])) + else: + plpy.error('Error email to') + return None + + msg = MIMEText(email_content, 'plain', 'utf-8') + msg["Subject"] = email_subject + msg["From"] = email_from + msg["To"] = email_to + msg["Date"] = formatdate() + msg["Message-ID"] = make_msgid() + + server_smtp = 'localhost' + if 'app.email_server' in app and app['app.email_server']: + server_smtp = app['app.email_server'] + + # Send the message via our own SMTP server. + try: + # send your message with credentials specified above + with smtplib.SMTP(server_smtp, 25) as server: + if 'app.email_user' in app and app['app.email_user'] \ + and 'app.email_pass' in app and app['app.email_pass']: + server.starttls() + server.login(app['app.email_user'], app['app.email_pass']) + #server.send_message(msg) + server.sendmail(msg["From"], msg["To"], msg.as_string()) + server.quit() + # tell the script to report if your message was sent or which errors need to be fixed + plpy.notice('Sent email successfully to [{}] [{}]'.format(msg["To"], msg["Subject"])) + return None + except OSError as error: + plpy.error(error) + except smtplib.SMTPConnectError: + plpy.error('Failed to connect to the server. Bad connection settings?') + except smtplib.SMTPServerDisconnected: + plpy.error('Failed to connect to the server. Wrong user/password?') + except smtplib.SMTPException as e: + plpy.error('SMTP error occurred: ' + str(e)) +$send_email_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u; +-- Description +COMMENT ON FUNCTION + public.send_email_py_fn + IS 'Send email notification using plpython3u'; + +--------------------------------------------------------------------------- +-- python send pushover message +-- https://pushover.net/ +DROP FUNCTION IF EXISTS send_pushover_py_fn; +CREATE OR REPLACE FUNCTION send_pushover_py_fn(IN message_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void +AS $send_pushover_py$ + import requests + + # Use the shared cache to avoid preparing the email metadata + if message_type in SD: + plan = SD[message_type] + # A prepared statement from Python + else: + plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"]) + SD[message_type] = plan + + # Execute the statement with the message_type param and limit to 1 result + rv = plpy.execute(plan, [message_type], 1) + pushover_title = rv[0]['pushover_title'] + pushover_message = rv[0]['pushover_message'] + + # Replace fields using input jsonb obj + if 'logbook_name' in _user and _user['logbook_name']: + pushover_message = pushover_message.replace('__LOGBOOK_NAME__', _user['logbook_name']) + if 'logbook_link' in _user and _user['logbook_link']: + pushover_message = pushover_message.replace('__LOGBOOK_LINK__', str(_user['logbook_link'])) + if 'recipient' in _user and _user['recipient']: + pushover_message = pushover_message.replace('__RECIPIENT__', _user['recipient']) + if 'boat' in _user and _user['boat']: + pushover_message = pushover_message.replace('__BOAT__', _user['boat']) + if 'badge' in _user and _user['badge']: + pushover_message = pushover_message.replace('__BADGE_NAME__', _user['badge']) + + if 'app.url' in app and app['app.url']: + pushover_message = pushover_message.replace('__APP_URL__', app['app.url']) + + pushover_token = None + if 'app.pushover_app_token' in app and app['app.pushover_app_token']: + pushover_token = app['app.pushover_app_token'] + else: + plpy.error('Error no pushover token defined, check app settings') + return None + pushover_user = None + if 'pushover_user_key' in _user and _user['pushover_user_key']: + pushover_user = _user['pushover_user_key'] + else: + plpy.error('Error no pushover user token defined, check user settings') + return None + + # requests + r = requests.post("https://api.pushover.net/1/messages.json", data = { + "token": pushover_token, + "user": pushover_user, + "title": pushover_title, + "message": pushover_message + }) + + #print(r.text) + # Return ?? or None if not found + plpy.notice('Sent pushover successfully to [{}] [{}]'.format(r.text, r.status_code)) + if r.status_code == 200: + plpy.notice('Sent pushover successfully to [{}] [{}] [{}]'.format("__USER__", pushover_title, r.text)) + else: + plpy.error('Failed to send pushover') + return None +$send_pushover_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u; +-- Description +COMMENT ON FUNCTION + public.send_pushover_py_fn + IS 'Send pushover notification using plpython3u'; + +--------------------------------------------------------------------------- +-- python send telegram message +-- https://core.telegram.org/ +DROP FUNCTION IF EXISTS send_telegram_py_fn; +CREATE OR REPLACE FUNCTION send_telegram_py_fn(IN message_type TEXT, IN _user JSONB, IN app JSONB) RETURNS void +AS $send_telegram_py$ + """ + Send a message to a telegram user or group specified on chatId + chat_id must be a number! + """ + import requests + import json + + # Use the shared cache to avoid preparing the email metadata + if message_type in SD: + plan = SD[message_type] + # A prepared statement from Python + else: + plan = plpy.prepare("SELECT * FROM email_templates WHERE name = $1", ["text"]) + SD[message_type] = plan + + # Execute the statement with the message_type param and limit to 1 result + rv = plpy.execute(plan, [message_type], 1) + telegram_title = rv[0]['pushover_title'] + telegram_message = rv[0]['pushover_message'] + + # Replace fields using input jsonb obj + if 'logbook_name' in _user and _user['logbook_name']: + telegram_message = telegram_message.replace('__LOGBOOK_NAME__', _user['logbook_name']) + if 'logbook_link' in _user and _user['logbook_link']: + telegram_message = telegram_message.replace('__LOGBOOK_LINK__', str(_user['logbook_link'])) + if 'recipient' in _user and _user['recipient']: + telegram_message = telegram_message.replace('__RECIPIENT__', _user['recipient']) + if 'boat' in _user and _user['boat']: + telegram_message = telegram_message.replace('__BOAT__', _user['boat']) + if 'badge' in _user and _user['badge']: + telegram_message = telegram_message.replace('__BADGE_NAME__', _user['badge']) + + if 'app.url' in app and app['app.url']: + telegram_message = telegram_message.replace('__APP_URL__', app['app.url']) + + telegram_token = None + if 'app.telegram_bot_token' in app and app['app.telegram_bot_token']: + telegram_token = app['app.telegram_bot_token'] + else: + plpy.error('Error no telegram token defined, check app settings') + return None + telegram_chat_id = None + if 'telegram_chat_id' in _user and _user['telegram_chat_id']: + telegram_chat_id = _user['telegram_chat_id'] + else: + plpy.error('Error no telegram user token defined, check user settings') + return None + + # requests + headers = {'Content-Type': 'application/json', + 'Proxy-Authorization': 'Basic base64'} + data_dict = {'chat_id': telegram_chat_id, + 'text': telegram_message, + 'parse_mode': 'HTML', + 'disable_notification': False} + data = json.dumps(data_dict) + url = f'https://api.telegram.org/bot{telegram_token}/sendMessage' + r = requests.post(url, + data=data, + headers=headers) + print(r.text) + # Return the full address or None if not found + plpy.notice('Sent telegram successfully to [{}] [{}]'.format(r.text, r.status_code)) + if r.status_code == 200: + plpy.notice('Sent telegram successfully to [{}] [{}] [{}]'.format("__USER__", telegram_title, r.text)) + else: + plpy.error('Failed to send telegram') + return None +$send_telegram_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u; +-- Description +COMMENT ON FUNCTION + public.send_telegram_py_fn + IS 'Send a message to a telegram user or group specified on chatId using plpython3u';