#!/usr/bin/env bash
#
# Author: Dmitry Razumov <asmeron@ublinux.com>
# Copyright (c) 2021-2025 UBLinux <support@ublinux.com>
#
# Extended pattern matching: https://www.gnu.org/software/bash/manual/html_node/Pattern-Matching.html#Pattern-Matching
shopt -s extglob

ENABLED=yes
[[ ${ENABLED} == yes ]] || exit 0

SOURCE=/usr/lib/ublinux/functions; [[ -f ${SOURCE} ]] && . ${SOURCE} 2>/dev/null
SOURCE=/usr/lib/ublinux/default; [[ -f ${SOURCE} ]] && . ${SOURCE} 2>/dev/null

SOURCE=${SYSCONF}/config; [[ -f ${SOURCE} ]] && . ${SOURCE} 2>/dev/null
SOURCE=${SYSCONF}/logging; [ -f ${SOURCE} ] && . ${SOURCE} 2>/dev/null
SOURCE=${SYSCONF}/network; [ -f ${SOURCE} ] && . ${SOURCE} 2>/dev/null

##  Назначение модулей:
##  account   подтвердить личность, проверив учетные данные, такие как пароль, ключ, токен и другие
##  auth      проверить авторизацию для таких действий, как разрешения, ограничения и т. д.
##  password  обновить учетные данные
##  session   распределять ресурсы во время входа в систему, такие как личные данные, лимиты и другие.
##
##  Выполнение из /etc/pam.d/system-login
##  Тип модуля: session
##  Глобальные переменные:
##    PAM_RHOST    Удалённый хост
##    PAM_RUSER    Удалённый пользователь
##    PAM_SERVICE  Сервис выполняющий вход /etc/pam.d/service_name
##    PAM_TTY      Консоль, может быть как "/dev/tty2" так и ":0"
##    PAM_USER     Текущий пользователь
##    PAM_TYPE     Тип сессии, возможные значения: account, auth, password, open_session, close_session
##
## PAM_RHOST=        PAM_RUSER= PAM_SERVICE=login             PAM_TTY=/dev/tty2 PAM_USER=superadmin PAM_TYPE=open_session
## PAM_RHOST=        PAM_RUSER= PAM_SERVICE=login             PAM_TTY=/dev/tty2 PAM_USER=superadmin PAM_TYPE=close_session
## PAM_RHOST=        PAM_RUSER= PAM_SERVICE=lightdm-autologin PAM_TTY=:0        PAM_USER=superadmin PAM_TYPE=open_session
## PAM_RHOST=        PAM_RUSER= PAM_SERVICE=systemd-user      PAM_TTY=          PAM_USER=lightdm    PAM_TYPE=open_session
## PAM_RHOST=        PAM_RUSER= PAM_SERVICE=systemd-user      PAM_TTY=          PAM_USER=user-2     PAM_TYPE=open_session
## PAM_RHOST=        PAM_RUSER= PAM_SERVICE=lightdm           PAM_TTY=:0        PAM_USER=user-2     PAM_TYPE=open_session
## PAM_RHOST=1.2.3.4 PAM_RUSER= PAM_SERVICE=sshd              PAM_TTY=ssh       PAM_USER=user-2     PAM_TYPE=open_session


if [[ ${PAM_TYPE} == "open_session" && -n ${PAM_USER} ]]; then
# Выполнение функции вызвано используя PAM, получаем имя пользователя вызвавшего PAM, будем пременять только для пользователя
    SELECT_USER="${PAM_USER}"
fi

## Вывод уведомления на рабочий стол полученных от лога journald
## JOURNALD_NOTIFY[<type>:<type_value>:<priority>:<options>]=<filter>;<filter>;<filter>
##   <type>		# Тип фильтра
##     u		# Фильтр по юнитам системным или pattern
##     uu		# Фильтр (user-unit) по юнитам пользователя или pattern
##     t		# Фильтр (identifier) по идентификатору <SYSLOG_IDENTIFIER> или pattern
##     f		# Фильтр (facility) по типу объекта <SYSLOG>, строгое значение
##   <type_value>	# Значения для типа фильтра
##     <unit>		# Если type=u то имя юнита системы или pattern
##     <user-unit>		# Если type=uu то имя юнита пользователя или pattern
##     <SYSLOG_IDENTIFIER> # Если type=t то имя идентификатора или pattern. Вывести доступные: journalctl -F SYSLOG_IDENTIFIER
##			# Если type=f то применимы варианты facility:
##     0 | kern		#   0: kern	(Kernel messages)
##     1 | user		#   1: user	(User-level messages)
##     2 | mail		#   2: mail	(Mail system)
##     3 | daemon	#   3: daemon	(System daemons)
##     4 | auth		#   4: auth	(Security/authorization messages)
##     5 | syslog	#   5: syslog	(Messages generated internally by syslogd)
##     6 | lpr		#   6: lpr	(Line printer subsystem (archaic subsystem))
##     7 | news		#   7: news	(Network news subsystem (archaic subsystem))
##     8 | uucp		#   8: uucp	(UUCP subsystem (archaic subsystem))
##     9		#   9: -	(Clock daemon systemd-timesyncd)
##     10| authpriv	#   10: authpriv (Security/authorization messages)
##     11| ftp		#   11: ftp	(FTP daemon)
##     12		#   12: -	(NTP subsystem)
##     13		#   13: -	(Log audit)
##     14		#   14: -	(Log alert)
##     15| cron		#   15: cron	(Scheduling daemon)
##     16| local0	#   16: local0	(Local use 0 (local0))
##     17| local1	#   17: local1	(Local use 1 (local1))
##     18| local2	#   18: local2	(Local use 2 (local2))
##     19| local3	#   19: local3	(Local use 3 (local3))
##     20| local4	#   20: local4	(Local use 4 (local4))
##     21| local5	#   21: local5	(Local use 5 (local5))
##     22| local6	#   22: local6	(Local use 6 (local6))
##     23| local7	#   23: local7	(Local use 7 (local7))
##   <priority>		# Фильтр по приоритету 0:emerg 1:alert 2:crit 3:err 4:warning 5:notice 6:info 7:debug
##     0 | emerg	# 0: emergency (неработоспособность системы)
##     1 | alert	# 1: alerts (предупреждения, требующие немедленного вмешательства)
##     2 | crit		# 2: critical (критическое состояние)
##     3 | err		# 3: errors (ошибки)
##     4 | warning	# 4: warning (предупреждения)
##     5 | notice	# 5: notice (уведомления)
##     6 | info		# 6: info (информационные сообщения)
##     7 | debug	# 7: debug (отладочные сообщения)
##   <options>		# Произвольные дополнительные опции фильтра пользователя для journald
##   <filter>		# Фильтр сообщения
##     @		# Фильтр отключен, выводить все сообщения
##     ip@conflict	# Фильтровать сообщения о конфликте IP адреса
##     ip@manager-connecting    # Фильтровать сообщения NetworkManager о соединении
##     ip@manager-disconnecting # Фильтровать сообщения NetworkManager об отсоединении
##     ip		# Фильтровать сообщения NetworkManager
##     <regexp_message>	# Шаблон обработки сообщения, произвольное выражение вырезки для команды sed

## JOURNALD_NOTIFY[u:NetworkManager:warning]=@
## JOURNALD_NOTIFY[u:NetworkManager:warning]=ip@conflict
## JOURNALD_NOTIFY[u:*Network*:warning]=@
## JOURNALD_NOTIFY[uu:NetworkManager:warning]=@
## JOURNALD_NOTIFY[t:*Network*:warning]=@
## JOURNALD_NOTIFY[f:daemon:warning]=@
## JOURNALD_NOTIFY[u:NetworkManager:warning]=@
## JOURNALD_NOTIFY[u:NetworkManager]=ip@manager-disconnecting,ip@manager-connecting

#1749316860.013993 NetworkManager[34283]: <warn>  [1749316860.0139] device (ens192): IP address 192.168.7.121 cannot be configured because it is already in use in the network by host 00:0C:29:80:F2:5E
#1749316861.486311 NetworkManager[34283]: <warn>  [1749316861.4862] device (ens192): Activation: failed for connection 'Проводное подключение 1'

## echo "$(printf 'journalctl'; printf ' -u %s' $(journalctl -q -F UNIT | grep -E '^Network'))"
## echo "$(printf 'journalctl'; printf ' -t %s' $(journalctl -q -F SYSLOG_IDENTIFIER | grep -E '^gnome'))"
## journalctl -t gnome-system-monitor.desktop -t gnome-shell -t gnome-keyring-daemon -t gnome-session-binary -t gnome-session

## TODO
## 1. type=uu требует доработки, т.к. запуск от рута и будут видны юниты только рута, а нужно от вошедшего пользователя. Поможет PAM pamsession расположение от которого запускается утилита
##    Пользователь запустивший: ${SELECT_USER}
## 2. Рассмотреть запуск не через setsid, а через создание сервиса. Возможно и не нужно.

exec_01_journald_notify(){
    [[ $1 == @("set="|"set+="|"set++="|"set-="|"set--="|"remove") ]] && local COMMAND=$1 && shift
    [[ -n ${COMMAND} ]] || local COMMAND="set="
    [[ $(declare -p JOURNALD_NOTIFY 2>/dev/null) =~ ^"declare -A" ]] || declare -gA JOURNALD_NOTIFY
    local PARAM="$@"
    if [[ -n ${PARAM} ]]; then
	local JOURNALD_NOTIFY=
	declare -A JOURNALD_NOTIFY=()
	[[ ${PARAM} =~ ^[[:alnum:]_]+("="|"[".*"]=") ]] && eval "${PARAM%%=*}=\${PARAM#*=}"
    fi
    if [[ ${COMMAND} == @("set="|"set+="|"set++=") ]] && [[ ${#JOURNALD_NOTIFY[@]} -ne 0 ]]; then
	local TYPE= TYPE_VOLUE= PRIORITY= OPTIONS= FILTER=
	for SELECT_JOURNALD_NOTIFY in "${!JOURNALD_NOTIFY[@]}"; do
	    IFS=: read -r TYPE TYPE_VOLUE PRIORITY OPTIONS NULL <<< ${SELECT_JOURNALD_NOTIFY}
	    [[ -n ${TYPE} && -n ${TYPE_VOLUE} ]] || continue
	    case "${TYPE,,}" in
		"u")	TYPE="--unit" ;;
		"uu")	TYPE="--user-unit" ;;
		"t")	TYPE="--identifier" ;;
		"f")	TYPE="--facility" ;;
		*)	continue ;;
	    esac
	    [[ ${TYPE} == "--facility" ]] && { [[ ${TYPE_VOLUE,,} == @(0|kern|1|user|2|mail|3|daemon|4|auth|5|syslog|6|lpr|7|news|8|uucp|9|10|authpriv|11|ftp|12|13|14|15|cron|16|local0|17|local1|18|local2|19|local3|20|local4|21|local5|22|local6|23|local7) ]] || continue; }
	    [[ -n ${PRIORITY} && ${PRIORITY} == @(0|emerg|1|alert|2|crit|3|err|4|warning|5|notice|6|info|7|debug) ]] && PRIORITY="--priority ${PRIORITY}"
	    FILTER=${JOURNALD_NOTIFY[${SELECT_JOURNALD_NOTIFY}]}
	    pkill -f "/usr/lib/ublinux/rc.pamsession.d/02-journald-notify ubconfig_journald_notify_live ${TYPE} ${TYPE_VOLUE} ${PRIORITY} ${OPTIONS} ${FILTER}"
	    setsid -f /usr/lib/ublinux/rc.pamsession.d/02-journald-notify ubconfig_journald_notify_live "${TYPE}" "${TYPE_VOLUE}" "${PRIORITY}" "${OPTIONS}" "${FILTER}"
	    if [[ ${FILTER} =~ (^|,)"ip@conflict"(,|$) ]]; then
		# Включить таймаут обнаружения конфликтов IP на всех сетевых интерфейсах
		[[ ${NETWORK[all@connmod]} =~ "ipv4.dad-timeout -1" ]] || ubconfig --quiet --target system set [network] NETWORK[all@connmod]+=" ipv4.dad-timeout -1"
	    fi
	done
    elif [[ ${COMMAND} == @("set-="|"set--="|"remove") ]]; then
	if [[ ${PARAM%%=*} =~ ^.*'['(.*)']' && ${BASH_REMATCH[1]} == @("*"|"**"|"/"|"//") ]]; then
	    PARAM_VALUE="${PARAM#*=}"
	    JOURNALD_NOTIFY+="${PARAM_VALUE// /,}"
	fi
	local TYPE= TYPE_VOLUE= PRIORITY= OPTIONS= FILTER=
	for SELECT_JOURNALD_NOTIFY in "${!JOURNALD_NOTIFY[@]}"; do
	    IFS=: read -r TYPE TYPE_VOLUE PRIORITY OPTIONS NULL <<< ${SELECT_JOURNALD_NOTIFY}
	    [[ -n ${TYPE} && -n ${TYPE_VOLUE} ]] || continue
	    case "${TYPE,,}" in
		"u")	TYPE="--unit" ;;
		"uu")	TYPE="--user-unit" ;;
		"t")	TYPE="--identifier" ;;
		"f")	TYPE="--facility" ;;
		*)	continue ;;
	    esac
	    [[ ${TYPE} == "--facility" ]] && { [[ ${TYPE_VOLUE,,} == @(0|kern|1|user|2|mail|3|daemon|4|auth|5|syslog|6|lpr|7|news|8|uucp|9|10|authpriv|11|ftp|12|13|14|15|cron|16|local0|17|local1|18|local2|19|local3|20|local4|21|local5|22|local6|23|local7) ]] || continue; }
	    [[ -n ${PRIORITY} && ${PRIORITY} == @(0|emerg|1|alert|2|crit|3|err|4|warning|5|notice|6|info|7|debug) ]] && PRIORITY="--priority ${PRIORITY}"
	    FILTER=${JOURNALD_NOTIFY[${SELECT_JOURNALD_NOTIFY}]}
	    pkill -f "/usr/lib/ublinux/rc.pamsession.d/02-journald-notify ubconfig_journald_notify_live ${TYPE} ${TYPE_VOLUE} ${PRIORITY} ${OPTIONS} ${FILTER}"
	    if [[ ${FILTER} =~ (^|,)"ip@conflict"(,|$) ]]; then
		# Выключить таймаут обнаружения конфликтов IP на всех сетевых интерфейсах
	        [[ ${NETWORK[all@connmod]} =~ "ipv4.dad-timeout -1" ]] && ubconfig --quiet --target system set [network] NETWORK[all@connmod]-="ipv4.dad-timeout -1"
	    fi
	done
    fi
}

ubconfig_journald_notify_live(){
    local TYPE=$1 TYPE_VOLUE=$2 PRIORITY=$3 OPTIONS=$4 FILTER=$5
    NOTIFY_ICON="--icon dialog-information --urgency=normal"
    init(){
	export TEXTDOMAIN="ublinux-init-${0##*/}"
	export TEXTDOMAINDIR="${PATH_ROOT}/usr/share/locale"
    }
    i18n(){
	local KEY="$1"
	shift
	printf "$(gettext -s "${KEY}")" "$@"
    }
    show_notify(){
        local NO_NOTIFY=
        while IFS= read -ru3 SELECT_FILTER; do
	    local NOTIFY_MESSAGE_SHOW=
	    case "${SELECT_FILTER}" in
		"@") # Без фильтра
		    true
		;;
		"ip@conflict") # Фильтр NetworkManager.service с переводом
		    # Убираем первых два блока тип сообщеия и дату, оставляем только:
		    #device (ens192): IP address 192.168.7.121 cannot be configured because it is already in use in the network by host 00:0C:29:80:F2:5E
		    FILTER_SED="s/^[^[:blank:]]+[[:blank:]]+[^[:blank:]]*[[:blank:]]+(.*)/\1/p"
		    NOTIFY_MESSAGE_TRIM=$(sed -En "${FILTER_SED}" <<< ${NOTIFY_MESSAGE})
		    [[ ${NOTIFY_MESSAGE_TRIM} =~ ^"device ("([[:alnum:]]+)"): IP address "([[:digit:].]+)" cannot be configured because it is already in use in the network by host "([[:alnum:]:]+)$ ]] \
		    && NOTIFY_MESSAGE_SHOW=$(i18n "device (%s): IP address %s cannot be configured because it is already in use in the network by host %s" "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}") #"
		;;
		"ip@manager-connecting") # Фильтр NetworkManager.service с переводом
		    # Убираем первых два блока тип сообщеия и дату
		    FILTER_SED="s/^[^[:blank:]]+[[:blank:]]+[^[:blank:]]*[[:blank:]]+(.*)/\1/p"
		    NOTIFY_MESSAGE_TRIM=$(sed -En "${FILTER_SED}" <<< ${NOTIFY_MESSAGE})
		    [[ ${NOTIFY_MESSAGE_TRIM} =~ ^"manager: NetworkManager state is now CONNECTING"$ ]] \
		    && NOTIFY_MESSAGE_SHOW=$(i18n "manager: NetworkManager state is now CONNECTING")
		;;
		"ip@manager-disconnecting") # Фильтр NetworkManager.service с переводом
		    # Убираем первых два блока тип сообщеия и дату
		    FILTER_SED="s/^[^[:blank:]]+[[:blank:]]+[^[:blank:]]*[[:blank:]]+(.*)/\1/p"
		    NOTIFY_MESSAGE_TRIM=$(sed -En "${FILTER_SED}" <<< ${NOTIFY_MESSAGE})
		    [[ ${NOTIFY_MESSAGE_TRIM} =~ ^"manager: NetworkManager state is now DISCONNECTING"$ ]] \
		    && NOTIFY_MESSAGE_SHOW=$(i18n "manager: NetworkManager state is now DISCONNECTING")
		;;
		    "ip") # Фильтр NetworkManager.service
		    # Убираем первых два блока тип сообщеия и дату, оставляем только:
		    FILTER_SED="s/^[^[:blank:]]+[[:blank:]]+[^[:blank:]]*[[:blank:]]+(.*)/\1/p"
		    NOTIFY_MESSAGE_SHOW=$(sed -En "${FILTER_SED}" <<< ${NOTIFY_MESSAGE})
		;;
		*) # Фильтр пользователя
		    FILTER_SED="${SELECT_FILTER}"
		    NOTIFY_MESSAGE_SHOW=$(sed -En "${FILTER_SED}" <<< ${NOTIFY_MESSAGE})
		;;
	    esac
		#[[ -n ${NOTIFY_MESSAGE_SHOW} ]] && echo "::${NOTIFY_ICON}::${NOTIFY_TIMESTAMP}::${NOTIFY_MESSAGE_SHOW}::"
		[[ -n ${NOTIFY_MESSAGE_SHOW} ]] && notify_send --app-name "$(i18n "UBLinux notify from Journald")" ${NOTIFY_ICON} "$(i18n "New notify from %s" "${NOTIFY_SYSTEMD_UNIT}")" "<b>${NOTIFY_TIMESTAMP}</b>\n\n${NOTIFY_MESSAGE_SHOW}" &
		#[[ -n ${NOTIFY_MESSAGE_SHOW} ]] && notify-send --app-name "UBConfig notify from Journald" ${NOTIFY_ICON} "New notify from UBConfig" "<b>${NOTIFY_TIMESTAMP}   ${NOTIFY_SYSTEMD_UNIT}</b>\n\ndevice (ens192): IP address 192.168.7.121 cannot be configured because it is already in use in the network by host 00:0C:29:80:F2:5E"
	done 3<<< ${FILTER//,/$'\n'}
    }
    init
    journalctl ${TYPE} "${TYPE_VOLUE}" ${PRIORITY} --no-hostname --lines 1 --output export --follow ${OPTIONS} | while IFS= read SELECT_JOURNAL_ITEM; do
        [[ ${SELECT_JOURNAL_ITEM} =~ ($'\n'|^)+"PRIORITY="([^$'\n']*)($'\n'|$)+ ]] && case ${BASH_REMATCH[2]} in
	    0|emerg) NOTIFY_ICON="--icon dialog-error --urgency=critical" ;;
	    1|alert) NOTIFY_ICON="--icon dialog-error --urgency=critical" ;;
	    2|crit) NOTIFY_ICON="--icon dialog-error --urgency=critical" ;;
	    3|err) NOTIFY_ICON="--icon dialog-error --urgency=normal" ;;
	    4|warning) NOTIFY_ICON="--icon dialog-warning --urgency=normal" ;;
	    5|notice) NOTIFY_ICON="--icon dialog-information --urgency=low" ;;
	    6|info) NOTIFY_ICON="--icon dialog-information --urgency=low" ;;
	    7|debug) NOTIFY_ICON="--icon dialog-information --urgency=low" ;;
	esac
	[[ ${SELECT_JOURNAL_ITEM} =~ ($'\n'|^)+"__REALTIME_TIMESTAMP="([^$'\n']*)($'\n'|$)+ ]] && NOTIFY_TIMESTAMP=$(date '+%d.%m.%Y %H:%M:%S' -d @${BASH_REMATCH[2]:0:10})
	[[ ${SELECT_JOURNAL_ITEM} =~ ($'\n'|^)+"_SYSTEMD_UNIT="([^$'\n']*)($'\n'|$)+ ]] && NOTIFY_SYSTEMD_UNIT="${BASH_REMATCH[2]}"
	[[ ${SELECT_JOURNAL_ITEM} =~ ($'\n'|^)+"MESSAGE="([^$'\n']*)($'\n'|$)+ ]] && NOTIFY_MESSAGE="${BASH_REMATCH[2]}" && show_notify
    done
}

################
##### MAIN #####
################

    # Если файл подключен как ресурс с функциями, то выйти
    return 0 2>/dev/null && return 0
    if [[ -z $@ ]]; then
        while read -r FUNCTION; do
            $"${FUNCTION##* }"
        done < <(declare -F | grep "declare -f exec_")
    else
	FUNCTION=
	while [[ $# -gt 0 ]]; do
	    #[[ -z ${1} ]] || { declare -f "${1}" &>/dev/null && FUNCTION+="; ${1}" || FUNCTION+=" '${1}'"; }
	    # Что-бы передавать пустые параметры как аргументы, нужно для соблюдения очередности и кол-ва, отключил [[ -z ${1} ]] ||
	    declare -f "${1}" &>/dev/null && FUNCTION+="; ${1}" || FUNCTION+=" '${1}'"
	    shift
	done
	eval ${FUNCTION#*; }
    fi
    true
