yannstatic/static/2024/02/22/Ntfy.html

2394 lines
222 KiB
HTML
Raw Normal View History

2024-10-31 20:18:37 +01:00
<!DOCTYPE html><html lang="fr">
<head><meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"><title>Ntfy service de notification - YannStatic</title>
<meta name="description" content="Ntfy, qui se prononce “notify”, est un service de notification ultra léger, permettant denvoyer des messages vers un smartphone ou un ordinateur via de simp...">
<link rel="canonical" href="https://static.rnmkcy.eu/2024/02/22/Ntfy.html"><link rel="alternate" type="application/rss+xml" title="YannStatic" href="/feed.xml">
<!-- - include head/favicon.html - -->
<link rel="shortcut icon" type="image/png" href="/assets/favicon/favicon.png"><link rel="stylesheet" href="/assets/css/main.css"><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" ><!-- start custom head snippets --><link rel="stylesheet" href="/assets/css/expand.css">
<!-- end custom head snippets --><script>(function() {
window.isArray = function(val) {
return Object.prototype.toString.call(val) === '[object Array]';
};
window.isString = function(val) {
return typeof val === 'string';
};
window.hasEvent = function(event) {
return 'on'.concat(event) in window.document;
};
window.isOverallScroller = function(node) {
return node === document.documentElement || node === document.body || node === window;
};
window.isFormElement = function(node) {
var tagName = node.tagName;
return tagName === 'INPUT' || tagName === 'SELECT' || tagName === 'TEXTAREA';
};
window.pageLoad = (function () {
var loaded = false, cbs = [];
window.addEventListener('load', function () {
var i;
loaded = true;
if (cbs.length > 0) {
for (i = 0; i < cbs.length; i++) {
cbs[i]();
}
}
});
return {
then: function(cb) {
cb && (loaded ? cb() : (cbs.push(cb)));
}
};
})();
})();
(function() {
window.throttle = function(func, wait) {
var args, result, thisArg, timeoutId, lastCalled = 0;
function trailingCall() {
lastCalled = new Date;
timeoutId = null;
result = func.apply(thisArg, args);
}
return function() {
var now = new Date,
remaining = wait - (now - lastCalled);
args = arguments;
thisArg = this;
if (remaining <= 0) {
clearTimeout(timeoutId);
timeoutId = null;
lastCalled = now;
result = func.apply(thisArg, args);
} else if (!timeoutId) {
timeoutId = setTimeout(trailingCall, remaining);
}
return result;
};
};
})();
(function() {
var Set = (function() {
var add = function(item) {
var i, data = this._data;
for (i = 0; i < data.length; i++) {
if (data[i] === item) {
return;
}
}
this.size ++;
data.push(item);
return data;
};
var Set = function(data) {
this.size = 0;
this._data = [];
var i;
if (data.length > 0) {
for (i = 0; i < data.length; i++) {
add.call(this, data[i]);
}
}
};
Set.prototype.add = add;
Set.prototype.get = function(index) { return this._data[index]; };
Set.prototype.has = function(item) {
var i, data = this._data;
for (i = 0; i < data.length; i++) {
if (this.get(i) === item) {
return true;
}
}
return false;
};
Set.prototype.is = function(map) {
if (map._data.length !== this._data.length) { return false; }
var i, j, flag, tData = this._data, mData = map._data;
for (i = 0; i < tData.length; i++) {
for (flag = false, j = 0; j < mData.length; j++) {
if (tData[i] === mData[j]) {
flag = true;
break;
}
}
if (!flag) { return false; }
}
return true;
};
Set.prototype.values = function() {
return this._data;
};
return Set;
})();
window.Lazyload = (function(doc) {
var queue = {js: [], css: []}, sources = {js: {}, css: {}}, context = this;
var createNode = function(name, attrs) {
var node = doc.createElement(name), attr;
for (attr in attrs) {
if (attrs.hasOwnProperty(attr)) {
node.setAttribute(attr, attrs[attr]);
}
}
return node;
};
var end = function(type, url) {
var s, q, qi, cbs, i, j, cur, val, flag;
if (type === 'js' || type ==='css') {
s = sources[type], q = queue[type];
s[url] = true;
for (i = 0; i < q.length; i++) {
cur = q[i];
if (cur.urls.has(url)) {
qi = cur, val = qi.urls.values();
qi && (cbs = qi.callbacks);
for (flag = true, j = 0; j < val.length; j++) {
cur = val[j];
if (!s[cur]) {
flag = false;
}
}
if (flag && cbs && cbs.length > 0) {
for (j = 0; j < cbs.length; j++) {
cbs[j].call(context);
}
qi.load = true;
}
}
}
}
};
var load = function(type, urls, callback) {
var s, q, qi, node, i, cur,
_urls = typeof urls === 'string' ? new Set([urls]) : new Set(urls), val, url;
if (type === 'js' || type ==='css') {
s = sources[type], q = queue[type];
for (i = 0; i < q.length; i++) {
cur = q[i];
if (_urls.is(cur.urls)) {
qi = cur;
break;
}
}
val = _urls.values();
if (qi) {
callback && (qi.load || qi.callbacks.push(callback));
callback && (qi.load && callback());
} else {
q.push({
urls: _urls,
callbacks: callback ? [callback] : [],
load: false
});
for (i = 0; i < val.length; i++) {
node = null, url = val[i];
if (s[url] === undefined) {
(type === 'js' ) && (node = createNode('script', { src: url }));
(type === 'css') && (node = createNode('link', { rel: 'stylesheet', href: url }));
if (node) {
node.onload = (function(type, url) {
return function() {
end(type, url);
};
})(type, url);
(doc.head || doc.body).appendChild(node);
s[url] = false;
}
}
}
}
}
};
return {
js: function(url, callback) {
load('js', url, callback);
},
css: function(url, callback) {
load('css', url, callback);
}
};
})(this.document);
})();
</script><script>
(function() {
var TEXT_VARIABLES = {
version: '2.2.6',
sources: {
font_awesome: 'https://use.fontawesome.com/releases/v5.0.13/css/all.css',
jquery: '/assets/js/jquery.min.js',
leancloud_js_sdk: '//cdn.jsdelivr.net/npm/leancloud-storage@3.13.2/dist/av-min.js',
chart: 'https://cdn.bootcss.com/Chart.js/2.7.2/Chart.bundle.min.js',
gitalk: {
js: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.js',
css: 'https://cdn.bootcss.com/gitalk/1.2.2/gitalk.min.css'
},
valine: 'https://unpkg.com/valine/dist/Valine.min.js'
},
site: {
toc: {
selectors: 'h1,h2,h3'
}
},
paths: {
search_js: '/assets/search.js'
}
};
window.TEXT_VARIABLES = TEXT_VARIABLES;
})();
</script>
</head>
<body>
<div class="root" data-is-touch="false">
<div class="layout--page js-page-root"><!----><div class="page__main js-page-main page__viewport hide-footer has-aside has-aside cell cell--auto">
<div class="page__main-inner"><div class="page__header d-print-none"><header class="header"><div class="main">
<div class="header__title">
<div class="header__brand"><svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="478.9473684210526" viewBox="0, 0, 400,478.9473684210526"><g id="svgg"><path id="path0" d="M308.400 56.805 C 306.970 56.966,303.280 57.385,300.200 57.738 C 290.906 58.803,278.299 59.676,269.200 59.887 L 260.600 60.085 259.400 61.171 C 258.010 62.428,256.198 63.600,255.645 63.600 C 255.070 63.600,252.887 65.897,252.598 66.806 C 252.460 67.243,252.206 67.600,252.034 67.600 C 251.397 67.600,247.206 71.509,247.202 72.107 C 247.201 72.275,246.390 73.190,245.400 74.138 C 243.961 75.517,243.598 76.137,243.592 77.231 C 243.579 79.293,241.785 83.966,240.470 85.364 C 239.176 86.740,238.522 88.365,237.991 91.521 C 237.631 93.665,236.114 97.200,235.554 97.200 C 234.938 97.200,232.737 102.354,232.450 104.472 C 232.158 106.625,230.879 109.226,229.535 110.400 C 228.933 110.926,228.171 113.162,226.434 119.500 C 226.178 120.435,225.795 121.200,225.584 121.200 C 225.373 121.200,225.200 121.476,225.200 121.813 C 225.200 122.149,224.885 122.541,224.500 122.683 C 223.606 123.013,223.214 123.593,223.204 124.600 C 223.183 126.555,220.763 132.911,219.410 134.562 C 218.443 135.742,217.876 136.956,217.599 138.440 C 217.041 141.424,215.177 146.434,214.532 146.681 C 214.240 146.794,214.000 147.055,214.000 147.261 C 214.000 147.467,213.550 148.086,213.000 148.636 C 212.450 149.186,212.000 149.893,212.000 150.208 C 212.000 151.386,208.441 154.450,207.597 153.998 C 206.319 153.315,204.913 150.379,204.633 147.811 C 204.365 145.357,202.848 142.147,201.759 141.729 C 200.967 141.425,199.200 137.451,199.200 135.974 C 199.200 134.629,198.435 133.224,196.660 131.311 C 195.363 129.913,194.572 128.123,193.870 125.000 C 193.623 123.900,193.236 122.793,193.010 122.540 C 190.863 120.133,190.147 118.880,188.978 115.481 C 188.100 112.928,187.151 111.003,186.254 109.955 C 185.358 108.908,184.518 107.204,183.847 105.073 C 183.280 103.273,182.497 101.329,182.108 100.753 C 181.719 100.177,180.904 98.997,180.298 98.131 C 179.693 97.265,178.939 95.576,178.624 94.378 C 178.041 92.159,177.125 90.326,175.023 87.168 C 174.375 86.196,173.619 84.539,173.342 83.486 C 172.800 81.429,171.529 79.567,170.131 78.785 C 169.654 78.517,168.697 77.511,168.006 76.549 C 167.316 75.587,166.594 74.800,166.402 74.800 C 166.210 74.800,164.869 73.633,163.421 72.206 C 160.103 68.936,161.107 69.109,146.550 69.301 C 133.437 69.474,128.581 70.162,126.618 72.124 C 126.248 72.495,125.462 72.904,124.872 73.033 C 124.282 73.163,123.088 73.536,122.219 73.863 C 121.349 74.191,119.028 74.638,117.061 74.858 C 113.514 75.254,109.970 76.350,108.782 77.419 C 107.652 78.436,100.146 80.400,97.388 80.400 C 95.775 80.400,93.167 81.360,91.200 82.679 C 90.430 83.195,89.113 83.804,88.274 84.031 C 85.875 84.681,78.799 90.910,74.400 96.243 L 73.400 97.456 73.455 106.028 C 73.526 117.055,74.527 121.238,77.820 124.263 C 78.919 125.273,80.400 127.902,80.400 128.842 C 80.400 129.202,81.075 130.256,81.900 131.186 C 83.563 133.059,85.497 136.346,86.039 138.216 C 86.233 138.886,87.203 140.207,88.196 141.153 C 89.188 142.098,90.000 143.104,90.000 143.388 C 90.000 144.337,92.129 148.594,92.869 149.123 C 93.271 149.410,93.600 149.831,93.600 150.059 C 93.600 150.286,93.932 150.771,94.337 151.136 C 94.743 151.501,95.598 153.004,96.237 154.475 C 96.877 155.947,97.760 157.351,98.200 157.596 C 98.640 157.841,99.900 159.943,101.000 162.267 C 102.207 164.817,103.327 166.644,103.825 166.876 C 104.278 167.087,105.065 168.101,105.573 169.130 C 107.658 173.348,108.097 174.093,110.006 176.647 C 111.103 178.114,112.000 179.725,112.000 180.227 C 112.000 181.048,113.425 183.163,114.678 184.200 C 115.295 184.711,117.396 188.733,117.720 190.022 C 117.855 190.562,118.603 191.633,119.381 192.402 C 120.160 193.171,121.496 195.258,122.351 197.039 C 123.206 198.820,124.167 200.378,124.487 200.501 C 124.807 200.624,125.953 202.496,127.034 204.662 C 128.114 206.828,129.676 209.299,130.505 210.153 C 131.333 211.007,132.124 212.177,132.262 212.753 C 132.618 214.239,134.291 217.048,136.288 219.5
" href="/">YannStatic</a></div><!--<button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button>--><!-- <li><button class="button button--secondary button--circle search-button js-search-toggle"><i class="fas fa-search"></i></button></li> -->
<!-- Champ de recherche -->
<div id="searchbox" class="search search--dark" style="visibility: visible">
<div class="main">
<div class="search__header"></div>
<div class="search-bar">
<div class="search-box js-search-box">
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
<input id="search-input" type="text" />
<!-- <div class="search-box__icon-clear js-icon-clear">
<a><i class="fas fa-times"></i></a>
</div> -->
</div>
</div>
</div>
</div>
<!-- Script pointing to search-script.js -->
<script>/*!
* Simple-Jekyll-Search
* Copyright 2015-2020, Christian Fei
* Licensed under the MIT License.
*/
(function(){
'use strict'
var _$Templater_7 = {
compile: compile,
setOptions: setOptions
}
const options = {}
options.pattern = /\{(.*?)\}/g
options.template = ''
options.middleware = function () {}
function setOptions (_options) {
options.pattern = _options.pattern || options.pattern
options.template = _options.template || options.template
if (typeof _options.middleware === 'function') {
options.middleware = _options.middleware
}
}
function compile (data) {
return options.template.replace(options.pattern, function (match, prop) {
const value = options.middleware(prop, data[prop], options.template)
if (typeof value !== 'undefined') {
return value
}
return data[prop] || match
})
}
'use strict';
function fuzzysearch (needle, haystack) {
var tlen = haystack.length;
var qlen = needle.length;
if (qlen > tlen) {
return false;
}
if (qlen === tlen) {
return needle === haystack;
}
outer: for (var i = 0, j = 0; i < qlen; i++) {
var nch = needle.charCodeAt(i);
while (j < tlen) {
if (haystack.charCodeAt(j++) === nch) {
continue outer;
}
}
return false;
}
return true;
}
var _$fuzzysearch_1 = fuzzysearch;
'use strict'
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
function FuzzySearchStrategy () {
this.matches = function (string, crit) {
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
}
}
'use strict'
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
function LiteralSearchStrategy () {
this.matches = function (str, crit) {
if (!str) return false
str = str.trim().toLowerCase()
crit = crit.trim().toLowerCase()
return crit.split(' ').filter(function (word) {
return str.indexOf(word) >= 0
}).length === crit.split(' ').length
}
}
'use strict'
var _$Repository_4 = {
put: put,
clear: clear,
search: search,
setOptions: __setOptions_4
}
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
function NoSort () {
return 0
}
const data = []
let opt = {}
opt.fuzzy = false
opt.limit = 10
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
opt.sort = NoSort
opt.exclude = []
function put (data) {
if (isObject(data)) {
return addObject(data)
}
if (isArray(data)) {
return addArray(data)
}
return undefined
}
function clear () {
data.length = 0
return data
}
function isObject (obj) {
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
}
function isArray (obj) {
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
}
function addObject (_data) {
data.push(_data)
return data
}
function addArray (_data) {
const added = []
clear()
for (let i = 0, len = _data.length; i < len; i++) {
if (isObject(_data[i])) {
added.push(addObject(_data[i]))
}
}
return added
}
function search (crit) {
if (!crit) {
return []
}
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
}
function __setOptions_4 (_opt) {
opt = _opt || {}
opt.fuzzy = _opt.fuzzy || false
opt.limit = _opt.limit || 10
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
opt.sort = _opt.sort || NoSort
opt.exclude = _opt.exclude || []
}
function findMatches (data, crit, strategy, opt) {
const matches = []
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
const match = findMatchesInObject(data[i], crit, strategy, opt)
if (match) {
matches.push(match)
}
}
return matches
}
function findMatchesInObject (obj, crit, strategy, opt) {
for (const key in obj) {
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
return obj
}
}
}
function isExcluded (term, excludedTerms) {
for (let i = 0, len = excludedTerms.length; i < len; i++) {
const excludedTerm = excludedTerms[i]
if (new RegExp(excludedTerm).test(term)) {
return true
}
}
return false
}
/* globals ActiveXObject:false */
'use strict'
var _$JSONLoader_2 = {
load: load
}
function load (location, callback) {
const xhr = getXHR()
xhr.open('GET', location, true)
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
xhr.send()
}
function createStateChangeListener (xhr, callback) {
return function () {
if (xhr.readyState === 4 && xhr.status === 200) {
try {
callback(null, JSON.parse(xhr.responseText))
} catch (err) {
callback(err, null)
}
}
}
}
function getXHR () {
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
}
'use strict'
var _$OptionsValidator_3 = function OptionsValidator (params) {
if (!validateParams(params)) {
throw new Error('-- OptionsValidator: required options missing')
}
if (!(this instanceof OptionsValidator)) {
return new OptionsValidator(params)
}
const requiredOptions = params.required
this.getRequiredOptions = function () {
return requiredOptions
}
this.validate = function (parameters) {
const errors = []
requiredOptions.forEach(function (requiredOptionName) {
if (typeof parameters[requiredOptionName] === 'undefined') {
errors.push(requiredOptionName)
}
})
return errors
}
function validateParams (params) {
if (!params) {
return false
}
return typeof params.required !== 'undefined' && params.required instanceof Array
}
}
'use strict'
var _$utils_9 = {
merge: merge,
isJSON: isJSON
}
function merge (defaultParams, mergeParams) {
const mergedOptions = {}
for (const option in defaultParams) {
mergedOptions[option] = defaultParams[option]
if (typeof mergeParams[option] !== 'undefined') {
mergedOptions[option] = mergeParams[option]
}
}
return mergedOptions
}
function isJSON (json) {
try {
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
return true
}
return false
} catch (err) {
return false
}
}
var _$src_8 = {};
(function (window) {
'use strict'
let options = {
searchInput: null,
resultsContainer: null,
json: [],
success: Function.prototype,
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
templateMiddleware: Function.prototype,
sortMiddleware: function () {
return 0
},
noResultsText: 'No results found',
limit: 10,
fuzzy: false,
debounceTime: null,
exclude: []
}
let debounceTimerHandle
const debounce = function (func, delayMillis) {
if (delayMillis) {
clearTimeout(debounceTimerHandle)
debounceTimerHandle = setTimeout(func, delayMillis)
} else {
func.call()
}
}
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
/* removed: const _$Templater_7 = require('./Templater') */;
/* removed: const _$Repository_4 = require('./Repository') */;
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
const optionsValidator = _$OptionsValidator_3({
required: requiredOptions
})
/* removed: const _$utils_9 = require('./utils') */;
window.SimpleJekyllSearch = function (_options) {
const errors = optionsValidator.validate(_options)
if (errors.length > 0) {
throwError('You must specify the following required options: ' + requiredOptions)
}
options = _$utils_9.merge(options, _options)
_$Templater_7.setOptions({
template: options.searchResultTemplate,
middleware: options.templateMiddleware
})
_$Repository_4.setOptions({
fuzzy: options.fuzzy,
limit: options.limit,
sort: options.sortMiddleware,
exclude: options.exclude
})
if (_$utils_9.isJSON(options.json)) {
initWithJSON(options.json)
} else {
initWithURL(options.json)
}
const rv = {
search: search
}
typeof options.success === 'function' && options.success.call(rv)
return rv
}
function initWithJSON (json) {
_$Repository_4.put(json)
registerInput()
}
function initWithURL (url) {
_$JSONLoader_2.load(url, function (err, json) {
if (err) {
throwError('failed to get JSON (' + url + ')')
}
initWithJSON(json)
})
}
function emptyResultsContainer () {
options.resultsContainer.innerHTML = ''
}
function appendToResultsContainer (text) {
options.resultsContainer.innerHTML += text
}
function registerInput () {
options.searchInput.addEventListener('input', function (e) {
if (isWhitelistedKey(e.which)) {
emptyResultsContainer()
debounce(function () { search(e.target.value) }, options.debounceTime)
}
})
}
function search (query) {
if (isValidQuery(query)) {
emptyResultsContainer()
render(_$Repository_4.search(query), query)
}
}
function render (results, query) {
const len = results.length
if (len === 0) {
return appendToResultsContainer(options.noResultsText)
}
for (let i = 0; i < len; i++) {
results[i].query = query
appendToResultsContainer(_$Templater_7.compile(results[i]))
}
}
function isValidQuery (query) {
return query && query.length > 0
}
function isWhitelistedKey (key) {
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
}
function throwError (message) {
throw new Error('SimpleJekyllSearch --- ' + message)
}
})(window)
}());
</script>
<!-- Configuration -->
<script>
SimpleJekyllSearch({
searchInput: document.getElementById('search-input'),
resultsContainer: document.getElementById('results-container'),
json: '/search.json',
//searchResultTemplate: '<li><a href="https://static.rnmkcy.eu{url}">{date}&nbsp;{title}</a></li>'
searchResultTemplate: '<li><a href="{url}">{date}&nbsp;{title}</a></li>'
})
</script>
<!-- Fin déclaration champ de recherche --></div><nav class="navigation">
<ul><li class="navigation__item"><a href="/archive.html">Etiquettes</a></li><li class="navigation__item"><a href="/htmldoc.html">Documents</a></li><li class="navigation__item"><a href="/liens_ttrss.html">Liens</a></li><li class="navigation__item"><a href="/aide-jekyll-text-theme.html">Aide</a></li></ul>
</nav></div>
</header>
</div><div class="page__content"><div class ="main"><div class="grid grid--reverse">
<div class="col-main cell cell--auto"><!-- start custom main top snippet --><div id="results-container" class="search-result js-search-result"></div><!-- end custom main top snippet -->
<article itemscope itemtype="http://schema.org/Article"><div class="article__header"><header><h1 style="color:Tomato;">Ntfy service de notification</h1></header></div><meta itemprop="headline" content="Ntfy service de notification"><div class="article__info clearfix"><ul class="left-col menu"><li>
<a class="button button--secondary button--pill button--sm"
href="/archive.html?tag=divers">divers</a>
</li></ul><ul class="right-col menu"><li>
<i class="far fa-calendar-alt"></i>&nbsp;<span title="Création" style="color:#FF00FF">22&nbsp;févr.&nbsp;2024</span>
2024-11-07 22:37:17 +01:00
<span title="Modification" style="color:#00FF7F">&nbsp;2&nbsp;nov.&nbsp;&nbsp;2024</span></li></ul></div><meta itemprop="datePublished" content="2024-11-02T00:00:00+01:00">
2024-10-31 20:18:37 +01:00
<meta itemprop="keywords" content="divers"><div class="js-article-content">
<div class="layout--article"><!-- start custom article top snippet -->
<style>
#myBtn {
display: none;
position: fixed;
bottom: 10px;
right: 10px;
z-index: 99;
font-size: 12px;
font-weight: bold;
border: none;
outline: none;
background-color: white;
color: black;
cursor: pointer;
padding: 5px;
border-radius: 4px;
}
#myBtn:hover {
background-color: #555;
}
</style>
<button onclick="topFunction()" id="myBtn" title="Haut de page">&#8679;</button>
<script>
//Get the button
var mybutton = document.getElementById("myBtn");
// When the user scrolls down 20px from the top of the document, show the button
window.onscroll = function() {scrollFunction()};
function scrollFunction() {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
mybutton.style.display = "block";
} else {
mybutton.style.display = "none";
}
}
// When the user clicks on the button, scroll to the top of the document
function topFunction() {
document.body.scrollTop = 0;
document.documentElement.scrollTop = 0;
}
</script>
<!-- end custom article top snippet -->
<div class="article__content" itemprop="articleBody"><details>
<summary><b>Afficher/cacher Sommaire</b></summary>
<!-- affichage sommaire -->
<div class="toc-aside js-toc-root"></div>
</details><p><em><a href="https://ntfy.sh/">Ntfy</a>, qui se prononce “notify”, est un service de notification ultra léger, permettant denvoyer des messages vers un smartphone ou un ordinateur via de simples scripts, sans besoin de compte et totalement gratuitement !</em></p>
2024-11-07 22:37:17 +01:00
<ul>
<li><a href="https://docs.ntfy.sh/publish/">Publishing</a></li>
<li><a href="https://docs.ntfy.sh/emojis/">Emoji reference</a></li>
<li><a href="https://gist.github.com/BuonOmo/77b75349c517defb01ef1097e72227af">UTF-8 emojis in bash or zsh</a></li>
</ul>
2024-10-31 20:18:37 +01:00
<h2 id="ntfy">Ntfy</h2>
<h3 id="installation">Installation</h3>
<p>Le CLI de ntfy vous permet de publier des messages, de vous abonner à des sujets et dhéberger vous-même votre propre serveur ntfy. Cest très simple. Il suffit dinstaller le binaire, le paquet ou limage Docker, de le configurer et de lexécuter.</p>
<p class="info">Les étapes suivantes ne sont nécessaires que si vous voulez héberger votre propre serveur ntfy ou si vous voulez utiliser le CLI ntfy. Si vous voulez juste envoyer des messages en utilisant ntfy.sh, vous navez pas besoin dinstaller quoi que ce soit. Vous pouvez simplement utiliser curl.</p>
<p>Le serveur ntfy se présente sous la forme dun binaire lié statiquement et est livré sous forme de paquetage tarball, deb/rpm et sous forme dimage Docker. Nous supportons amd64, armv7 et arm64.<br />
Veuillez consulter la <a href="https://github.com/binwiederhier/ntfy/releases">page des versions</a> pour les binaires et les paquets deb/rpm.</p>
<h4 id="debian">Debian</h4>
<p>manuellement</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://github.com/binwiederhier/ntfy/releases/download/v2.8.0/ntfy_2.8.0_linux_amd64.deb
<span class="nb">sudo </span>dpkg <span class="nt">-i</span> ntfy_<span class="k">*</span>.deb
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>ntfy
</code></pre></div></div>
<p>Avec le dépôt</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
sudo apt install apt-transport-https
sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
&gt; /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
sudo systemctl start ntfy
</code></pre></div></div>
<h4 id="arch-linux">Arch Linux</h4>
<p>2 méthodes</p>
<p><strong>Méthode 1</strong><br />
Vous pouvez récupérer le fichier binaire ntfy depuis le <a href="https://github.com/binwiederhier/ntfy/releases">dépôt GitHub</a></p>
<p>Créer un utilisateur système nommé “ntfy”<br />
Utilisez loption -r (system) pour créer un compte dutilisateur système</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo useradd -r ntfy
</code></pre></div></div>
<p>Télécharger la dernière version, décompresser, copier binaire dans /usr/local/bin et modifier les droits</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Téléchargement</span>
wget https://github.com/binwiederhier/ntfy/releases/download/v2.11.0/ntfy_2.11.0_linux_amd64.tar.gz
<span class="c"># Décompresser</span>
<span class="nb">tar </span>xzvf ntfy_2.11.0_linux_amd64.tar.gz
<span class="c"># copier binaire</span>
<span class="nb">sudo cp </span>ntfy_2.11.0_linux_amd64/ntfy /usr/local/bin/
<span class="c"># modifier les droits</span>
<span class="nb">sudo chown </span>ntfy:ntfy /usr/local/bin/ntfy
</code></pre></div></div>
<p><strong>Méthode 2</strong><br />
installer le paquet</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>yay -S --noconfirm ntfysh-bin
</code></pre></div></div>
<p>Création groupe et utilisateur à linstallation</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Creating group 'ntfy' with GID 951.
Creating user 'ntfy' (ntfy user) with UID 951 and GID 951.
</code></pre></div></div>
<h3 id="configuration-du-serveur-ntfy">Configuration du serveur ntfy</h3>
<p>Le serveur ntfy peut être configuré de trois manières :</p>
<ol>
<li>Par défaut, en utilisant un fichier de configuration (typiquement dans <code class="language-plaintext highlighter-rouge">/etc/ntfy/server.yml</code>)</li>
<li>via des arguments de ligne de commande</li>
<li>ou en utilisant des variables denvironnement.</li>
</ol>
<h4 id="ntfy-derrière-un-proxy">ntfy derrière un proxy</h4>
<p>Si vous exécutez ntfy derrière un proxy, vous devez activer le drapeau behind-proxy. Cela demandera à la logique de limitation de taux dutiliser len-tête X-Forwarded-For comme identifiant principal dun visiteur, plutôt que ladresse IP distante. Si lindicateur “behind-proxy” nest pas activé, tous les visiteurs seront comptés comme un seul, car du point de vue du serveur ntfy, ils partagent tous ladresse IP du proxy.</p>
<p>Fichier <strong>/etc/ntfy/server.yml</strong>, les modifications</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">listen-http</span><span class="pi">:</span> <span class="s2">"</span><span class="s">127.0.0.1:8100"</span>
<span class="na">behind-proxy</span><span class="pi">:</span> <span class="kc">true</span>
</code></pre></div></div>
<p>Démarrer le service</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl start ntfy
</code></pre></div></div>
<h4 id="proxy-nginx">Proxy Nginx</h4>
<p>Le fichier proxy <code class="language-plaintext highlighter-rouge">/etc/nginx/conf.d/noti.rnmkcy.eu.conf</code></p>
<div class="language-nginx highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">server</span> <span class="p">{</span>
<span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
<span class="kn">listen</span> <span class="s">[::]:80</span><span class="p">;</span>
<span class="kn">server_name</span> <span class="s">noti.rnmkcy.eu</span><span class="p">;</span>
<span class="c1"># redirect all plain HTTP requests to HTTPS</span>
<span class="kn">return</span> <span class="mi">301</span> <span class="s">https://noti.rnmkcy.eu</span><span class="nv">$request_uri</span><span class="p">;</span>
<span class="p">}</span>
<span class="k">server</span> <span class="p">{</span>
<span class="c1"># ipv4 listening port/protocol</span>
<span class="kn">listen</span> <span class="mi">443</span> <span class="s">ssl</span> <span class="s">http2</span><span class="p">;</span>
<span class="c1"># ipv6 listening port/protocol</span>
<span class="kn">listen</span> <span class="s">[::]:443</span> <span class="s">ssl</span> <span class="s">http2</span><span class="p">;</span>
<span class="kn">server_name</span> <span class="s">noti.rnmkcy.eu</span><span class="p">;</span>
<span class="kn">include</span> <span class="n">/etc/nginx/conf.d/security.conf.inc</span><span class="p">;</span>
<span class="kn">location</span> <span class="n">/</span> <span class="p">{</span>
<span class="kn">proxy_pass</span> <span class="s">http://127.0.0.1:8100</span><span class="p">;</span>
<span class="kn">proxy_http_version</span> <span class="mf">1.1</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">Upgrade</span> <span class="nv">$http_upgrade</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">Connection</span> <span class="s">"upgrade"</span><span class="p">;</span>
<span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span>
<span class="kn">proxy_connect_timeout</span> <span class="mi">3m</span><span class="p">;</span>
<span class="kn">proxy_send_timeout</span> <span class="mi">3m</span><span class="p">;</span>
<span class="kn">proxy_read_timeout</span> <span class="mi">3m</span><span class="p">;</span>
<span class="kn">client_max_body_size</span> <span class="mi">0</span><span class="p">;</span> <span class="c1"># Stream request body to backend</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Vérifier et recharger nginx</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo nginx -t &amp;&amp; sudo systemctl reload nginx
</code></pre></div></div>
<p>Première connexion depuis un navigateur firefox <a href="https://noti.rnmkcy.eu">https://noti.rnmkcy.eu</a> <br />
<img src="/images/ntfy01.png" alt="" /></p>
<h3 id="instance-publique">Instance publique</h3>
<p>Après avoir installé le serveur ntfy, accessibilité lien <a href="https://noti.rnmkcy.eu">https://noti.rnmkcy.eu</a></p>
<p>Installer lapplication android ntfy<br />
Pour sabonner à un sujet, il suffit de lancer lapplication et de cliquer sur le bouton <code class="language-plaintext highlighter-rouge">+</code><br />
<img src="/images/ntfy06.png" alt="" width="200" /></p>
<p>Nous écrivons ensuite le nom du sujet auquel nous voulons nous abonner (le nom est complètement arbitraire), et, pour utiliser notre instance Ntfy auto-hébergée, nous cochons « Utiliser un serveur différent » et entrons lIP de notre serveur ; enfin, nous cliquons sur « ABONNER »<br />
<img src="/images/ntfy07.png" alt="" width="200" /></p>
<p>Pour envoyer une notification au sujet, il suffit denvoyer une requête POST ou PUT au serveur, en utilisant le langage de programmation de notre choix ou notre outil de ligne de commande préféré (commande <code class="language-plaintext highlighter-rouge">ntfy pub</code> ou une requête POST via curl).</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -d "Ceci est une notification ntfy" https://noti.rnmkcy.eu/notif_infos
ntfy pub notif_infos "Ceci est une notification ntfy"
</code></pre></div></div>
<p>La notification push devrait apparaître sur notre appareil client android<br />
<img src="/images/ntfy08.png" alt="" width="200" /></p>
<p>Affichage des messages au format JSON</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"topic"</span><span class="p">:</span><span class="s2">"monsujet"</span><span class="p">,</span><span class="nl">"message"</span><span class="p">:</span><span class="s2">"Ceci est une notification ntfy"</span><span class="p">,</span><span class="nl">"time"</span><span class="p">:</span><span class="mi">1622656800</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p><a href="https://docs.ntfy.sh/examples/#cronjobs">Exemples dutilisation</a><br />
<a href="https://www.pofilo.fr/post/2023/09/10-alerte-connexion-ssh/">Recevoir une alerte à chaque connexion SSH en utilisant ntfy</a></p>
<h3 id="instance-privée">Instance privée</h3>
<h4 id="configurer-serveryml">Configurer server.yml</h4>
<p>La façon la plus simple de configurer une instance privée est de définir <code class="language-plaintext highlighter-rouge">auth-default-access: deny-all</code> dans le fichier <code class="language-plaintext highlighter-rouge">/etc/ntfy/server.yml</code> :</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">auth-file</span><span class="pi">:</span> <span class="s">/var/lib/ntfy/user.db</span>
<span class="na">auth-default-access</span><span class="pi">:</span> <span class="s2">"</span><span class="s">deny-all"</span>
</code></pre></div></div>
<p>Le fichier de configuration <code class="language-plaintext highlighter-rouge">/etc/ntfy/server.yml</code></p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">listen-http</span><span class="pi">:</span> <span class="s2">"</span><span class="s">127.0.0.1:8100"</span>
<span class="na">auth-file</span><span class="pi">:</span> <span class="s">/var/lib/ntfy/user.db</span>
<span class="na">auth-default-access</span><span class="pi">:</span> <span class="s2">"</span><span class="s">deny-all"</span>
<span class="na">behind-proxy</span><span class="pi">:</span> <span class="kc">true</span>
<span class="c1"># web-root: "disable"</span>
</code></pre></div></div>
<p>Explications</p>
<ul>
<li>Listen-http - Le port sur lequel le serveur Web HTTP écoute (par défaut = 80). Vous pouvez également ajouter une adresse IP à laquelle vous connecter (<code class="language-plaintext highlighter-rouge">listen-http : "127.0.0.1:8100"</code>)</li>
<li><strong>auth-file</strong> - Emplacement du user.dbfichier dauthentification.</li>
<li><strong>auth-default-access</strong> - Définir ceci sur <strong>“deny-all”</strong> forcera tous les sujets à être privés par défaut.</li>
<li><strong>behind-proxy</strong> - Si ntfy est déployé derrière un proxy comme Caddy, nginx, etc., alors cela doit être défini sur true.</li>
<li><strong>web-root</strong> - Emplacement du répertoire racine pour ntfy. Le réglage sur « désactiver » désactive le client Web.</li>
</ul>
<p>Redémarrer le serveur ntfy</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo systemctl restart ntfy
</code></pre></div></div>
<h4 id="créer-utilisateur">Créer utilisateur</h4>
<p>Passer en mode su : <code class="language-plaintext highlighter-rouge">sudo -s</code></p>
<p><strong>Créer utilisateur dans NTFY</strong> (<a href="https://docs.ntfy.sh/config/#users-and-roles">Users and roles</a>)<br />
Puisque nous avons déjà défini l attribut <code class="language-plaintext highlighter-rouge">auth-file</code> et <code class="language-plaintext highlighter-rouge">auth-default-access</code>, nous pouvons passer à lajout de notre premier utilisateur administrateur “notif”.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy user add --role=admin notif
</code></pre></div></div>
<p>Lappel de cette commande avec un nom dutilisateur vous invitera automatiquement à créer un mot de passe pour le nouvel utilisateur.<br />
Saisir un mot de passe pour “notif”<br />
<code class="language-plaintext highlighter-rouge">user leno added with role admin</code></p>
<p>Une fois lutilisateur créé, nous pouvons labonner à notre premier sujet de notification.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy access notif notif_infos rw
</code></pre></div></div>
<p>Explications</p>
<ul>
<li><strong>rw</strong> spécifie le niveau daccès de lutilisateur <strong>notif</strong> pour le sujet <strong>notif_infos</strong>. Puisque cet utilisateur sera utilisé pour envoyer des notifications, nous devons lui donner la possibilité décrire sur le sujet.</li>
<li><strong>wo</strong> “write-only” (écriture seule)</li>
<li><strong>rw</strong> “read-write” (lecture-écriture)</li>
<li><strong>ro</strong> “read-only” (lecture seule)</li>
</ul>
<p>Vous pouvez modifier les autorisations daccès à tout moment, en réexécutant la commande ci-dessus avec un niveau daccès différent.</p>
<p>Créez maintenant un deuxième utilisateur <strong>notifmob</strong> avec les autorisations sur le sujet en lecture seul.<br />
Ce deuxième utilisateur sera utilisé pour notre application mobile.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy user add notifmob
ntfy access notifmob notif_infos ro
</code></pre></div></div>
<p>Liste des commandes pour la gestion des utilisateurs</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo ntfy user list # Shows list of users (alias: 'ntfy access')
sudo ntfy user add phil # Add regular user phil
sudo ntfy user add --role=admin phil # Add admin user phil
sudo ntfy user del phil # Delete user phil
sudo ntfy user change-pass phil # Change password for user phil
sudo ntfy user change-role phil admin # Make user phil an admin
sudo ntfy user change-tier phil pro # Change phil's tier to "pro"
</code></pre></div></div>
<p>Une fois que vous avez fait cela, vous pouvez publier et vous abonner en utilisant lauthentification de base avec le nom dutilisateur/mot de passe donné. Veillez à utiliser HTTPS pour éviter les écoutes et lexposition de votre mot de passe.</p>
<h2 id="authentification">Authentification</h2>
<p><a href="https://docs.ntfy.sh">Doc ntfy</a></p>
<h3 id="utilisateur--mot-de-passe">Utilisateur + mot de passe</h3>
<p><a href="https://docs.ntfy.sh/publish/#username-password">Username + password</a><br />
Lorsque lagent utilisateur souhaite envoyer des informations dauthentification au serveur, il peut utiliser le champ den-tête <strong>Authorization</strong></p>
<p>Le champ den-tête <strong>Authorization</strong> est construit comme suit :</p>
<ul>
<li>Le nom dutilisateur et le mot de passe sont combinés avec un seul deux-points ( <code class="language-plaintext highlighter-rouge">:</code>). Cela signifie que le nom dutilisateur lui-même ne peut pas contenir de deux points.</li>
<li>La chaîne résultante est encodée en une séquence doctets. Le jeu de caractères à utiliser pour cet encodage est par défaut non spécifié, tant quil est compatible avec US-ASCII, mais le serveur peut suggérer lutilisation dUTF-8 en envoyant le paramètre charset.</li>
<li>La chaîne résultante est encodée à laide dune variante de Base64 (+/ et avec remplissage).</li>
<li>La méthode dautorisation et un caractère despacement (par exemple “Basic “) sont ensuite ajoutés à la chaîne encodée.</li>
</ul>
<p>Par exemple, si le navigateur utilise <em>Aladdin</em> comme nom dutilisateur et <em>open sesame</em> comme mot de passe, la valeur du champ est le codage Base64 de <em>Aladdin:open sesame</em>, ou <em>QWxhZGRpbjpvcGVuIHNlc2FtZQ==</em>. Le champ den-tête Authorization se présente alors comme suit :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
</code></pre></div></div>
<p>Pour générer len-tête dautorisation, utilisez la norme base64 pour encoder les <code class="language-plaintext highlighter-rouge">&lt;nom d'utilisateur&gt;:&lt;mot de passe&gt;</code> séparés par deux points et ajoutez-y le mot Basic, cest-à-dire Authorization : Basic base64(<code class="language-plaintext highlighter-rouge">&lt;username&gt;:&lt;password&gt;</code>).<br />
Voici un pseudo-code qui, je lespère, lexplique mieux :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>username = "testuser"
password = "fakepassword"
authHeader = "Basic " + base64(username + " :" + password) // -&gt; Basic dGVzdHVzZXI6ZmFrZXBhc3N3b3Jk
</code></pre></div></div>
<p>La commande suivante génère la valeur appropriée pour vous sur les systèmes *nix :</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>echo "Basic $(echo -n 'testuser:fakepassword' | base64)"
</code></pre></div></div>
<p>INFO: Entête autorisation au format <strong>utilisateur:Mot_de_passe</strong> base 64 <br />
<code class="language-plaintext highlighter-rouge">echo -n 'leno:mot de passe' | base64</code><br />
Renvoie &gt; <code class="language-plaintext highlighter-rouge">eWFubjptb3QgZGUgcGFzc2U=</code></p>
<h4 id="curl">curl</h4>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="se">\</span>
<span class="nt">-u</span> leno:mypass <span class="se">\</span>
<span class="nt">-d</span> <span class="s2">"accès avec authentification"</span> <span class="se">\</span>
https://noti.rnmkcy.eu/yan_infos
</code></pre></div></div>
<h4 id="post-http">POST http</h4>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="se">\</span>
<span class="nt">-i</span> <span class="nt">-s</span> <span class="nt">-X</span> POST <span class="nt">-H</span> <span class="s2">"Authorization: Basic bGVubzpRpbjpvsZW5vNDk="</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s2">"POST/HTTP accès avec authentification"</span> <span class="s2">"https://noti.rnmkcy.eu/yan_infos"</span>
</code></pre></div></div>
<h4 id="javascript">JavaScript</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://ntfy.example.com/mysecrets</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// PUT works too</span>
<span class="na">body</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Look ma, with auth</span><span class="dl">'</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">Authorization</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Basic cGhpbDpteXBhc3M=</span><span class="dl">'</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<h4 id="go">Go</h4>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">req</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">http</span><span class="o">.</span><span class="n">NewRequest</span><span class="p">(</span><span class="s">"POST"</span><span class="p">,</span> <span class="s">"https://ntfy.example.com/mysecrets"</span><span class="p">,</span>
<span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"Look ma, with auth"</span><span class="p">))</span>
<span class="n">req</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Authorization"</span><span class="p">,</span> <span class="s">"Basic cGhpbDpteXBhc3M="</span><span class="p">)</span>
<span class="n">http</span><span class="o">.</span><span class="n">DefaultClient</span><span class="o">.</span><span class="n">Do</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
</code></pre></div></div>
<h4 id="python">Python</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">requests</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="sh">"</span><span class="s">https://ntfy.example.com/mysecrets</span><span class="sh">"</span><span class="p">,</span>
<span class="n">data</span><span class="o">=</span><span class="sh">"</span><span class="s">Look ma, with auth</span><span class="sh">"</span><span class="p">,</span>
<span class="n">headers</span><span class="o">=</span><span class="p">{</span>
<span class="sh">"</span><span class="s">Authorization</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Basic cGhpbDpteXBhc3M=</span><span class="sh">"</span>
<span class="p">})</span>
</code></pre></div></div>
<h4 id="php">PHP</h4>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">file_get_contents</span><span class="p">(</span><span class="s1">'https://ntfy.example.com/mysecrets'</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="nb">stream_context_create</span><span class="p">([</span>
<span class="s1">'http'</span> <span class="o">=&gt;</span> <span class="p">[</span>
<span class="s1">'method'</span> <span class="o">=&gt;</span> <span class="s1">'POST'</span><span class="p">,</span> <span class="c1">// PUT also works</span>
<span class="s1">'header'</span> <span class="o">=&gt;</span>
<span class="s1">'Content-Type: text/plain\r\n'</span> <span class="mf">.</span>
<span class="s1">'Authorization: Basic cGhpbDpteXBhc3M='</span><span class="p">,</span>
<span class="s1">'content'</span> <span class="o">=&gt;</span> <span class="s1">'Look ma, with auth'</span>
<span class="p">]</span>
<span class="p">]));</span>
</code></pre></div></div>
<h4 id="ntfy-cli">ntfy CLI</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy publish \
-u phil:mypass \
ntfy.example.com/mysecrets \
"Look ma, with auth"
</code></pre></div></div>
<h3 id="jetons-tokens">Jetons (Tokens)</h3>
<p><em>Le jeton peut être utilisé à la place du nom dutilisateur et du mot de passe lors de lenvoi de messages. (<a href="https://docs.ntfy.sh/config/#access-tokens">Access tokens</a>)</em></p>
<p>En plus de lauthentification par nom dutilisateur/mot de passe, ntfy fournit également une authentification par jetons daccès. Les jetons daccès sont utiles pour éviter davoir à configurer votre mot de passe dans plusieurs applications de publication/abonnement. Par exemple, vous pouvez utiliser un jeton dédié pour publier à partir de votre hôte de sauvegarde, et un autre à partir de votre système domotique.</p>
<p>Vous pouvez créer des jetons daccès en utilisant la commande <code class="language-plaintext highlighter-rouge">ntfy token</code>, ou dans lapplication web dans la section “Compte” (lorsque vous êtes connecté). Voir les <a href="https://docs.ntfy.sh/config/#access-tokens">jetons daccès</a> pour plus de détails.</p>
<p>Une fois le jeton daccès créé, vous pouvez lutiliser pour vous authentifier auprès du serveur ntfy, par exemple lorsque vous publiez ou vous abonnez à des sujets.</p>
<p>La commande ntfy token permet de gérer les jetons daccès des utilisateurs. Les jetons peuvent avoir des étiquettes, et ils peuvent expirer automatiquement (ou ne jamais expirer). Chaque utilisateur peut avoir jusquà 20 jetons (codés en dur).</p>
<p>Exemples de commandes (tapez ntfy token help ou ntfy token COMMAND help pour plus de détails)</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy token list # Affiche la liste des jetons pour tous les utilisateurs
ntfy token list phil # Affiche la liste des jetons de l'utilisateur phil
ntfy token add phil # Crée un jeton pour l'utilisateur phil qui n'expire jamais
ntfy token add --expires=2d phil # Crée un jeton pour l'utilisateur phil qui expire dans 2 jours
ntfy token remove phil tk_th2sxr... # Supprimer le jeton
</code></pre></div></div>
<p>Création dun jeton daccès :</p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy token add notif
ntfy token add <span class="nt">--expires</span><span class="o">=</span>30d <span class="nt">--label</span><span class="o">=</span><span class="s2">"backups"</span> notif
ntfy token list
utilisateur notif
- tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 <span class="o">(</span>sauvegardes<span class="o">)</span>, expire le 15 mars 23 14:33 EDT, accédé depuis 0.0.0.0 le 13 février 23 13:33 EST
</code></pre></div></div>
<p>Une fois le jeton daccès créé, vous pouvez lutiliser pour vous authentifier auprès du serveur ntfy, par exemple lorsque vous publiez ou vous abonnez à des sujets.</p>
<p>Voici un exemple utilisant Bearer auth, avec le jeton tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 :</p>
<h4 id="curl-1">curl</h4>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s2">"Look ma, with auth"</span> <span class="se">\</span>
https://ntfy.example.com/mysecrets
</code></pre></div></div>
<h4 id="post-http-1">POST http</h4>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST /mysecrets HTTP/1.1
Host: ntfy.example.com
Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
Look ma, with auth
</code></pre></div></div>
<h4 id="javascript-1">JavaScript</h4>
<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">fetch</span><span class="p">(</span><span class="dl">'</span><span class="s1">https://ntfy.example.com/mysecrets</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
<span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// PUT works too</span>
<span class="na">body</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Look ma, with auth</span><span class="dl">'</span><span class="p">,</span>
<span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
<span class="dl">'</span><span class="s1">Authorization</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2</span><span class="dl">'</span>
<span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>
<h4 id="go-1">Go</h4>
<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">req</span><span class="p">,</span> <span class="n">_</span> <span class="o">:=</span> <span class="n">http</span><span class="o">.</span><span class="n">NewRequest</span><span class="p">(</span><span class="s">"POST"</span><span class="p">,</span> <span class="s">"https://ntfy.example.com/mysecrets"</span><span class="p">,</span>
<span class="n">strings</span><span class="o">.</span><span class="n">NewReader</span><span class="p">(</span><span class="s">"Look ma, with auth"</span><span class="p">))</span>
<span class="n">req</span><span class="o">.</span><span class="n">Header</span><span class="o">.</span><span class="n">Set</span><span class="p">(</span><span class="s">"Authorization"</span><span class="p">,</span> <span class="s">"Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"</span><span class="p">)</span>
<span class="n">http</span><span class="o">.</span><span class="n">DefaultClient</span><span class="o">.</span><span class="n">Do</span><span class="p">(</span><span class="n">req</span><span class="p">)</span>
</code></pre></div></div>
<h4 id="python-1">Python</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">requests</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="sh">"</span><span class="s">https://ntfy.example.com/mysecrets</span><span class="sh">"</span><span class="p">,</span>
<span class="n">data</span><span class="o">=</span><span class="sh">"</span><span class="s">Look ma, with auth</span><span class="sh">"</span><span class="p">,</span>
<span class="n">headers</span><span class="o">=</span><span class="p">{</span>
<span class="sh">"</span><span class="s">Authorization</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2</span><span class="sh">"</span>
<span class="p">})</span>
</code></pre></div></div>
<h4 id="php-1">PHP</h4>
<div class="language-php highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">file_get_contents</span><span class="p">(</span><span class="s1">'https://ntfy.example.com/mysecrets'</span><span class="p">,</span> <span class="kc">false</span><span class="p">,</span> <span class="nb">stream_context_create</span><span class="p">([</span>
<span class="s1">'http'</span> <span class="o">=&gt;</span> <span class="p">[</span>
<span class="s1">'method'</span> <span class="o">=&gt;</span> <span class="s1">'POST'</span><span class="p">,</span> <span class="c1">// PUT also works</span>
<span class="s1">'header'</span> <span class="o">=&gt;</span>
<span class="s1">'Content-Type: text/plain\r\n'</span> <span class="mf">.</span>
<span class="s1">'Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2'</span><span class="p">,</span>
<span class="s1">'content'</span> <span class="o">=&gt;</span> <span class="s1">'Look ma, with auth'</span>
<span class="p">]</span>
<span class="p">]));</span>
</code></pre></div></div>
<h4 id="ntfy-cli-1">ntfy CLI</h4>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy publish \
--token tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 \
ntfy.example.com/mysecrets \
"Look ma, with auth"
</code></pre></div></div>
<h2 id="android">Android</h2>
<h3 id="installer-android-ntfy">Installer android ntfy</h3>
<p>Installer lapplication ntfy disponible sur F-Droid</p>
<p>Après ouverture de lapplication <br />
<img src="/images/ntfy02.png" alt="" width="200" /> <img src="/images/ntfy03.png" alt="" width="200" />
<img src="/images/ntfy03a.png" alt="" width="200" /> <img src="/images/ntfy04.png" alt="" width="200" /></p>
<p><code class="language-plaintext info highlighter-rouge">Il faut saisir utilisateur et mot de passe car notification privée</code></p>
<h3 id="configurer-android-ntfy-prive">Configurer android ntfy (PRIVE)</h3>
<p><strong>Configuration de lapplication mobile Android</strong><br />
Téléchargez dabord lapplication depuis <a href="https://f-droid.org/en/packages/io.heckel.ntfy/">f-droid</a> , et après son installation, lancez-la.<br />
<img src="/images/ntfy10.png" alt="" width="200" /><br />
Au premier lancement, vous serez accueilli par un écran vide sans aucun sujet. Pour configurer lapplication pour votre instance auto-hébergée, ouvrez le menu des paramètres (menu à 3 points &gt; paramètres)<br />
<img src="/images/ntfy11.png" alt="" width="200" /><br />
et accédez à « Gérer les utilisateurs » sous Général. <br />
<img src="/images/ntfy12.png" alt="" width="200" /><br />
Appuyez sur « Ajouter un nouvel utilisateur » et dans le Service URLchamp, entrez lURL que vous avez utilisée pour votre instance ntfy ci-dessus . Les champs Nom dutilisateur et Mot de passe seront le deuxième utilisateur que vous avez créé précédemment<br />
Une fois les champs remplis, appuyez sur « AJOUTER UN UTILISATEUR ». <br />
<img src="/images/ntfy13.png" alt="" width="200" /><br />
Votre section utilisateur devrait alors ressembler à limage suivante<br />
<img src="/images/ntfy14.png" alt="" width="200" /><br />
Appuyez sur la flèche de retour (ou faites glisser votre doigt vers larrière), entrez le paramètre <strong>Serveur par défaut</strong> et saisissez à nouveau votre URL ntfy.<br />
<img src="/images/ntfy15.png" alt="" width="200" /><br />
Maintenant, appuyez ou faites glisser votre doigt pour revenir à lécran daccueil. Pour désactiver la notification de livraison instantanée, qui saffiche dans votre tiroir de notification, faites glisser votre doigt vers le sélecteur dapplication et appuyez longuement sur licône de lapplication ntfy <img src="/images/ntfy16.png" alt="" height="40" /><br />
Appuyez ensuite sur « Informations sur lapplication ».<br />
<img src="/images/ntfy17.png" alt="" width="200" /><br />
Sur la page des paramètres de lapplication, appuyez sur <strong>Notifications</strong> et désactivez la notification « Service dabonnement » sous « Autre » ???.<br />
<img src="/images/ntfy18.png" alt="" width="200" /></p>
<p><strong>Sabonner à un sujet</strong><br />
<code class="language-plaintext highlighter-rouge">ATTENTION :Pour s'abonner à un sujet, il faut que le sujet existe.</code><br />
Sur lécran daccueil de lapplication, appuyez sur le bouton vert « + » et saisissez le nom du sujet que vous avez créé précédemment lors de l étape daccès utilisateur . Appuyez ensuite sur « SABONNER ».<br />
<img src="/images/ntfy19.png" alt="" width="200" /><br />
Si cela est fait correctement, votre nouveau sujet devrait apparaître sur lécran daccueil de votre application.</p>
<p>Les paramètres de labonnement peuvent être ajustés en appuyant dessus puis en appuyant sur le menu à 3 points. Appuyez ensuite sur « Paramètres dabonnement » dans le menu. Ici, vous pouvez modifier les préférences de notification ainsi que licône et le nom daffichage de labonnement.</p>
<h2 id="ligne-de-commande">Ligne de commande</h2>
<h3 id="sécuriser">Sécuriser</h3>
<p>Pour éviter que nimporte qui puisse envoyer ou recevoir des messages, vous pouvez utiliser un mot de passe.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy publish -u admin:myp@ssw0rd ntfy.example.com/monsujet "This is a message"
curl -u admin:myp@ssw0rd -d "This is a message" https://ntfy.example.com/monsujet
</code></pre></div></div>
<p>Cela implique un hébergement du serveur ntfy</p>
<h3 id="envoi">Envoi</h3>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy publish -u utilisateur:mot_de_passe ntfysrv.eu/test "Nouveau test depuis PC1 en ligne de commande" |jq
</code></pre></div></div>
<p>Affichage message expédié</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"WqRBXcEmOoqv"</span><span class="p">,</span><span class="w">
</span><span class="nl">"time"</span><span class="p">:</span><span class="w"> </span><span class="mi">1708692353</span><span class="p">,</span><span class="w">
</span><span class="nl">"expires"</span><span class="p">:</span><span class="w"> </span><span class="mi">1708735553</span><span class="p">,</span><span class="w">
</span><span class="nl">"event"</span><span class="p">:</span><span class="w"> </span><span class="s2">"message"</span><span class="p">,</span><span class="w">
</span><span class="nl">"topic"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test"</span><span class="p">,</span><span class="w">
</span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Nouveau test depuis PC1 en ligne de commande"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<h3 id="réception">Réception</h3>
<p>Se mettre en attente dun message</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy sub -u utilisateur:mot_de_passe ntfysrv.eu/test |jq
</code></pre></div></div>
<p>Affichage à la réception dun message</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"BY9zD5GQqCI9"</span><span class="p">,</span><span class="w">
</span><span class="nl">"time"</span><span class="p">:</span><span class="w"> </span><span class="mi">1708693062</span><span class="p">,</span><span class="w">
</span><span class="nl">"expires"</span><span class="p">:</span><span class="w"> </span><span class="mi">1708736262</span><span class="p">,</span><span class="w">
</span><span class="nl">"event"</span><span class="p">:</span><span class="w"> </span><span class="s2">"message"</span><span class="p">,</span><span class="w">
</span><span class="nl">"topic"</span><span class="p">:</span><span class="w"> </span><span class="s2">"test"</span><span class="p">,</span><span class="w">
</span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Firefox PC1"</span><span class="p">,</span><span class="w">
</span><span class="nl">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Nouvel essai depuis le navigateur"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Utilitaire jq pour un affichage au format json</p>
<h3 id="réception-avec-notify-send">Réception avec notify-send</h3>
<p>Pour afficher vos notifications directement sur votre environnement de bureau, vous pouvez utiliser la commande <code class="language-plaintext highlighter-rouge">ntfy sub</code> avec notify-send.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy sub -u utilisateur:mot_de_passe ntfysrv.eu/test 'notify-send -t 0 "ntfy" "$m"'
</code></pre></div></div>
<p><img src="/images/ntfy05.png" alt="" /></p>
<p>Voir tous les anciens messages</p>
<p>Si vous souhaitez voir lhistorique des messages, vous pouvez utiliser le paramètre <code class="language-plaintext highlighter-rouge">-s all</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ntfy sub -s all monsujet
</code></pre></div></div>
<h3 id="notification-token--curl">Notification token + curl</h3>
<p>Lutilisateur <strong>notif</strong> a les droits en lecture/écriture et possède une clé token qui englobe utilisateur/mot de passe</p>
<p>Ligne de commande</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl -H "Title: Test envoi notification" -u :token_utilisateur_notif https://noti.rnmkcy.eu/notif_infos -d "ceci est un message de test"
</code></pre></div></div>
<p>Différentes publications <a href="https://docs.ntfy.sh/publish/">Publishing</a></p>
<div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
curl <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Title: Alerte!"</span> <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"</span> <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"X-Tags: warning,mailsrv13,daily-backup"</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s2">"Envoi en cas d'erreur!"</span> <span class="se">\</span>
https://noti.rnmkcy.eu/notif_infos
curl <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Authorization: Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2"</span> <span class="se">\</span>
<span class="nt">-d</span> <span class="s2">"Look ma, with auth"</span> <span class="se">\</span>
https://ntfy.example.com/mysecrets
</code></pre></div></div>
<h3 id="alerte-sur-connexion-ssh">Alerte sur connexion SSH</h3>
<p><strong>Alerte en cas de connexion SSH</strong></p>
<p>Le fichier <code class="language-plaintext highlighter-rouge">/etc/pam.d/sshd</code></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># à la fin du fichier
session optional pam_exec.so /usr/bin/ntfy-ssh-login.sh
</code></pre></div></div>
<p>Le fichier <code class="language-plaintext highlighter-rouge">/usr/bin/ntfy-ssh-login.sh</code></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="k">if</span> <span class="o">[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">PAM_TYPE</span><span class="k">}</span><span class="s2">"</span> <span class="o">=</span> <span class="s2">"open_session"</span> <span class="o">]</span><span class="p">;</span> <span class="k">then
</span>curl <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Title: Alerte connexion SSH!"</span> <span class="se">\</span>
<span class="nt">-H</span> <span class="s2">"Authorization: Bearer tk_cpifjh59xo3zu2bgi2jyfkif4wsbf"</span> <span class="se">\</span>
<span class="nt">-H</span> prio:high <span class="se">\</span>
<span class="nt">-H</span> tags:warning <span class="se">\</span>
<span class="nt">-d</span> <span class="s2">"SSH </span><span class="k">${</span><span class="nv">HOSTNAME</span><span class="k">}</span><span class="s2"> user: </span><span class="k">${</span><span class="nv">PAM_USER</span><span class="k">}</span><span class="s2"> from </span><span class="k">${</span><span class="nv">PAM_RHOST</span><span class="k">}</span><span class="s2">"</span> <span class="se">\</span>
https://noti.rnmkcy.eu/notif_infos
<span class="k">fi</span>
</code></pre></div></div>
<p>Le rendre exécutable : <code class="language-plaintext highlighter-rouge">chmod +x /usr/bin/ntfy-ssh-login.sh</code></p>
<p>Message en cas de connexion<br />
<img src="/images/ntfy20.png" alt="" width="200" /></p>
</div>
<div class="d-print-none"><footer class="article__footer"><meta itemprop="dateModified" content="2024-02-22T00:00:00+01:00"><!-- start custom article footer snippet -->
<!-- end custom article footer snippet -->
<!--
<div align="right"><a type="application/rss+xml" href="/feed.xml" title="S'abonner"><i class="fa fa-rss fa-2x"></i></a>
&emsp;</div>
-->
</footer>
<div class="article__section-navigator clearfix"><div class="previous"><span>PRÉCÉDENT</span><a href="/2024/02/12/debian_dans_environnement_virtuel_KVM_Lenovo.html">Image cloud virtuelle Debian 12 dans un environnement KVM Lenovo</a></div><div class="next"><span>SUIVANT</span><a href="/2024/02/22/sqlite3.html">Base de données Sqlite3</a></div></div></div>
</div>
<script>(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
$(function() {
var $this ,$scroll;
var $articleContent = $('.js-article-content');
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
var scroll = hasSidebar ? '.js-page-main' : 'html, body';
$scroll = $(scroll);
$articleContent.find('.highlight').each(function() {
$this = $(this);
$this.attr('data-lang', $this.find('code').attr('data-lang'));
});
$articleContent.find('h1[id], h2[id], h3[id], h4[id], h5[id], h6[id]').each(function() {
$this = $(this);
$this.append($('<a class="anchor d-print-none" aria-hidden="true"></a>').html('<i class="fas fa-anchor"></i>'));
});
$articleContent.on('click', '.anchor', function() {
$scroll.scrollToAnchor('#' + $(this).parent().attr('id'), 400);
});
});
});
})();
</script>
</div><section class="page__comments d-print-none"></section></article><!-- start custom main bottom snippet -->
<!-- end custom main bottom snippet -->
</div>
</div></div></div></div>
</div><script>(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
var $body = $('body'), $window = $(window);
var $pageRoot = $('.js-page-root'), $pageMain = $('.js-page-main');
var activeCount = 0;
function modal(options) {
var $root = this, visible, onChange, hideWhenWindowScroll = false;
var scrollTop;
function setOptions(options) {
var _options = options || {};
visible = _options.initialVisible === undefined ? false : show;
onChange = _options.onChange;
hideWhenWindowScroll = _options.hideWhenWindowScroll;
}
function init() {
setState(visible);
}
function setState(isShow) {
if (isShow === visible) {
return;
}
visible = isShow;
if (visible) {
activeCount++;
scrollTop = $(window).scrollTop() || $pageMain.scrollTop();
$root.addClass('modal--show');
$pageMain.scrollTop(scrollTop);
activeCount === 1 && ($pageRoot.addClass('show-modal'), $body.addClass('of-hidden'));
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.on('scroll', hide);
$window.on('keyup', handleKeyup);
} else {
activeCount > 0 && activeCount--;
$root.removeClass('modal--show');
$window.scrollTop(scrollTop);
activeCount === 0 && ($pageRoot.removeClass('show-modal'), $body.removeClass('of-hidden'));
hideWhenWindowScroll && window.hasEvent('touchstart') && $window.off('scroll', hide);
$window.off('keyup', handleKeyup);
}
onChange && onChange(visible);
}
function show() {
setState(true);
}
function hide() {
setState(false);
}
function handleKeyup(e) {
// Char Code: 27 ESC
if (e.which === 27) {
hide();
}
}
setOptions(options);
init();
return {
show: show,
hide: hide,
$el: $root
};
}
$.fn.modal = modal;
});
})();
</script><div class="modal modal--overflow page__search-modal d-print-none js-page-search-modal"><script>
(function () {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
// search panel
var search = (window.search || (window.search = {}));
var useDefaultSearchBox = window.useDefaultSearchBox === undefined ?
true : window.useDefaultSearchBox ;
var $searchModal = $('.js-page-search-modal');
var $searchToggle = $('.js-search-toggle');
var searchModal = $searchModal.modal({ onChange: handleModalChange, hideWhenWindowScroll: true });
var modalVisible = false;
search.searchModal = searchModal;
var $searchBox = null;
var $searchInput = null;
var $searchClear = null;
function getModalVisible() {
return modalVisible;
}
search.getModalVisible = getModalVisible;
function handleModalChange(visible) {
modalVisible = visible;
if (visible) {
search.onShow && search.onShow();
useDefaultSearchBox && $searchInput[0] && $searchInput[0].focus();
} else {
search.onShow && search.onHide();
useDefaultSearchBox && $searchInput[0] && $searchInput[0].blur();
setTimeout(function() {
useDefaultSearchBox && ($searchInput.val(''), $searchBox.removeClass('not-empty'));
search.clear && search.clear();
window.pageAsideAffix && window.pageAsideAffix.refresh();
}, 400);
}
}
$searchToggle.on('click', function() {
modalVisible ? searchModal.hide() : searchModal.show();
});
// Char Code: 83 S, 191 /
$(window).on('keyup', function(e) {
if (!modalVisible && !window.isFormElement(e.target || e.srcElement) && (e.which === 83 || e.which === 191)) {
modalVisible || searchModal.show();
}
});
if (useDefaultSearchBox) {
$searchBox = $('.js-search-box');
$searchInput = $searchBox.children('input');
$searchClear = $searchBox.children('.js-icon-clear');
search.getSearchInput = function() {
return $searchInput.get(0);
};
search.getVal = function() {
return $searchInput.val();
};
search.setVal = function(val) {
$searchInput.val(val);
};
$searchInput.on('focus', function() {
$(this).addClass('focus');
});
$searchInput.on('blur', function() {
$(this).removeClass('focus');
});
$searchInput.on('input', window.throttle(function() {
var val = $(this).val();
if (val === '' || typeof val !== 'string') {
search.clear && search.clear();
} else {
$searchBox.addClass('not-empty');
search.onInputNotEmpty && search.onInputNotEmpty(val);
}
}, 400));
$searchClear.on('click', function() {
$searchInput.val(''); $searchBox.removeClass('not-empty');
search.clear && search.clear();
});
}
});
})();
</script><div class="search search--dark">
<div class="main">
<div class="search__header">Recherche</div>
<div class="search-bar">
<div class="search-box js-search-box">
<div class="search-box__icon-search"><i class="fas fa-search"></i></div>
<input id="search-input" type="text" />
<div class="search-box__icon-clear js-icon-clear">
<a><i class="fas fa-times"></i></a>
</div>
</div>
<button class="button button--theme-dark button--pill search__cancel js-search-toggle">
Annuler</button>
</div>
<div id="results-container" class="search-result js-search-result"></div>
</div>
</div>
<!-- Script pointing to search-script.js -->
<script>/*!
* Simple-Jekyll-Search
* Copyright 2015-2020, Christian Fei
* Licensed under the MIT License.
*/
(function(){
'use strict'
var _$Templater_7 = {
compile: compile,
setOptions: setOptions
}
const options = {}
options.pattern = /\{(.*?)\}/g
options.template = ''
options.middleware = function () {}
function setOptions (_options) {
options.pattern = _options.pattern || options.pattern
options.template = _options.template || options.template
if (typeof _options.middleware === 'function') {
options.middleware = _options.middleware
}
}
function compile (data) {
return options.template.replace(options.pattern, function (match, prop) {
const value = options.middleware(prop, data[prop], options.template)
if (typeof value !== 'undefined') {
return value
}
return data[prop] || match
})
}
'use strict';
function fuzzysearch (needle, haystack) {
var tlen = haystack.length;
var qlen = needle.length;
if (qlen > tlen) {
return false;
}
if (qlen === tlen) {
return needle === haystack;
}
outer: for (var i = 0, j = 0; i < qlen; i++) {
var nch = needle.charCodeAt(i);
while (j < tlen) {
if (haystack.charCodeAt(j++) === nch) {
continue outer;
}
}
return false;
}
return true;
}
var _$fuzzysearch_1 = fuzzysearch;
'use strict'
/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
function FuzzySearchStrategy () {
this.matches = function (string, crit) {
return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
}
}
'use strict'
var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
function LiteralSearchStrategy () {
this.matches = function (str, crit) {
if (!str) return false
str = str.trim().toLowerCase()
crit = crit.trim().toLowerCase()
return crit.split(' ').filter(function (word) {
return str.indexOf(word) >= 0
}).length === crit.split(' ').length
}
}
'use strict'
var _$Repository_4 = {
put: put,
clear: clear,
search: search,
setOptions: __setOptions_4
}
/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
function NoSort () {
return 0
}
const data = []
let opt = {}
opt.fuzzy = false
opt.limit = 10
opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
opt.sort = NoSort
opt.exclude = []
function put (data) {
if (isObject(data)) {
return addObject(data)
}
if (isArray(data)) {
return addArray(data)
}
return undefined
}
function clear () {
data.length = 0
return data
}
function isObject (obj) {
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
}
function isArray (obj) {
return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
}
function addObject (_data) {
data.push(_data)
return data
}
function addArray (_data) {
const added = []
clear()
for (let i = 0, len = _data.length; i < len; i++) {
if (isObject(_data[i])) {
added.push(addObject(_data[i]))
}
}
return added
}
function search (crit) {
if (!crit) {
return []
}
return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
}
function __setOptions_4 (_opt) {
opt = _opt || {}
opt.fuzzy = _opt.fuzzy || false
opt.limit = _opt.limit || 10
opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
opt.sort = _opt.sort || NoSort
opt.exclude = _opt.exclude || []
}
function findMatches (data, crit, strategy, opt) {
const matches = []
for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
const match = findMatchesInObject(data[i], crit, strategy, opt)
if (match) {
matches.push(match)
}
}
return matches
}
function findMatchesInObject (obj, crit, strategy, opt) {
for (const key in obj) {
if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
return obj
}
}
}
function isExcluded (term, excludedTerms) {
for (let i = 0, len = excludedTerms.length; i < len; i++) {
const excludedTerm = excludedTerms[i]
if (new RegExp(excludedTerm).test(term)) {
return true
}
}
return false
}
/* globals ActiveXObject:false */
'use strict'
var _$JSONLoader_2 = {
load: load
}
function load (location, callback) {
const xhr = getXHR()
xhr.open('GET', location, true)
xhr.onreadystatechange = createStateChangeListener(xhr, callback)
xhr.send()
}
function createStateChangeListener (xhr, callback) {
return function () {
if (xhr.readyState === 4 && xhr.status === 200) {
try {
callback(null, JSON.parse(xhr.responseText))
} catch (err) {
callback(err, null)
}
}
}
}
function getXHR () {
return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
}
'use strict'
var _$OptionsValidator_3 = function OptionsValidator (params) {
if (!validateParams(params)) {
throw new Error('-- OptionsValidator: required options missing')
}
if (!(this instanceof OptionsValidator)) {
return new OptionsValidator(params)
}
const requiredOptions = params.required
this.getRequiredOptions = function () {
return requiredOptions
}
this.validate = function (parameters) {
const errors = []
requiredOptions.forEach(function (requiredOptionName) {
if (typeof parameters[requiredOptionName] === 'undefined') {
errors.push(requiredOptionName)
}
})
return errors
}
function validateParams (params) {
if (!params) {
return false
}
return typeof params.required !== 'undefined' && params.required instanceof Array
}
}
'use strict'
var _$utils_9 = {
merge: merge,
isJSON: isJSON
}
function merge (defaultParams, mergeParams) {
const mergedOptions = {}
for (const option in defaultParams) {
mergedOptions[option] = defaultParams[option]
if (typeof mergeParams[option] !== 'undefined') {
mergedOptions[option] = mergeParams[option]
}
}
return mergedOptions
}
function isJSON (json) {
try {
if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
return true
}
return false
} catch (err) {
return false
}
}
var _$src_8 = {};
(function (window) {
'use strict'
let options = {
searchInput: null,
resultsContainer: null,
json: [],
success: Function.prototype,
searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
templateMiddleware: Function.prototype,
sortMiddleware: function () {
return 0
},
noResultsText: 'No results found',
limit: 10,
fuzzy: false,
debounceTime: null,
exclude: []
}
let debounceTimerHandle
const debounce = function (func, delayMillis) {
if (delayMillis) {
clearTimeout(debounceTimerHandle)
debounceTimerHandle = setTimeout(func, delayMillis)
} else {
func.call()
}
}
const requiredOptions = ['searchInput', 'resultsContainer', 'json']
/* removed: const _$Templater_7 = require('./Templater') */;
/* removed: const _$Repository_4 = require('./Repository') */;
/* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
const optionsValidator = _$OptionsValidator_3({
required: requiredOptions
})
/* removed: const _$utils_9 = require('./utils') */;
window.SimpleJekyllSearch = function (_options) {
const errors = optionsValidator.validate(_options)
if (errors.length > 0) {
throwError('You must specify the following required options: ' + requiredOptions)
}
options = _$utils_9.merge(options, _options)
_$Templater_7.setOptions({
template: options.searchResultTemplate,
middleware: options.templateMiddleware
})
_$Repository_4.setOptions({
fuzzy: options.fuzzy,
limit: options.limit,
sort: options.sortMiddleware,
exclude: options.exclude
})
if (_$utils_9.isJSON(options.json)) {
initWithJSON(options.json)
} else {
initWithURL(options.json)
}
const rv = {
search: search
}
typeof options.success === 'function' && options.success.call(rv)
return rv
}
function initWithJSON (json) {
_$Repository_4.put(json)
registerInput()
}
function initWithURL (url) {
_$JSONLoader_2.load(url, function (err, json) {
if (err) {
throwError('failed to get JSON (' + url + ')')
}
initWithJSON(json)
})
}
function emptyResultsContainer () {
options.resultsContainer.innerHTML = ''
}
function appendToResultsContainer (text) {
options.resultsContainer.innerHTML += text
}
function registerInput () {
options.searchInput.addEventListener('input', function (e) {
if (isWhitelistedKey(e.which)) {
emptyResultsContainer()
debounce(function () { search(e.target.value) }, options.debounceTime)
}
})
}
function search (query) {
if (isValidQuery(query)) {
emptyResultsContainer()
render(_$Repository_4.search(query), query)
}
}
function render (results, query) {
const len = results.length
if (len === 0) {
return appendToResultsContainer(options.noResultsText)
}
for (let i = 0; i < len; i++) {
results[i].query = query
appendToResultsContainer(_$Templater_7.compile(results[i]))
}
}
function isValidQuery (query) {
return query && query.length > 0
}
function isWhitelistedKey (key) {
return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
}
function throwError (message) {
throw new Error('SimpleJekyllSearch --- ' + message)
}
})(window)
}());
</script>
<!-- Configuration -->
<script>
SimpleJekyllSearch({
searchInput: document.getElementById('search-input'),
resultsContainer: document.getElementById('results-container'),
noResultsText: '<p>Aucun résultat!</p>',
json: '/search.json',
searchResultTemplate: '<li><a href="{url}">{date}&nbsp;{title}</a>&nbsp;(Création {create})</li>'
})
</script>
</div></div>
<script>(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
function scrollToAnchor(anchor, duration, callback) {
var $root = this;
$root.animate({ scrollTop: $(anchor).position().top }, duration, function() {
window.history.replaceState(null, '', window.location.href.split('#')[0] + anchor);
callback && callback();
});
}
$.fn.scrollToAnchor = scrollToAnchor;
});
})();
(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
function affix(options) {
var $root = this, $window = $(window), $scrollTarget, $scroll,
offsetBottom = 0, scrollTarget = window, scroll = window.document, disabled = false, isOverallScroller = true,
rootTop, rootLeft, rootHeight, scrollBottom, rootBottomTop,
hasInit = false, curState;
function setOptions(options) {
var _options = options || {};
_options.offsetBottom && (offsetBottom = _options.offsetBottom);
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
_options.scroll && (scroll = _options.scroll);
_options.disabled !== undefined && (disabled = _options.disabled);
$scrollTarget = $(scrollTarget);
isOverallScroller = window.isOverallScroller($scrollTarget[0]);
$scroll = $(scroll);
}
function preCalc() {
top();
rootHeight = $root.outerHeight();
rootTop = $root.offset().top + (isOverallScroller ? 0 : $scrollTarget.scrollTop());
rootLeft = $root.offset().left;
}
function calc(needPreCalc) {
needPreCalc && preCalc();
scrollBottom = $scroll.outerHeight() - offsetBottom - rootHeight;
rootBottomTop = scrollBottom - rootTop;
}
function top() {
if (curState !== 'top') {
$root.removeClass('fixed').css({
left: 0,
top: 0
});
curState = 'top';
}
}
function fixed() {
if (curState !== 'fixed') {
$root.addClass('fixed').css({
left: rootLeft + 'px',
top: 0
});
curState = 'fixed';
}
}
function bottom() {
if (curState !== 'bottom') {
$root.removeClass('fixed').css({
left: 0,
top: rootBottomTop + 'px'
});
curState = 'bottom';
}
}
function setState() {
var scrollTop = $scrollTarget.scrollTop();
if (scrollTop >= rootTop && scrollTop <= scrollBottom) {
fixed();
} else if (scrollTop < rootTop) {
top();
} else {
bottom();
}
}
function init() {
if(!hasInit) {
var interval, timeout;
calc(true); setState();
// run calc every 100 millisecond
interval = setInterval(function() {
calc();
}, 100);
timeout = setTimeout(function() {
clearInterval(interval);
}, 45000);
window.pageLoad.then(function() {
setTimeout(function() {
clearInterval(interval);
clearTimeout(timeout);
}, 3000);
});
$scrollTarget.on('scroll', function() {
disabled || setState();
});
$window.on('resize', function() {
disabled || (calc(true), setState());
});
hasInit = true;
}
}
setOptions(options);
if (!disabled) {
init();
}
$window.on('resize', window.throttle(function() {
init();
}, 200));
return {
setOptions: setOptions,
refresh: function() {
calc(true, { animation: false }); setState();
}
};
}
$.fn.affix = affix;
});
})();
(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
function toc(options) {
var $root = this, $window = $(window), $scrollTarget, $scroller, $tocUl = $('<ul class="toc toc--ellipsis"></ul>'), $tocLi, $headings, $activeLast, $activeCur,
selectors = 'h1,h2,h3', container = 'body', scrollTarget = window, scroller = 'html, body', disabled = false,
headingsPos, scrolling = false, hasRendered = false, hasInit = false;
function setOptions(options) {
var _options = options || {};
_options.selectors && (selectors = _options.selectors);
_options.container && (container = _options.container);
_options.scrollTarget && (scrollTarget = _options.scrollTarget);
_options.scroller && (scroller = _options.scroller);
_options.disabled !== undefined && (disabled = _options.disabled);
$headings = $(container).find(selectors).filter('[id]');
$scrollTarget = $(scrollTarget);
$scroller = $(scroller);
}
function calc() {
headingsPos = [];
$headings.each(function() {
headingsPos.push(Math.floor($(this).position().top));
});
}
function setState(element, disabled) {
var scrollTop = $scrollTarget.scrollTop(), i;
if (disabled || !headingsPos || headingsPos.length < 1) { return; }
if (element) {
$activeCur = element;
} else {
for (i = 0; i < headingsPos.length; i++) {
if (scrollTop >= headingsPos[i]) {
$activeCur = $tocLi.eq(i);
} else {
$activeCur || ($activeCur = $tocLi.eq(i));
break;
}
}
}
$activeLast && $activeLast.removeClass('active');
($activeLast = $activeCur).addClass('active');
}
function render() {
if(!hasRendered) {
$root.append($tocUl);
$headings.each(function() {
var $this = $(this);
$tocUl.append($('<li></li>').addClass('toc-' + $this.prop('tagName').toLowerCase())
.append($('<a></a>').text($this.text()).attr('href', '#' + $this.prop('id'))));
});
$tocLi = $tocUl.children('li');
$tocUl.on('click', 'a', function(e) {
e.preventDefault();
var $this = $(this);
scrolling = true;
setState($this.parent());
$scroller.scrollToAnchor($this.attr('href'), 400, function() {
scrolling = false;
});
});
}
hasRendered = true;
}
function init() {
var interval, timeout;
if(!hasInit) {
render(); calc(); setState(null, scrolling);
// run calc every 100 millisecond
interval = setInterval(function() {
calc();
}, 100);
timeout = setTimeout(function() {
clearInterval(interval);
}, 45000);
window.pageLoad.then(function() {
setTimeout(function() {
clearInterval(interval);
clearTimeout(timeout);
}, 3000);
});
$scrollTarget.on('scroll', function() {
disabled || setState(null, scrolling);
});
$window.on('resize', window.throttle(function() {
if (!disabled) {
render(); calc(); setState(null, scrolling);
}
}, 100));
}
hasInit = true;
}
setOptions(options);
if (!disabled) {
init();
}
$window.on('resize', window.throttle(function() {
init();
}, 200));
return {
setOptions: setOptions
};
}
$.fn.toc = toc;
});
})();
/*(function () {
})();*/
</script><script>
/* toc must before affix, since affix need to konw toc' height. */(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
var TOC_SELECTOR = window.TEXT_VARIABLES.site.toc.selectors;
window.Lazyload.js(SOURCES.jquery, function() {
var $window = $(window);
var $articleContent = $('.js-article-content');
var $tocRoot = $('.js-toc-root'), $col2 = $('.js-col-aside');
var toc;
var tocDisabled = false;
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
var hasToc = $articleContent.find(TOC_SELECTOR).length > 0;
function disabled() {
return $col2.css('display') === 'none' || !hasToc;
}
tocDisabled = disabled();
toc = $tocRoot.toc({
selectors: TOC_SELECTOR,
container: $articleContent,
scrollTarget: hasSidebar ? '.js-page-main' : null,
scroller: hasSidebar ? '.js-page-main' : null,
disabled: tocDisabled
});
$window.on('resize', window.throttle(function() {
tocDisabled = disabled();
toc && toc.setOptions({
disabled: tocDisabled
});
}, 100));
});
})();
(function() {
var SOURCES = window.TEXT_VARIABLES.sources;
window.Lazyload.js(SOURCES.jquery, function() {
var $window = $(window), $pageFooter = $('.js-page-footer');
var $pageAside = $('.js-page-aside');
var affix;
var tocDisabled = false;
var hasSidebar = $('.js-page-root').hasClass('layout--page--sidebar');
affix = $pageAside.affix({
offsetBottom: $pageFooter.outerHeight(),
scrollTarget: hasSidebar ? '.js-page-main' : null,
scroller: hasSidebar ? '.js-page-main' : null,
scroll: hasSidebar ? $('.js-page-main').children() : null,
disabled: tocDisabled
});
$window.on('resize', window.throttle(function() {
affix && affix.setOptions({
disabled: tocDisabled
});
}, 100));
window.pageAsideAffix = affix;
});
})();
</script><!---->
</div>
<script>(function () {
var $root = document.getElementsByClassName('root')[0];
if (window.hasEvent('touchstart')) {
$root.dataset.isTouch = true;
document.addEventListener('touchstart', function(){}, false);
}
})();
</script>
</body>
</html>