aboutsummaryrefslogtreecommitdiffstats
#!rsc by RouterOS
# RouterOS script: global-functions
# Copyright (c) 2013-2020 Christian Hesse <mail@eworm.de>
#                         Michael Gisbers <michael@gisbers.de>
# https://git.eworm.de/cgit/routeros-scripts/about/COPYING.md
#
# global functions
# https://git.eworm.de/cgit/routeros-scripts/about/

# expected configuration version
:global ExpectedConfigVersion 34;

# global variables not to be changed by user
:global GlobalFunctionsReady false;
:global Identity [ / system identity get name ];

# global functions
:global CertificateAvailable;
:global CertificateDownload;
:global CertificateNameByCN;
:global CharacterReplace;
:global CleanFilePath;
:global DefaultRouteIsReachable;
:global DeviceInfo;
:global DNSIsResolving;
:global DownloadPackage;
:global FlushTelegramQueue;
:global GetMacVendor;
:global GetRandom20CharHex;
:global GetRandomNumber;
:global IfThenElse;
:global IPCalc;
:global LogPrintExit;
:global MailServerIsUp;
:global MkDir;
:global ParseKeyValueStore;
:global RandomDelay;
:global ScriptFromTerminal;
:global ScriptInstallUpdate;
:global ScriptLock;
:global SendEMail;
:global SendNotification;
:global SendTelegram;
:global SymbolByUnicodeName;
:global SymbolForNotification;
:global TimeIsSync;
:global UrlEncode;
:global VersionToNum;
:global WaitDefaultRouteReachable;
:global WaitDNSResolving;
:global WaitForFile;
:global WaitFullyConnected;
:global WaitTimeSync;

# check and download required certificate
:set CertificateAvailable do={
  :local CommonName [ :tostr $1 ];

  :global CertificateDownload;
  :global LogPrintExit;
  :global ParseKeyValueStore;

  :if ([ / system resource get free-hdd-space ] < 8388608 && \
       [ / certificate settings get crl-download ] = true && \
       [ / certificate settings get crl-store ] = "system") do={
    $LogPrintExit warning ("This system has low free flash space but " . \
      "is configured to download certificate CRLs to system!") false;
  }

  :if ([ :len [ / certificate find where common-name=$CommonName ] ] = 0) do={
    $LogPrintExit info ("Certificate with CommonName \"" . $CommonName . "\" not available.") false;
    :if ([ $CertificateDownload $CommonName ] = false) do={
      :return false;
    }
  }

  :local CertVal;
  :local Issuer $CommonName;
  :do {
    :if ([ :len [ / certificate find where common-name=$Issuer ] ] = 0) do={
      $LogPrintExit info ("Certificate chain for \"" . $CommonName . \
        "\" is incomplete, missing \"" . $Issuer . "\".") false;
      :if ([ $CertificateDownload $CommonName ] = false) do={
        :return false;
      }
    }
    :set CertVal [ / certificate get [ find where common-name=$Issuer ] ];
    :set Issuer ([ $ParseKeyValueStore ($CertVal->"issuer") ]->"CN");
  } while=($Issuer != $CertVal->"common-name");
  :return true;
}

# download and import certificate
:set CertificateDownload do={
  :local CommonName [ :tostr $1 ];

  :global ScriptUpdatesBaseUrl;
  :global ScriptUpdatesUrlSuffix;

  :global CertificateNameByCN;
  :global LogPrintExit;
  :global UrlEncode;
  :global WaitForFile;

  $LogPrintExit info ("Downloading and importing certificate with " . \
      "CommonName \"" . $CommonName . "\".") false;
  :do {
    :local LocalFileName ($CommonName . ".pem");
    :local UrlFileName ([ $UrlEncode $CommonName ] . ".pem");
    / tool fetch check-certificate=yes-without-crl \
      ($ScriptUpdatesBaseUrl . "certs/" . \
      $UrlFileName . $ScriptUpdatesUrlSuffix) \
      dst-path=$LocalFileName;
    $WaitForFile $LocalFileName;
    / certificate import file-name=$LocalFileName passphrase="";
    / file remove $LocalFileName;

    :foreach Cert in=[ / certificate find where name~("^" . $LocalFileName . "_[0-9]+\$") ] do={
      $CertificateNameByCN [ / certificate get $Cert common-name ];
    }
  } on-error={
    $LogPrintExit warning ("Failed imprting certificate!") false;
    :return false;
  }
  :return true;
}

# name a certificate by its common-name
:set CertificateNameByCN do={
  :local CommonName [ :tostr $1 ];

  :global CharacterReplace;

  :local Cert [ / certificate find where common-name=$CommonName ];
  / certificate set $Cert \
    name=[ $CharacterReplace [ $CharacterReplace [ $CharacterReplace $CommonName "'" "-" ] " " "-" ] "---" "-" ];
}

# character replace
:set CharacterReplace do={
  :local String [ :tostr $1 ];
  :local ReplaceFrom [ :tostr $2 ];
  :local ReplaceWith [ :tostr $3 ];
  :local Return "";

  :if ($ReplaceFrom = "") do={
    :return $String;
  }

  :while ([ :typeof [ :find $String $ReplaceFrom ] ] != "nil") do={
    :local Pos [ :find $String $ReplaceFrom ];
    :set Return ($Return . [ :pick $String 0 $Pos ] . $ReplaceWith);
    :set String [ :pick $String ($Pos + [ :len $ReplaceFrom ]) [ :len $String ] ];
  }

  :return ($Return . $String);
}

# clean file path
:set CleanFilePath do={
  :local Path [ :tostr $1 ];

  :global CharacterReplace;

  :while ($Path ~ "//") do={
    :set $Path [ $CharacterReplace $Path "//" "/" ];
  }
  :if ([ :pick $Path 0 ] = "/") do={
    :set Path [ :pick $Path 1 [ :len $Path ] ];
  }
  :if ([ :pick $Path ([ :len $Path ] - 1) ] = "/") do={
    :set Path [ :pick $Path 0 ([ :len $Path ] - 1) ];
  }

  :return $Path;
}

# default route is reachable
:set DefaultRouteIsReachable do={
  :if ([ :len [ / ip route find where dst-address=0.0.0.0/0 active !blackhole !routing-mark !unreachable ] ] > 0) do={
    :return true;
  }
  :return false;
}

# get readable device info
:set DeviceInfo do={
  :global ExpectedConfigVersion;
  :global GlobalConfigVersion;
  :global Identity;

  :global IfThenElse;

  :local Resource [ / system resource get ];
  :local RouterBoard [ / system routerboard get ];
  :local Update [ / system package update get ];

  :return ( \
         "Hostname:       " . $Identity . \
       "\nBoard name:     " . $Resource->"board-name" . \
       "\nArchitecture:   " . $Resource->"architecture-name" . \
    [ $IfThenElse ($RouterBoard->"routerboard" = true) \
      ("\nModel:          " . $RouterBoard->"model" . \
         [ $IfThenElse ([ :len ($RouterBoard->"revision") ] > 0) \
           (" " . $RouterBoard->"revision") ] . \
       "\nSerial number:  " . $RouterBoard->"serial-number") ] . \
       "\nRouterOS:" . \
       "\n    Channel:    " . $Update->"channel" . \
       "\n    Installed:  " . $Update->"installed-version" . \
    [ $IfThenElse ([ :typeof ($Update->"latest-version") ] != "nothing" && \
        $Update->"installed-version" != $Update->"latest-version") \
      ("\n    Available:  " . $Update->"latest-version") ] . \
       "\nRouterOS-Scripts Configuration Version:" . \
       "\n    Current:    " . $GlobalConfigVersion . \
    [ $IfThenElse ($GlobalConfigVersion != $ExpectedConfigVersion) \
      ("\n    Expected:   " . $ExpectedConfigVersion) ]);
}

# check if DNS is resolving
:set DNSIsResolving do={
  :global CharacterReplace;

  :do {
    :resolve ([ $CharacterReplace  [ / system clock get time ] ":" "" ] . ".mikrotik.com");
    :return true;
  } on-error={
    :return false;
  }
}

# download package from upgrade server
:set DownloadPackage do={
  :local PkgName [ :tostr $1 ];
  :local PkgVer  [ :tostr $2 ];
  :local PkgArch [ :tostr $3 ];
  :local PkgDir  [ :tostr $4 ];

  :global CertificateAvailable;
  :global CleanFilePath;
  :global LogPrintExit;
  :global WaitForFile;

  :if ([ :len $PkgName ] = 0) do={ :return false; }
  :if ([ :len $PkgVer  ] = 0) do={ :set PkgVer  [ / system package update get installed-version ]; }
  :if ([ :len $PkgArch ] = 0) do={ :set PkgArch [ / system resource get architecture-name ]; }

  :local PkgFile ($PkgName . "-" . $PkgVer . "-" . $PkgArch . ".npk");
  :if ($PkgArch = "x86_64" || $PkgName ~ "^routeros-") do={
    :set PkgFile ($PkgName . "-" . $PkgVer . ".npk");
  }
  :local PkgDest [ $CleanFilePath ($PkgDir . "/" . $PkgFile) ];

  :if ([ :len [ / file find where name=$PkgDest type="package" ] ] > 0) do={
    $LogPrintExit info ("Package file alreasy exists.") false;
    :return true;
  }

  :if ([ $CertificateAvailable "Let's Encrypt Authority X3" ] = false) do={
    $LogPrintExit error ("Downloading required certificate failed.") true;
  }

  :local Retry 3;
  :while ($Retry > 0) do={
    :do {
      / tool fetch check-certificate=yes-without-crl \
        ("https://upgrade.mikrotik.com/routeros/" . $PkgVer . "/" . $PkgFile) \
        dst-path=$PkgDest;
      $WaitForFile $PkgDest;

      :if ([ / file get [ find where name=$PkgDest ] type ] = "package") do={
        :return true;
      }
    } on-error={
      $LogPrintExit debug ("Downloading package failed.") false;
    }

    / file remove [ find where name=$PkgDest ];
    :set Retry ($Retry - 1);
  }

  :return false;
}

# flush telegram queue
:set FlushTelegramQueue do={
  :global TelegramQueue;
  :global TelegramTokenId;

  :global LogPrintExit;

  :local AllDone true;
  :local QueueLen [ :len $TelegramQueue ];
  :foreach Id,Message in=$TelegramQueue do={
    :if ([ :typeof $Message ] = "array" ) do={
      :do {
        / tool fetch check-certificate=yes-without-crl output=none http-method=post \
          ("https://api.telegram.org/bot" . $TelegramTokenId . "/sendMessage") \
          http-data=("chat_id=" . ($Message->"chatid") . \
          "&disable_notification=" . ($Message->"silent") . \
          "&parse_mode=" . ($Message->"parsemode") . "&text=" . ($Message->"text"));
        :set ($TelegramQueue->$Id);
      } on-error={
        $LogPrintExit debug ("Sending queued Telegram message failed.") false;
        :set AllDone false;
      }
    }
  }

  :if ($AllDone = true && $QueueLen = [ :len $TelegramQueue ]) do={
    / system scheduler remove FlushTelegramQueue;
    :set TelegramQueue;
  }
}

# get MAC vendor
:set GetMacVendor do={
  :local Mac [ :tostr $1 ];

  :global CertificateAvailable;
  :global LogPrintExit;

  :do {
    :if ([ $CertificateAvailable "Let's Encrypt Authority X3" ] = false) do={
      $LogPrintExit warning ("Downloading required certificate failed.") true;
    }
    :local Vendor ([ / tool fetch check-certificate=yes-without-crl \
        ("https://api.macvendors.com/" . [ :pick $Mac 0 8 ]) output=user as-value ]->"data");
    :return $Vendor;
  } on-error={
    :return "unknown vendor";
  }
}

# generate random 20 chars hex (0-9 and a-f)
:set GetRandom20CharHex do={
  :local Random ([ / certificate scep-server otp generate minutes-valid=0 as-value ]->"password");
  / certificate scep-server otp remove [ find where password=$Random ];
  :return $Random;
}

# generate random number
:set GetRandomNumber do={
  :local Max 4294967295;
  :if ([ :typeof $1 ] != "nothing" ) do={
    :set Max ([ :tonum $1 ] + 1);
  }

  :global GetRandom20CharHex;

  :local Num;
  :local 40CharHex ([ $GetRandom20CharHex ] . [ $GetRandom20CharHex ]);

  :for I from=0 to=39 do={
    :local Char [ :pick $40CharHex $I ];
    :if ($Char~"[0-9]") do={
      :set Num ($Num . $Char);
    }
  }

  :return ([ :tonum [ :pick $Num 0 18 ] ] % $Max);
}

# mimic conditional/ternary operator (condition ? consequent : alternative)
:set IfThenElse do={
  :if ([ :tostr $1 ] = "true" || [ :tobool $1 ] = true) do={
    :return $2;
  }
  :return $3;
}

# calculate and print netmask, network, min host, max host and broadcast
:set IPCalc do={
  :local Input [ :tostr $1 ];
  :local Address [ :toip [ :pick $Input 0 [ :find $Input "/" ] ] ];
  :local Bits [ :tonum [ :pick $Input ([ :find $Input "/" ] + 1) [ :len $Input ] ] ];
  :local Mask ((255.255.255.255 << (32 - $Bits)) & 255.255.255.255);

  :local Return {
    "address"=$Address;
    "netmask"=$Mask;
    "networkaddress"=($Address & $Mask);
    "networkbits"=$Bits;
    "network"=(($Address & $Mask) . "/" . $Bits);
    "hostmin"=(($Address & $Mask) | 0.0.0.1);
    "hostmax"=(($Address | ~$Mask) ^ 0.0.0.1);
    "broadcast"=($Address | ~$Mask);
  }

  :put ( \
    "Address:   " . $Return->"address" . "\n\r" . \
    "Netmask:   " . $Return->"netmask" . "\n\r" . \
    "Network:   " . $Return->"network" . "\n\r" . \
    "HostMin:   " . $Return->"hostmin" . "\n\r" . \
    "HostMax:   " . $Return->"hostmax" . "\n\r" . \
    "Broadcast: " . $Return->"broadcast");

  :return $Return;
}

# log and print with same text, optionally exit
:set LogPrintExit do={
  :local Severity [ :tostr $1 ];
  :local Message  [ :tostr $2 ];
  :local Exit     [ :tostr $3 ];

  :global PrintDebug;

  :if ($Severity ~ "^(debug|error|info)\$") do={
    :if ($Severity = "debug") do={ :log debug $Message; }
    :if ($Severity = "error") do={ :log error $Message; }
    :if ($Severity = "info" ) do={ :log info  $Message; }
  } else={
    :log warning $Message;
    :set Severity "warning";
  }

  :if ($Severity != "debug" || $PrintDebug = true) do={
    :if ($Exit = "true") do={
      :error ($Severity . ": " . $Message);
    } else={
      :put ($Severity . ": " . $Message);
    }
  }
}

# check if mail server is up
:set MailServerIsUp do={
  :local MailServer [ / tool e-mail get address ];

  :global EmailGeneralTo;

  :global LogPrintExit;

  :if ([ :len $EmailGeneralTo ] = 0) do={
    :return true;
  } else={
    :if ($MailServer = "0.0.0.0") do={
      $LogPrintExit warning ("No mail server is configured! Returning gracefully...") false;
      :return true;
    }
  }

  :if ([ :len [ / tool netwatch find where comment=$MailServer ] ] = 0) do={
    $LogPrintExit warning ("Adding netwatch entry for mail server.") false;
    :local MailHost $MailServer;
    :if ([ :typeof [ :toip $MailHost ] ] != "ip" ) do={
      :do {
        :set MailHost [ :resolve $MailServer ];
      } on-error={
        $LogPrintExit warning ("Resolving mail server failed.") false;
        :return false;
      }
    }
    / tool netwatch add comment=$MailServer host=$MailHost;
  }

  :local NetWatch [ / tool netwatch find where comment=$MailServer ];
  :local NetWatchVal [ / tool netwatch get $NetWatch ];
  :if ($NetWatchVal->"status" = "up") do={
    :return true;
  }

  / tool netwatch set interval=($NetWatchVal->"interval") $NetWatch;
  :delay ($NetWatchVal->"timeout");
  :if ([ / tool netwatch get $NetWatch status ] = "up") do={
    :return true;
  }

  :return false;
}

# create directory
:set MkDir do={
  :local Dir [ :tostr $1 ];

  :global WaitForFile;

  :if ([ :len [ / file find where name=$Dir type="directory" ] ] = 0) do={
    :local WwwVal [ / ip service get www ];
    / ip service set www address=127.0.0.1/32 disabled=no port=80;
    / tool fetch http://127.0.0.1/ dst-path=($Dir . "/tmp");
    $WaitForFile ($Dir . "/tmp");
    / file remove ($Dir . "/tmp");
    / ip service set www address=($WwwVal->"address") \
        disabled=($WwwVal->"disabled") port=($WwwVal->"port");
  }
}

# parse key value store
:set ParseKeyValueStore do={
  :local Source $1;
  :if ([ :typeof $Source ] != "array") do={
    :set Source [ :tostr $1 ];
  }
  :local Result [ :toarray "" ];
  :foreach KeyValue in=[ :toarray $Source ] do={
    :if ([ :find $KeyValue "=" ]) do={
      :set ($Result->[ :pick $KeyValue 0 [ :find $KeyValue "=" ] ]) \
        [ :pick $KeyValue ([ :find $KeyValue "=" ] + 1) [ :len $KeyValue ] ];
    } else={
      :set ($Result->$KeyValue) true;
    }
  }
  :return $Result;
}

# delay a random amount of seconds
:set RandomDelay do={
  :global GetRandomNumber;

  :delay ([ $GetRandomNumber $1 ] . "s");
}

# check if script is run from terminal
:set ScriptFromTerminal do={
  :local Script [ :tostr $1 ];

  :global LogPrintExit;

  :foreach Job in=[ / system script job find where script=$Script ] do={
    :set Job [ / system script job get $Job ];
    :while ([ :typeof ($Job->"parent") ] = "id") do={
      :set Job [ / system script job get [ find where .id=($Job->"parent") ] ];
    }
    :if (($Job->"type") = "login") do={
      $LogPrintExit debug ("Script " . $Script . " started from terminal.") false;
      :return true;
    }
  }
  $LogPrintExit debug ("Script " . $Script . " NOT started from terminal.") false;

  :return false;
}

# install new scripts, update existing scripts
:set ScriptInstallUpdate do={
  :local Scripts [ :toarray $1 ];

  :global ExpectedConfigVersion;
  :global GlobalConfigVersion;
  :global Identity;
  :global IDonate;
  :global ScriptUpdatesBaseUrl;
  :global ScriptUpdatesFetch;
  :global ScriptUpdatesIgnore;
  :global ScriptUpdatesUrlSuffix;
  :global SentConfigChangesNotification;

  :global LogPrintExit;
  :global ParseKeyValueStore;
  :global SendNotification;
  :global SymbolForNotification;

  :foreach Script in=$Scripts do={
    :if ([ :len [ / system script find where name=$Script ] ] = 0) do={
      $LogPrintExit info ("Adding new script: " . $Script) false;
      / system script add name=$Script source="#!rsc by RouterOS\n";
    }
  }

  :foreach Script in=[ / system script find where source~"^#!rsc( by RouterOS)\?\n" ] do={
    :local Ignore 0;
    :local ScriptVal [ / system script get $Script ];
    :local ScriptFile [ / file find where name=("script-updates/" . $ScriptVal->"name") ];
    :local SourceNew;
    :if ([ :len $ScriptFile ] > 0) do={
      :set SourceNew [ / file get $ScriptFile content ];
      / file remove $ScriptFile;
    }

    :foreach Scheduler in=[ / system scheduler find where on-event~("\\b" . $ScriptVal->"name" . "\\b") ] do={
      :local SchedulerVal [ / system scheduler get $Scheduler ];
      :if ($ScriptVal->"policy" != $SchedulerVal->"policy") do={
        $LogPrintExit warning ("Policies differ for script " . $ScriptVal->"name" . \
          " and its scheduler " . $SchedulerVal->"name" . "!") false;
      }
      :if ($SchedulerVal->"name" != "global-scripts" && \
           $SchedulerVal->"start-time" = "startup" && \
           $SchedulerVal->"interval" = 0s && \
           !(($SchedulerVal->"on-event") ~ "\\brun global-wait\\b")) do={
        $LogPrintExit warning ("Scheduler " . $SchedulerVal->"name" . " starts on startup, " . \
          "without waiting for global-functions. Run 'global-wait' to avoid race conditions!") false;
      }
    }

    :if ([ :len $SourceNew ] = 0 && $ScriptUpdatesFetch = true) do={
      :local Comment [ $ParseKeyValueStore ($ScriptVal->"comment") ];
      :if ($Comment->"ignore" = true) do={
        :set Ignore 1;
      } else={
        # TODO: remove at later time
        :foreach IgnoreLoop in=$ScriptUpdatesIgnore do={
          :if ($IgnoreLoop = $ScriptVal->"name") do={
            :set Ignore 1;
            $LogPrintExit info ("Migrating script " . $ScriptVal->"name" . " to ignore flag in comment.") false;
            / system script set comment="ignore" ($ScriptVal->"name");
          }
        }
      }

      :if ($Ignore = 0) do={
        $LogPrintExit debug ("Fetching script from url: " . $ScriptVal->"name") false;
        :do {
          :local BaseUrl $ScriptUpdatesBaseUrl;
          :local UrlSuffix $ScriptUpdatesUrlSuffix;
          :if ([ :typeof ($Comment->"base-url") ] = "str") do={ :set BaseUrl ($Comment->"base-url"); }
          :if ([ :typeof ($Comment->"url-suffix") ] = "str") do={ :set UrlSuffix ($Comment->"url-suffix"); }

          :local Result [ / tool fetch check-certificate=yes-without-crl \
              ($BaseUrl . $ScriptVal->"name" . $UrlSuffix) output=user as-value ];
          :if ($Result->"status" = "finished") do={
            :set SourceNew ($Result->"data");
          }
        } on-error={
          $LogPrintExit warning ("Failed fetching " . $ScriptVal->"name") false;
        }
      }
    }

    :if ([ :len $SourceNew ] > 0) do={
      :if ([ :pick $SourceNew 0 18 ] = "#!rsc by RouterOS\n") do={
        :if ($SourceNew != $ScriptVal->"source") do={
          :local DontRequirePermissions \
              ($SourceNew~"\n# requires: dont-require-permissions=yes\n");
          $LogPrintExit info ("Updating script: " . $ScriptVal->"name") false;
          / system script set owner=($ScriptVal->"name") source=$SourceNew \
              dont-require-permissions=$DontRequirePermissions $Script;
          :if ($ScriptVal->"name" = "global-config" && \
               [ :len [ / system script find where name="global-config-overlay" ] ] > 0) do={
            $LogPrintExit info ("Reloading global configuration and overlay.") false;
            / system script { run global-config; run global-config-overlay; }
          }
          :if ($ScriptVal->"name" = "global-functions") do={
            $LogPrintExit info ("Reloading global functions.") false;
            / system script run global-functions;
          }
        } else={
          $LogPrintExit debug ("Script " .  $ScriptVal->"name" . " did not change.") false;
        }
      } else={
        $LogPrintExit warning ("Looks like new script " . $ScriptVal->"name" . " is not valid. Ignoring!") false;
      }
    } else={
      $LogPrintExit debug ("No update for script " . $ScriptVal->"name" . ".") false;
    }
  }

  :if ($SentConfigChangesNotification!=$ExpectedConfigVersion && \
       $GlobalConfigVersion < $ExpectedConfigVersion) do={
    :global GlobalConfigChanges;
    :local ChangeLogCode;
    :local ConfigScript "global-config";
    :if ([ :len [ / system script find where name="global-config-overlay" ] ] > 0) do={
      :set ConfigScript "global-config-overlay";
    }
    :local NotificationMessage ("Current configuration on " . $Identity . \
        " is out of date. Please update " . $ConfigScript . ", then increase " . \
        "\$GlobalConfigVersion (currently " . $GlobalConfigVersion . \
        ") to " . $ExpectedConfigVersion . " and re-run " . $ConfigScript . ".");
    $LogPrintExit info ($NotificationMessage) false;

    $LogPrintExit debug ("Fetching changelog.") false;
    :do {
      :local Result [ / tool fetch check-certificate=yes-without-crl \
          ($ScriptUpdatesBaseUrl . "global-config.changes" . $ScriptUpdatesUrlSuffix) \
          output=user as-value ];
      :if ($Result->"status" = "finished") do={
        :set ChangeLogCode ($Result->"data");
      }
      :set NotificationMessage ($NotificationMessage . "\n\nChanges:");
      [ :parse $ChangeLogCode ];
      :for I from=($GlobalConfigVersion + 1) to=$ExpectedConfigVersion do={
        :set NotificationMessage ($NotificationMessage . \
            "\n * " . $GlobalConfigChanges->[ :tostr $I ]);
        $LogPrintExit info ("Change: " . $GlobalConfigChanges->[ :tostr $I ]) false;
      }
      :set GlobalConfigChanges;
    } on-error={
      $LogPrintExit warning ("Failed fetching changes!") false;
      :set NotificationMessage ($NotificationMessage . \
          "\n\nChanges are not available.");
    }

    :if ($IDonate != true) do={
      :set NotificationMessage ($NotificationMessage . \
        "\n\n==== donation hint ====\n" . \
        "This project is developed in private spare time and usage is " . \
        "free of charge for you. If you like the scripts and think this is " . \
        "of value for you or your business please consider a donation:\n" . \
        "https://git.eworm.de/cgit/routeros-scripts/about/#donate");
    }

    $SendNotification ([ $SymbolForNotification "pushpin" ] . "News and configuration changes") \
      $NotificationMessage;
    :set SentConfigChangesNotification $ExpectedConfigVersion;
  }
}

# lock script against multiple invocation
:set ScriptLock do={
  :global LogPrintExit;

  :local Script [ :tostr $1 ];

  :if ([ :len [ / system script job find where script=$Script ] ] > 1) do={
    $LogPrintExit info ("Script " . $Script . " started more than once... Aborting.") true;
  }
}

# send notification via e-mail
:set SendEMail do={
  :local Subject [ :tostr $1 ];
  :local Message [ :tostr $2 ];
  :local Attach  [ :tostr $3 ];

  :global Identity;
  :global EmailGeneralTo;
  :global EmailGeneralCc;

  :global LogPrintExit;

  :if ([ :len $EmailGeneralTo ] = 0) do={
    :return false;
  }

  :do {
    :local Signature [ / system note get note ];
    :if ([ :len $Signature ] > 0) do={
      :set Signature ("\n-- \n" . $Signature);
    }
    / tool e-mail send to=$EmailGeneralTo cc=$EmailGeneralCc \
      subject=("[" . $Identity . "] " . $Subject) \
      body=($Message . $Signature) file=$Attach;
  } on-error={
    $LogPrintExit warning ("Failed sending notification mail!") false;
  }
}

# send notification via e-mail and telegram
# Note that attachment is ignored for telegram, silent is ignored for e-mail!
:set SendNotification do={
  :local Subject [ :tostr $1 ];
  :local Message [ :tostr $2 ];
  :local Attach  [ :tostr $3 ];
  :local Silent  [ :tostr $4 ];

  :global SendEMail;
  :global SendTelegram;

  $SendEMail $Subject $Message $Attach;
  $SendTelegram $Subject $Message $Silent;
}

# send notification via telegram
:set SendTelegram do={
  :local Subject [ :tostr $1 ];
  :local Message [ :tostr $2 ];
  :local Silent  [ :tostr $3 ];

  :global Identity;
  :global TelegramChatId;
  :global TelegramChatIdOverride;
  :global TelegramFixedWidthFont;
  :global TelegramQueue;
  :global TelegramTokenId;

  :global CertificateAvailable;
  :global CharacterReplace;
  :global IfThenElse;
  :global LogPrintExit;
  :global SymbolForNotification;
  :global UrlEncode;

  :local EscapeMD do={
    :global TelegramFixedWidthFont;

    :global CharacterReplace;

    :if ($TelegramFixedWidthFont != true) do={
      :return $1;
    }

    :local Return $1;
    :local Chars {
      "fixed"={ "\\"; "`" };
      "hint"={ "_"; "*"; "["; "]"; "("; ")"; "~"; "`"; ">";
               "#"; "+"; "-"; "="; "|"; "{"; "}"; "."; "!" };
    }
    :foreach Char in=($Chars->$2) do={
      :set Return [ $CharacterReplace $Return $Char ("\\" . $Char) ];
    }

    :if ($2 = "fixed") do={
      :return ("```\n" . $Return . "\n```");
    }

    :return $Return;
  }

  :local ChatId $TelegramChatId;
  :if ([ :len $TelegramChatIdOverride ] > 0) do={
    :set ChatId $TelegramChatIdOverride;
  }

  :if ([ :len $TelegramTokenId ] = 0 || [ :len $ChatId ] = 0) do={
    :return false;
  }

  :local Text ("[" . $Identity . "] " . $Subject . "\n\n" . $Message);
  :if ([ :len $Text ] > 3968) do={
    :set Text ([ $EscapeMD ([ :pick $Text 0 3840 ] . "...") "fixed" ] . \
      "\n\n" . [ $SymbolForNotification "scissors" ] . \
      [ $EscapeMD "The Telegram message was too long and has been truncated!" "hint" ]);
  } else={
    :set Text [ $EscapeMD $Text "fixed" ];
  }
  :set Text [ $UrlEncode $Text ];
  :local ParseMode [ $IfThenElse ($TelegramFixedWidthFont = true) "MarkdownV2" "" ];

  :do {
    :if ([ $CertificateAvailable "Go Daddy Secure Certificate Authority - G2" ] = false) do={
      $LogPrintExit warning ("Downloading required certificate failed.") true;
    }
    / tool fetch check-certificate=yes-without-crl output=none http-method=post \
      ("https://api.telegram.org/bot" . $TelegramTokenId . "/sendMessage") \
      http-data=("chat_id=" . $ChatId . "&disable_notification=" . $Silent . \
      "&parse_mode=" . $ParseMode . "&text=" . $Text);
  } on-error={
    $LogPrintExit warning ("Failed sending telegram notification! Queuing...") false;

    :if ([ :typeof $TelegramQueue ] = "nothing") do={
      :set TelegramQueue [ :toarray "" ];
    }
    :set Text ($Text . [ $UrlEncode ("\n\n" . [ $SymbolForNotification "alarm-clock" ] . \
      [ $EscapeMD ("This message was queued since " . [ / system clock get date ] . \
      " " . [ / system clock get time ] . " and may be obsolete.") "hint" ]) ]);
    :set ($TelegramQueue->[ :len $TelegramQueue ]) {
      chatid=$ChatId; parsemode=$ParseMode; text=$Text; silent=$Silent };
    :if ([ :len [ / system scheduler find where name="FlushTelegramQueue" ] ] = 0) do={
      / system scheduler add name=FlushTelegramQueue interval=1m start-time=startup \
        on-event=":global FlushTelegramQueue; \$FlushTelegramQueue;";
    }
  }
}

# return UTF-8 symbol for unicode name
:set SymbolByUnicodeName do={
  :local Symbols {
    "alarm-clock"="\E2\8F\B0";
    "calendar"="\F0\9F\93\85";
    "cross-mark"="\E2\9D\8C";
    "fire"="\F0\9F\94\A5";
    "floppy-disk"="\F0\9F\92\BE";
    "high-voltage-sign"="\E2\9A\A1";
    "incoming-envelope"="\F0\9F\93\A8";
    "lock-with-ink-pen"="\F0\9F\94\8F";
    "mobile-phone"="\F0\9F\93\B1";
    "pushpin"="\F0\9F\93\8C";
    "scissors"="\E2\9C\82";
    "sparkles"="\E2\9C\A8";
    "warning-sign"="\E2\9A\A0";
    "white-heavy-check-mark"="\E2\9C\85"
  }

  :return ($Symbols->$1);
}

# return symbol for notification
:set SymbolForNotification do={
  :global NotificationsWithSymbols;

  :global SymbolByUnicodeName;

  :if ($NotificationsWithSymbols != true) do={
    :return "";
  }
  :return ([ $SymbolByUnicodeName $1 ] . " ");
}

# check if system time is sync
:set TimeIsSync do={
  :global LogPrintExit;

  :if ([ / system ntp client get enabled ] = true) do={
    :do {
      :if ([ / system ntp client get status ] = "synchronized") do={
        :return true;
      }
    } on-error={
      :if ([ :typeof [ / system ntp client get last-adjustment ] ] = "time") do={
        :return true;
      }
    }
    :return false;
  }

  :if ([ / ip cloud get ddns-enabled ] = true && [ / ip cloud get update-time ] = true) do={
    :if ([ :typeof [ / ip cloud get public-address ] ] = "ip") do={
      :return true;
    }
    :return false;
  }

  $LogPrintExit warning ("No time source configured! Returning gracefully...") false;
  :return true;
}

# url encoding
:set UrlEncode do={
  :local Input [ :tostr $1 ];
  :local Return "";

  :if ([ :len $Input ] > 0) do={
    :local Chars "\n\r !\"#\$%&'()*+,:;<=>\?@[\\]^`{|}~";
    :local Subs { "%0A"; "%0D"; "%20"; "%21"; "%22"; "%23"; "%24"; "%25"; "%26"; "%27";
                  "%28"; "%29"; "%2A"; "%2B"; "%2C"; "%3A"; "%3B"; "%3C"; "%3D"; "%3E";
                  "%3F"; "%40"; "%5B"; "%5C"; "%5D"; "%5E"; "%60"; "%7B"; "%7C"; "%7D";
                  "%7E" };

    :for I from=0 to=([ :len $Input ] - 1) do={
      :local Char [ :pick $Input $I ];
      :local Replace [ :find $Chars $Char ];

      :if ([ :len $Replace ] > 0) do={
        :set Char ($Subs->$Replace);
      }
      :set Return ($Return . $Char);
    }
  }

  :return $Return;
}

# convert version string to numeric value
:set VersionToNum do={
  :local Input [ :tostr $1 ];
  :local Multi 0x1000000;
  :local Return 0;

  :global CharacterReplace;

  :set Input [ $CharacterReplace [ $CharacterReplace [ $CharacterReplace $Input \
    "." "," ] "beta" ",beta," ] "rc" ",rc," ];

  :foreach Value in=([ :toarray $Input ], 0) do={
    :local Num [ :tonum $Value ];
    :if ($Multi = 0x100) do={
      :if ([ :typeof $Num ] = "num") do={
        :set Return ($Return + 0xff00);
        :set Multi ($Multi / 0x100);
      } else={
        :if ($Value = "beta") do={ :set Return ($Return + 0x3f00); }
        :if ($Value = "rc") do={ :set Return ($Return + 0x7f00); }
      }
    }
    :if ([ :typeof $Num ] = "num") do={ :set Return ($Return + ($Value * $Multi)); }
    :set Multi ($Multi / 0x100);
  }

  :return $Return;
}

# wait for default route to be reachable
:set WaitDefaultRouteReachable do={
  :global DefaultRouteIsReachable;

  :while ([ $DefaultRouteIsReachable ] = false) do={
    :delay 1s;
  }
}

# wait for DNS to resolve
:set WaitDNSResolving do={
  :global DNSIsResolving;

  :while ([ $DNSIsResolving ] = false) do={
    :delay 1s;
  }
}

# wait for file to be available
:set WaitForFile do={
  :global CleanFilePath;

  :local FileName [ $CleanFilePath [ :tostr $1 ] ];
  :local I 0;

  :while ([ :len [ / file find where name=$FileName ] ] = 0) do={
    :if ($I > 20) do={
      :return false;
    }
    :delay 100ms;
    :set I ($I + 1);
  }
  :return true;
}

# wait to be fully connected (default route is reachable, time is sync, DNS resolves)
:set WaitFullyConnected do={
  :global WaitDefaultRouteReachable;
  :global WaitDNSResolving;
  :global WaitTimeSync;

  $WaitDefaultRouteReachable;
  $WaitTimeSync;
  $WaitDNSResolving;
}

# wait for time to become synced
:set WaitTimeSync do={
  :global LogPrintExit;
  :global TimeIsSync;

  :while ([ $TimeIsSync ] = false) do={
    :if ([ :len [ / system script find where name="rotate-ntp" ] ] > 0 && \
         ([ / system resource get uptime ] % (180 * 1000000000)) = 0s) do={
      :do {
        / system script run rotate-ntp;
      } on-error={
        $LogPrintExit debug ("Running rotate-ntp failed.") false;
      }
    }
    :delay 1s;
  }
}

# signal we are ready
:set GlobalFunctionsReady true;