— je to obrovský nástroj, který kombinuje CRM, správu dokumentů a účetnictví, a další různé funkce, které se velmi líbí manažerům, ale IT personálům už tolik ne. Portál využívá velké množství malých a středních firem, včetně malých klinik, výrobců a dokonce i kosmetických salonů. Hlavní funkcí, kterou manažeři "milují", je integrace telefonie a CRM, kdy každý telefonní hovor je okamžitě zaznamenán v CRM, vytvářejí se karty zákazníků a při příchozím hovoru se zobrazují informace o zákazníkovi, takže je jasné, kdo je, co mu lze prodat a kolik dluží. Ale telefonie od Bitrix24 a její integrace s CRM něco stojí, někdy nemalé částky. V tomto článku se podělím o zkušenosti s integrací pomocí otevřených nástrojů a populární IP PBX. , a také prozkoumám logiku fungování různých částí.
Pracuji na outsourcingu ve firmě, která se zabývá prodejem, nastavením a integrací IP telefonie. Když se mě ptali, zda můžeme té a té firmě nabídnout něco pro integraci Bitrix24 s PBX, které mají u svých klientů, stejně jako s virtuálními PBX na různých VDS firem, šel jsem na Google. A samozřejmě mi vrátil odkaz na , kde jsou popisy, a github, a vypadá to, že vše funguje. Ale když jsem se pokusil tohle řešení použít, ukázalo se, že Bitrix24 již není tím, co býval, a je třeba mnohé předělat. Kromě toho, FreePBX není holý Asterisk, je třeba přemýšlet, jak sladit pohodlí použití s hardcore dial plánem v konfiguračních souborech.
Zkoumáme logiku fungování
Takže nejprve, jak to všechno má fungovat. Při příchozím hovoru z vnějšího zdroje do PBX (událost SIP INVITE od poskytovatele) začíná zpracování dial plánu (dialplan) — pravidel, co a v jakém pořadí s hovorem udělat. Z prvního paketu lze získat spoustu informací, které lze poté použít v pravidlech. Skvělým nástrojem pro zkoumání vnitřností SIP je analyzátor sngrep () který se jednoduše instaluje v populárních distribucích přes apt install/yum install a podobně, ale lze ho také sestavit ze zdrojových kódů. Podíváme se na log hovoru v sngrep.

Zjednodušeně se dialplán zabývá pouze prvním balíčkem, někdy také během hovoru dochází k převodu hovorů, stisků tlačítek (DTMF), různým zajímavostem jako FollowMe, RingGroup, IVR a dalším.
Co je uvnitř balíčku Invite

Ve skutečnosti většina jednoduchých dialplánů pracuje s prvními dvěma poli a veškerá logika se točí kolem DID a CallerID. DID — kam voláme, CallerID — kdo volá.
Ale máme firmu, ne jen jeden telefon — a to znamená, že v PBX pravděpodobně existují skupiny hovorů (současný/sekvenční hovor několika přístrojů) na městských číslech (Ring Group), IVR (Dobrý den, voláte… Stiskněte jedničku pro…), Hlasové pošty (Phrases), Časové podmínky (Time Conditions), Přesměrování na jiné čísla nebo na mobil (FollowMe, Forward). To znamená, že jednoznačně určit, komu skutečně hovor přijde a s kým bude rozhovor, je velmi obtížné. Zde je příklad začátku průchodu typického hovoru v PBX našich klientů.

Po úspěšném přijetí hovoru do PBX probíhá jeho cesta dialplánem v různých "kontextech". Kontext z pohledu Asteriska je číselný soubor příkazů, z nichž každý obsahuje filtr podle zadaného čísla (ten se nazývá exten, pro vnější hovor na začátku exten=DID). Příkazy v řádku dialplánu mohou být cokoliv — vnitřní funkce (například zavolat vnitřnímu účastníkovi — Dial(), ukončit hovor — Hangup()), podmínkoví operátory (IF, ELSE, ExecIF a podobně), přechody na jiná pravidla tohoto kontextu (Goto, GotoIF), přechod do jiných kontextů ve formě volání funkcí (Gosub, Macro). Samostatně stojí direktiva include jméno_kontextu, , která přidává příkazy jiného kontextu na konec aktuálního kontextu. Příkazy zahrnuté přes include se vždy vykonávají po příkazech aktuálního kontextu.
Celá logika práce FreePBX je postavena na vzájemném zahrnování různých kontextů přes include a volání přes Gosub, Macro a zpracovatele Handler. Podívejme se na kontext příchozích hovorů FreePBX.

Hovor prochází všemi kontexty shora dolů postupně, v každém kontextu mohou být volání jiných kontextů jako makra (Macro), funkcí(Gosub) nebo prostě přechody (Goto), a proto skutečné stromové rozložení toho, co je voláno, lze sledovat pouze v logách.
Typická schéma nastavení typické kancelářské PBX je znázorněna níže. Při příjmu hovorů se hledá DID, na jehož základě se kontrolují časové podmínky; pokud je vše v pořádku, spustí se hlasové menu. Z něj je prostřednictvím tlačítka 1 nebo po uplynutí časového limitu přesměrování na skupinu operátorů. Po skončení hovoru se vyvolá makro hangupcall, po kterém již nebude možné v dialplánu provést žádné akce kromě speciálních zpracovatelů (hangup handler).

Kde v tomto algoritmu hovoru bychom měli posílat informace o začátku hovoru do CRM, kde začít nahrávání, kde končit nahrávání a zasílat to spolu s informacemi o hovoru do CRM?
Integrace s externími systémy
Co je integrace PBX a CRM? Jsou to nastavení a programy, které převádějí data a události mezi těmito dvěma platformami a navzájem si je přenášejí. Nejobvyklejším způsobem interakce nezávislých systémů je API a nejoblíbenějším způsobem přístupu k API je HTTP REST. Ale to neplatí pro Asterisk.
Uvnitř Asterisku jsou:
AGI — synchronní volání externích programů/komponentů, používá se hlavně v dialplánu, jsou dostupné knihovny jako ,
AMI — textový TCP socket, který funguje na principu subscribe na události a vstupu textových příkazů, připomíná SMTP zevnitř, dokáže sledovat události a řídit hovory, existuje knihovna — nejoblíbenější pro vytváření spojení s Asteriskem
Příklad výstupu AMI
Událost: Newchannel
Oprávnění: call, all
Kanál: PJSIP/VMS_pjsip-0000078b
StavKanálu: 4
PopisStavuKanálu: Ring
ČísloVolajícího: 111222
JménoVolajícího: 111222
ČísloPřipojenéLinky:
JménoPřipojenéLinky:
Jazyk: en
KódÚčtu:
Kontext: from-pstn
Exten: s
Priorita: 1
Jedinečnéid: 1599589046.5244
Propojenéid: 1599589046.5244
ARI — směs toho a onoho, vše přes REST, WebSocket, ve formátu JSON — ale s novými knihovnami a obaly to moc slavné není, našly se (, ) které stagnovaly ve svém vývoji už asi 3 roky.
Příklad výstupu ARI při iniciaci hovoru
{ "variable": "CallMeCallerIDName", "value": "111222", "type": "ChannelVarset", "timestamp": "2020-09-09T09:38:36.269+0000", "channel": { "id": "1599644315.5334", "name": "PJSIP/VMS pjsip-000007b6", "state": "Ring", "caller": { "name": "111222", "number": "111222" }, "connected": { "name": "", "number": "" }, "accountcode": "", "dialplan": { "context": "from-pstn", "exten": "s", "priority": 2, "appname": "Stasis", "appdata": "hello-world" }, "creationtime": "2020-09-09T09:38:35.926+0000", "language": "ru" }, "asteriskid": "48:5b:aa:aa:aa:aa", "application": "hello-world" }id»:»48:5b:aa:aa:aa:aa», «application»:»hello-world» }
Pohodlí nebo nepohodlí, možnost nebo nemožnost práce s konkrétním API se určuje podle úkolů, které je třeba vyřešit. Úkoly pro integraci s CRM jsou následující:
Sledovat začátek volání, kam bylo přesměrováno, vyhledat CallerID, DID, časy začátku a konce, případně data z adresáře (k nalezení vztahu mezi telefonem a uživatelem CRM)
Zahájit a ukončit nahrávání hovoru, uložit ve správném formátu, po ukončení nahrávání sdělit, kde se soubor nachází
Iniciovat hovor na základě externí události (z programu), zavolat na interní číslo, externí a spojit je
Volitelně: integrovat s CRM, skupinami volání a FollowME pro automatické přesměrování hovorů v případě nepřítomnosti (na základě informací z CRM)
Všechny tyto úkoly lze řešit prostřednictvím AMI nebo ARI, ale ARI poskytuje mnohem méně informací, chybí mnohé události, nejsou sledovány mnohé proměnné, které v AMI přece jen jsou (například volání makro příkazů, zadávání proměnných uvnitř makro příkazů, včetně nahrávání hovorů). Proto pro správné a přesné sledování zvolíme zatím AMI (ale ne definitivně). Kromě toho (kam by byl svět bez toho, jsme líní lidé) — v původní práci () používají PAMI. *Poté je třeba zkusit přepsat na ARI, ale není jisté, že to půjde.
Vymýšlíme integraci znovu
Aby náš FreePBX mohl jednoduchými způsoby hlásit do AMI začátek hovoru, dobu ukončení, telefonní čísla, jména nahraných souborů a vypočítat délku hovoru, je nejjednodušší využít stejný trik jako původní autoři — zavést si vlastní proměnné a zpracovat výstup na jejich přítomnost. PAMI to umožňuje jednoduše prostřednictvím funkce filtr.
Zde je příklad, jak zadat svou proměnnou pro čas začátku hovoru (s — to je speciální číslo v dialplánu, které se provádí PŘED začátkem vyhledávání podle DID)
[ext-did-custom]
exten => s,1,Set(CallStart=${STRFTIME(epoch,,%s)})Příklad AMI události na tomto řádku
Událost: Newchannel
Oprávnění: call, all
Kanál: PJSIP/VMS_pjsip-0000078b
StavKanálu: 4
PopisStavuKanálu: Ring
ČísloVolajícího: 111222
JménoVolajícího: 111222
ČísloPřipojenéLinky:
JménoPřipojenéLinky:
Jazyk: en
KódÚčtu:
Kontext: from-pstn
Exten: s
Priorita: 1
Jedinečnéid: 1599589046.5244
Propojenéid: 1599589046.5244
Aplikace: Set AppData:
CallStart=1599571046
Jelikož FreePBX přepisuje soubory extention.conf a extention_additional.conf, budeme používat soubor extention_custom.conf
Kompletní kód extention_custom.conf
[globals]
;; Zkontrolujte cesty a práva ke složkám - uživatel asterisk musí mít práva pro zápis
;; Zde budou zapisovány rozhovory
WAV=\/var\/www\/html\/callme\/records\/wav
MP3=\/var\/www\/html\/callme\/records\/mp3
;; Pomocí těchto cest budou přehrávány a stahovány záznamy
URLRECORDS=https:\/\/www.host.ru\/callmeplus\/records\/mp3
;; Adresa pro callback při odchozím hovoru
URLPHP=https:\/\/www.host.ru\/callmeplus
;; Ano, zapisujeme rozhovory
RECORDING=1
;; Toto je makro pro zaznamenávání hovorů do naší složky.
;; Můžete použít i systémové nahrávání, ale zatím to dejme takto -
;; funguje to
[recording]
exten => ~~s~~,1,Set(LOCAL(calling)=${ARG1})
exten => ~~s~~,2,Set(LOCAL(called)=${ARG2})
exten => ~~s~~,3,GotoIf($["${RECORDING}" = "1"]?4:14)
exten => ~~s~~,4,Set(fname=${UNIQUEID}-${STRFTIME(${EPOCH},,%Y-%m-%d-%H_%M)}-${calling}-${called})
exten => ~~s~~,5,Set(datedir=${STRFTIME(${EPOCH},,%Y\/%m\/%d)})
exten => ~~s~~,6,System(mkdir -p ${MP3}\/${datedir})
exten => ~~s~~,7,System(mkdir -p ${WAV}\/${datedir})
exten => ~~s~~,8,Set(monopt=nice -n 19 \/usr\/bin\/lame -b 32 --silent "${WAV}\/${datedir}\/${fname}.wav" "${MP3}\/${datedir}\/${fname}.mp3" && rm -f "${WAV}\/${fname}.wav" && chmod o+r "${MP3}\/${datedir}\/${fname}.mp3")
exten => ~~s~~,9,Set(FullFname=${URLRECORDS}\/${datedir}\/${fname}.mp3)
exten => ~~s~~,10,Set(CDR(filename)=${fname}.mp3)
exten => ~~s~~,11,Set(CDR(recordingfile)=${fname}.wav)
exten => ~~s~~,12,Set(CDR(realdst)=${called})
exten => ~~s~~,13,MixMonitor(${WAV}\/${datedir}\/${fname}.wav,b,${monopt})
exten => ~~s~~,14,NoOp(Finish if_recording_1)
exten => ~~s~~,15,Return()
;; Toto je hlavní kontext pro začátek hovoru
[ext-did-custom]
;; Je to šílenství, dělat to takhle, ale funguje - přidáváme k číslu '8'
exten => s,1,Set(CALLERID(num)=8${CALLERID(num)})
;; Tady jsou různé proměnné pro skript
exten => s,n,Gosub(recording,~~s~~,1(${CALLERID(number)},${EXTEN}))
exten => s,n,ExecIF(${CallMeCallerIDName}?Set(CALLERID(name)=${CallMeCallerIDName}):NoOp())
exten => s,n,Set(CallStart=${STRFTIME(epoch,,%s)})
exten => s,n,Set(CallMeDISPOSITION=${CDR(disposition)})
;; To nejdůležitější! Zpracovatel konce hovoru.
;; Běžné cesty zpracování konce přes (exten=>h,1,něco) v FreePBX nefungují - Macro(hangupcall,) to všechno kazí.
;; Proto zavěšujeme Hangup_Handler na konec hovoru
exten => s,n,Set(CHANNEL(hangup_handler_push)=sub-call-from-cid-ended,s,1(${CALLERID(num)},${EXTEN}))
;; Zpracovatel konce příchozího hovoru
[sub-call-from-cid-ended]
;; Mlčení hodnot při konci hovoru
exten => s,1,Set(CDR_PROP(disable)=true)
exten => s,n,Set(CallStop=${STRFTIME(epoch,,%s)})
exten => s,n,Set(CallMeDURATION=${MATH(${CallStop}-${CallStart},int)})
;; Stav hovoru - Odpověděl, neodpověděl...
exten => s,n,Set(CallMeDISPOSITION=${CDR(disposition)})
exten => s,n,Return
;; Zpracovatel odchozích hovorů - všechno analogické
[outbound-allroutes-custom]
;; Záznam
exten => _.,1,Gosub(recording,~~s~~,1(${CALLERID(number)},${EXTEN}))
;; Proměnné
exten => _.,n,Set(__CallIntNum=${CALLERID(num)})
exten => _.,n,Set(CallExtNum=${EXTEN})
exten => _.,n,Set(CallStart=${STRFTIME(epoch,,%s)})
exten => _.,n,Set(CallmeCALLID=${SIPCALLID})
;; Zavěšujeme Hangup_Handler na konec hovoru
exten => _.,n,Set(CHANNEL(hangup_handler_push)=sub-call-internal-ended,s,1(${CALLERID(num)},${EXTEN}))
;; Zpracovatel konce odchozího hovoru
[sub-call-internal-ended]
;; proměnné
exten => s,1,Set(CDR_PROP(disable)=true)
exten => s,n,Set(CallStop=${STRFTIME(epoch,,%s)})
exten => s,n,Set(CallMeDURATION=${MATH(${CallStop}-${CallStart},int)})
exten => s,n,Set(CallMeDISPOSITION=${CDR(disposition)})
;; Volání skriptu, který oznámí hovor v CRM - to je odchozí,
;; takže po skončení
exten => s,n,System(curl -s ${URLPHP}\/CallMeOut.php --data action=sendcall2b24 --data ExtNum=${CallExtNum} --data call_id=${SIPCALLID} --data-urlencode FullFname='${FullFname}' --data CallIntNum=${CallIntNum} --data CallDuration=${CallMeDURATION} --data-urlencode CallDisposition='${CallMeDISPOSITION}')
exten => s,n,ReturnHlavní rys a odlišení od původní dialplánu autorů původního článku —
Dialplán ve formátu .conf, jak to vyžaduje FreePBX (ano, umí .ael, ale ne všechny verze a ne vždy je to pohodlné)
Místo zpracování konce přes exten=>h byla zavedena zpracování přes hangup_handler, protože dialplán FreePBX fungoval pouze s tímto
Opravený řádek volání skriptu, přidána uvozovka a externí číslo volání ExtNum
Zpracování bylo přesunuto do _custom kontextů, což umožňuje nezásahovat a neměnit konfigurace FreePBX — příchozí přes [ext-did-custom], odchozí přes [outbound-allroutes-custom]
Žádná vazba na čísla — soubor je univerzální a vyžaduje nastavování pouze cesty a odkazu na server
Pro zahájení práce je třeba také věnovat skripty do AMI podle přihlašovacího jména a hesla — pro to má FreePBX také _custom soubor
Soubor manager_custom.conf
;; to je uživatelské jméno
[callmeplus]
;; to je heslo
secret = trampampamturlala
deny = 0.0.0.0/0.0.0.0
;; pracuji s lokálním počítačem - ale pokud je potřeba, lze uvést i jiné
permit = 127.0.0.1/255.255.255.255
read = system,call,log,verbose,agent,user,config,dtmf,reporting,cdr,dialplan
write = system,call,agent,log,verbose,user,config,command,reporting,originateTyto oba soubory je třeba umístit do /etc/asterisk, poté přečíst konfigurace (nebo restartovat asterisk)
# astrisk -rv
Connected to Asterisk 16.6.2 currently running on freepbx (pid = 31629)
#freepbx*CLI> dialplan reload
Dialplan reloaded.
#freepbx*CLI> exitTeď přejdeme k PHP
Inicializace skriptů a vytvoření služby
Jelikož schéma práce s Bitrix24, službou pro AMI, není zcela jasné a průhledné, je třeba se na něm zastavit zvlášť. Asterisk při aktivaci AMI pouze otevře port a to je vše. Při připojení klienta si vyžádá autorizaci, poté se klient přihlásí k potřebným událostem. Události přicházejí v prostém textu, který PAMI převádí na strukturované objekty a nabízí možnost nastavit funkci filtrování pouze podle zajímavých událostí, polí, čísel atd.
Jakmile hovor přijde, vznikne událost NewExten začínající s rodičovským kontextem [from-pstn], poté následují všechny události podle pořadí řádků v kontextech. Při získávání informací z definovaných v dialplánu _custom proměnných CallMeCallerIDName a CallStart je volána
Funkce dotazu UserID, odpovídající internímu číslu, na které volání dorazilo. A pokud jde o skupinové volání? To je politická otázka, je třeba vytvořit hovor všem najednou (když volají všichni najednou) nebo vytvářet podle potřeby při postupném volání? Většina klientů má strategii First Available, takže s tím nejsou problémy, volá jen jeden. Ale otázku je třeba vyřešit.
Funkce registrace hovoru v Bitrix24, která vrací CallID, nutný později pro oznámení o parametrech hovoru a odkazu na záznam. Vyžaduje buď vnitřní číslo, nebo UserID.

Po skončení hovoru je volána funkce načtení záznamu, která současně oznamuje stav ukončení hovoru (Zaneprázdněno, Není odpověď, Úspěch) a také načítá odkaz na mp3 soubor se záznamem (pokud je k dispozici).
Jelikož modul CallMeIn.php musí běžet nepřetržitě, byl pro něj vytvořen spouštěcí soubor SystemD. callme.service, který je třeba umístit do /etc/systemd/system/callme.service.
[Unit]
Description=CallMe
[Service]
WorkingDirectory=/var/www/html/callmeplus
ExecStart=/usr/bin/php /var/www/html/callmeplus/CallMeIn.php 2>&1 >>/var/log/callmeplus.log
ExecStop=/bin/kill -WINCH ${MAINPID}
KillSignal=SIGKILL
Restart=on-failure
RestartSec=10s
# zde je třeba zkontrolovat, jaká práva na složky
#User=www-data #Ubuntu - debian
#User=nginx #Centos
[Install]
WantedBy=multi-user.targetInicializace a spuštění skriptu probíhá přes systemctl nebo service.
# systemctl enable callme
# systemctl start callmeSlužba se bude sama restartovat podle potřeby (při pádech). Služba sledování příchozích nevyžaduje instalaci webového serveru, potřebný je jen PHP (který je jistě na serveru FreePBX). Ale při absenci přístupu ke záznamům hovorů přes webový server (ještě s https) nebude možné poslouchat záznamy hovorů.
Teď se podívejme na odchozí hovory. Skript CallMeOut.php má dvě funkce:
Iniciace hovoru při přijetí žádosti na php skript (včetně tlačítka „Zavolat“ v samotném Bitrixu). Bez webového serveru nefunguje, žádost přichází přes HTTP POST, v žádosti je token.
Oznámení o hovoru, jeho parametrech a záznamech v Bitrixu. Probíhá na podnět Asterisku v dialplánu [sub-call-internal-ended] po skončení hovoru.

Webový server je potřeba pouze pro dvě věci — nahrání souborů záznamů Bitrixem (přes HTTPS) a volání skriptu CallMeOut.php. Můžete použít zabudovaný server FreePBX, soubory pro který leží /var/www/html, můžete nainstalovat jiný server nebo napsat jinou cestu.
Webový server
Nastavení webového serveru ponecháme na samostatné studium (, , ). Pokud nemáte doménu, můžete vyzkoušet FreeDomain ( ), která vám zdarma poskytne název pro vaši bílou IP (nezapomeňte přesměrovat porty 80, 443 přes router, pokud je externí adresa jen na něm). Pokud jste právě vytvořili DNS doménu, musíte počkat (od 15 minut do 48 hodin), než se všechny servery načtou. Zkušenosti s domácími poskytovateli ukazují, že to může trvat od 1 hodiny do 24 hodin.
Automatizace instalace
Na GitHubu byla zahájena vývoj instalátoru, aby bylo možné instalaci provádět ještě snadněji. Ale hladké to bylo na papíře — zatím instalujeme všechno ručně, naštěstí po prozkoumání této problematiky je jasné, kdo s kým kam chodí a jak to debuggovat. Instalátor zatím není (
Docker
Pokud chcete řešení rychle vyzkoušet — existuje možnost s Dockerem — rychle vytvořit kontejner, poskytnout mu porty ven, přidat konfigurační soubory a vyzkoušet (to je varianta s kontejnerem LetsEncrypt, pokud certifikát již existuje, stačí přesměrovat reverzní proxy na webový server FreePBX (tomu jsme dali jiný port — 88), LetsEncrypt v Dockeru podle doporučení.
Soubor je potřeba spustit ve stažené složce projektu (po git clone), ale předtím se podívat do konfigurací Asterisku (složka asterisk) a zapsat tam cesty k záznamům a URL vašeho webu.
version: '3.3'
services:
nginx:
image: nginx:1.15-alpine
ports:
- "80:80"
- "443:443"
volumes:
- .\/nginx\/ssl_docker.conf:\/etc\/nginx\/conf.d\/ssl_docker.conf
certbot:
image: certbot\/certbot
freepbx:
image: flaviostutz\/freepbx
ports:
- 88:80 # pro nastavení
- 5060:5060\/udp
- 5160:5160\/udp
- 127.0.0.1:5038:5038 # pro CallMeOut.php
# - 3306:3306
- 18000-18100:18000-18100\/udp
restart: always
environment:
- ADMIN_PASSWORD=admin123
volumes:
- backup:\/backup
- recordings:\/var\/spool\/asterisk\/monitor
- .\/callme:\/var\/www\/html\/callme
- .\/systemd\/callme.service:\/etc\/systemd\/system\/callme.conf
- .\/asterisk\/manager_custom.conf:\/etc\/asterisk\/manager_custom.conf
- .\/asterisk\/extensions_custom.conf:\/etc\/asterisk\/extensions_custom.conf
# - .\/conf\/startup.sh:\/startup.sh
volumes:
backup:
recordings:
Tento soubor docker-compose.yaml se spouští přes
docker-compose up -d
Pokud nginx neběží, znamená to, že něco není v pořádku s konfigurací ve složce nginx\/ssl_docker.conf.
Další integrace
A proč by se zároveň nezapojilo několik CRM do skriptů, říkali jsme si. Prozkoumali jsme několik API jiných CRM, zejména bezplatnou zabudovanou do některých Asterisk — ShugarCRM a Vtiger, a ano! jde to, princip je stejný. Ale to už je jiný příběh, který později nahrajeme na GitHub samostatně.
Odkazy
Samotný kód na GitHubu —
Původní článek pro inspiraci
Diskleimer: jakékoli shody s realitou jsou fiktivní a nebyl to já,
Zdroj: habr.com
