From d9d399429c61e023c367ee60ebf0f847989715a4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Hendrik=20S=C3=B6bbing?= <hendrik@soebbing.de>
Date: Wed, 13 Nov 2019 16:49:02 +0100
Subject: [PATCH 01/20] Improve German translations slightly
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Hendrik Söbbing <h.soebbing@shopware.com>
---
 pwnagotchi/locale/de/LC_MESSAGES/voice.mo | Bin 4547 -> 4928 bytes
 pwnagotchi/locale/de/LC_MESSAGES/voice.po |  60 +++++++++++++++-------
 pwnagotchi/locale/voice.pot               |   2 +-
 3 files changed, 42 insertions(+), 20 deletions(-)

diff --git a/pwnagotchi/locale/de/LC_MESSAGES/voice.mo b/pwnagotchi/locale/de/LC_MESSAGES/voice.mo
index 683dca252c8c344d352b7143eb61c867eda6786e..f0885f3437aaaf463bb7426625d40f732547c565 100644
GIT binary patch
delta 1916
zcmZ9MUuYaf9LFbVk|yotZ<AQl#>|<R7<;*Dt*JI`4@$vSV=EpKv}jp!licFvF5X@Y
zCJhHdK?I?6>4Wv5FXDeh2)-1-(ttjQ6pA1SO;Hp?T17!ntJKeTZ}q|9e*2l3oB93U
zT^qbOTwU*KIBOUOknKpK-k2=>YXc9);YMRx;1SpZvv3spuos4K2)+w-|1#VRuS1>x
z9d^LBCSwNR0PKeu$oZ;yhRK6$EI|ub;THHM?1Dc)DY^+;Vdq9;Zi53c_rPw}$6!0m
zL4KyfLxxYn3-DDq3=cQg^p;>R_RUL7q~JBU6@CC^;4`=hUV~EnBb4GlVG`Dp7M&Y{
zihLim@F}<hF2TFun@|y7h6>;YRDd_(cI=yWtXP<WO6g-Ub5I7Bp)Q25555KE&?U&v
zT;XvSyb2rO2J%nVO*15ErVYyRo_O6KuZLh&DNZqIgBdtcXUtJ3#jhl4hR?z=*6%<$
z^c7Uie};;@o3OZI?t{0({ZOf&f+H{wb?-F16TS`i!OvUCzdUcox)i0MO7JMug&b7v
zPeLg^6Z3tjjC=}p?rW%w{2H(8SzKY=2UUU_5MNC%UP;ej%>5nYUk0bxkfM3m315aX
zd=643^C498T!1?F4YcqNsIKng(!FpmOvC4(GIS0Stoa10$G(6H^xOFVTU92i`4DxG
zq6sL&$Dty89?IiW@&0+J(tHHR;6<n!{{&Uz-{SS(@wx#e+D}0G)^tNmntq7yrn-{}
zC6C6om~upoBtsg5HAELR)$982kaCS=4Q4NV0GU9P2}OGcGK$2X6lU}a!dp>|yyBY^
zMXgk;jv6}=rSN`4O{5Q$x_>`{&(Tl>DTHAE8{^FO#4Ei8O20CwEUD=<R0=gBUybM)
z)sEhYYO5WUWCx<A(NGgv<RN4?au2c#QSYb>YGS<s!{PT0x3(6&Ik#f-rA6zNoqe&i
zU~?tEIGb5rZ%ou}n)7njt7v=myQbas2cK|kEEoKu3r+;36Ej|U#TH7lHrla4$+Bb9
z>9o;7)YyU-RBYgaz%LbbG6=V{tZ$BtCT(_MKC^ngB~u@!5~sSN&6HjAot<_0%1Dq2
z-%C8+l(R0dhr+tnv3@B&GM%?GYaizeBX-Vbj!mynaDFkIY<;~g@D^OzdJ8l55Pz;-
zZ5^zutDI@ywK16YmV8$Z$J<YDlbY;wzTnL`sVdv$z)c4$0sG;#_5)S@e{4QT*_>y6
z++Xwp8%*bYH-lpZl)EBFY_{T+E6x_(f-W5NgFL4Hmz$@ZUvNe1&)G?`#BCQum(3&-
zn<G^A1DoS!!Bv(joXvLCvL&T>>HM|xmGIe)<1I<^WPVPD!%H2bjYrqs$h+{Dj_!wb
zaH?bjznDW&u^8nelQdKLQfXd6+2vZ1qgdQ*+B{N<w~}^>*p%&LCLHfv?2)ec^b4*$
K?*&06Y5oDM^c)cY

delta 1578
zcmYk+O>9(E7{>8;YH4j@+Myy8k$Q$gaV#AR6`@FgvLQeS5}*y}W}LZY#!P2w??54>
znHZIoQIi`hgasjhD<Ep!@DVU635za_U65$PS3-y;f{C~wMx+1ddYgFCx4(1G-1GIG
zGxJ@?WP9~aOYP@I=_Oty9>mOc;`wLzNBOzdtO0+)=kW=quwj<jeC)xcxDD06jI;4$
z)ceOVffw;5ypDSBF0M7J+Fw*04Xf+Sp2L1@!FN#ujA0xDT!5z|FX23%f53Wth??jz
zUdDg0126NT#=DPN@E@ptv!69<Wqxy1G;l9A;x25)w^0KZF^Lt_d*i4bTttUc*onWQ
z7TQX>w2=<fd+X5Q0BQk;BR|Cj%x_;(QHN{z0#2hA@(7pVQzYxwMkC3xc4TvwLM^a6
zdR`knZ$#~Q3pU|?d?RL7K>q9;=cD<)!fF?l8&tHj-%%4b&iQZU-AI$|z=gOEm0};4
z<A=Be&*OUh9+mPrq(M9H!B%_|HGTngr0=7~`yx*MRr#6*t!N6B`n#wOzu`32vkgu7
z6EcU@@RIs9qf(keO|%KsZy(OZ_fQjlfF#jQpvF0ax&u?qI)5tnXpk|s*Mm!txvd}d
zVh**VBdFUxj+*!iI-HK4pP*9T$n09+VpPi4U>9yi9o;bM=tiqigH*$@$WM^_WaFp-
zPa$L5H^@<jlh@!XLM2UT=l?IS^3WSKXfawi-NQSiX3baCB-+_(La9v=-Gt6jXFF4L
zF_lUc?Lg%<LTS|wRn`zBGF-5p`F`7CqK!~C^^@`n@j4NeYIq(iqo<cK8P%0`o#`S%
zshcV7RCGitx-&CH7ge{tGiuO}jWVWhjxO~I!V%WD)s1+W+>x~P`F_wp>+!@pb#<|c
z!}aN!;8DYm6So@I)x=zoh<_AZh;OTNInQ^w;O}@>n@gwDwl$k~gOgYDMK@H)=KZqE
z6pLP=<hfu=)5(cHn-<1`d5KlE#mvFMU`OIJM!KD74W1@8^o8Ab4*M%yF5?P*#d(#{
zjPLwxzTgeI1D?8gCB3z~k{PLZuH+pI`|Qr<y@K!d1UKit)sVKm`5`q2oh_;DrOC5-
zFPXG~umkHFaYubG>yP<i&w;Y@3#A-;D3!v6ZnS}XxjY<xdbE@o^2WlIjCu#s!M81=
S^TS@z2Zy|o;f(KBlJ+l?FTi{N

diff --git a/pwnagotchi/locale/de/LC_MESSAGES/voice.po b/pwnagotchi/locale/de/LC_MESSAGES/voice.po
index bd7d8b3..a7ae871 100644
--- a/pwnagotchi/locale/de/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/de/LC_MESSAGES/voice.po
@@ -7,7 +7,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: 0.0.1\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-10-23 20:56+0200\n"
+"POT-Creation-Date: 2019-11-14 21:15+0100\n"
 "PO-Revision-Date: 2019-09-29 14:00+0200\n"
 "Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
 "Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
@@ -20,7 +20,7 @@ msgid "ZzzzZZzzzzZzzz"
 msgstr ""
 
 msgid "Hi, I'm Pwnagotchi! Starting ..."
-msgstr "Hi, ich bin ein Pwnagotchi! Starte ..."
+msgstr "Hi, ich bin ein Pwnagotchi! Starte..."
 
 msgid "New day, new hunt, new pwns!"
 msgstr "Neuer Tag, neue Jagd, neue Pwns!"
@@ -35,23 +35,30 @@ msgid "The neural network is ready."
 msgstr "Das neurale Netz ist bereit."
 
 msgid "Generating keys, do not turn off ..."
-msgstr "Generiere Keys, nicht ausschalten ..."
+msgstr "Generiere Keys, nicht ausschalten..."
 
 #, python-brace-format
 msgid "Hey, channel {channel} is free! Your AP will say thanks."
-msgstr "Hey, Channel {channel} ist frei! Dein AP wir des dir danken."
+msgstr "Hey, Channel {channel} ist frei! Dein AP wird es Dir danken."
+
+msgid "Reading last session logs ..."
+msgstr "Lese die Logs der letzten Session..."
+
+#, python-brace-format
+msgid "Read {lines_so_far} log lines so far ..."
+msgstr "Bisher {lines_so_far} Zeilen im Log gelesen..."
 
 msgid "I'm bored ..."
 msgstr "Mir ist langweilig..."
 
 msgid "Let's go for a walk!"
-msgstr "Lass uns laufen gehen!"
+msgstr "Lass uns spazieren gehen!"
 
 msgid "This is the best day of my life!"
-msgstr "Das ist der beste Tag meines Lebens."
+msgstr "Das ist der beste Tag meines Lebens!"
 
 msgid "Shitty day :/"
-msgstr "Scheis Tag :/"
+msgstr "Scheißtag :/"
 
 msgid "I'm extremely bored ..."
 msgstr "Mir ist sau langweilig..."
@@ -62,6 +69,13 @@ msgstr "Ich bin sehr traurig..."
 msgid "I'm sad"
 msgstr "Ich bin traurig"
 
+#, fuzzy
+msgid "Leave me alone ..."
+msgstr "Lass mich in Ruhe..."
+
+msgid "I'm mad at you!"
+msgstr "Ich bin sauer auf Dich!"
+
 msgid "I'm living the life!"
 msgstr "Ich lebe das Leben!"
 
@@ -75,27 +89,35 @@ msgid "I'm having so much fun!"
 msgstr "Ich habe sooo viel Spaß!"
 
 msgid "My crime is that of curiosity ..."
-msgstr "Mein Verbrechen ist das der Neugier ..."
+msgstr "Mein Verbrechen ist das der Neugier..."
 
 #, python-brace-format
 msgid "Hello {name}! Nice to meet you."
 msgstr "Hallo {name}, nett Dich kennenzulernen."
 
+#, python-brace-format
+msgid "Yo {name}! Sup?"
+msgstr "Jo {name}! Was geht!?"
+
+#, python-brace-format
+msgid "Hey {name} how are you doing?"
+msgstr "Hey {name}, wie geht's?"
+
 #, python-brace-format
 msgid "Unit {name} is nearby!"
-msgstr "Gerät {name} ist in der nähe!!"
+msgstr "Gerät {name} ist in der Nähe!"
 
 #, python-brace-format
 msgid "Uhm ... goodbye {name}"
-msgstr "Uhm ...tschüß {name}"
+msgstr "Uhm... tschüß {name}"
 
 #, python-brace-format
 msgid "{name} is gone ..."
-msgstr "{name} ist weg ..."
+msgstr "{name} ist weg..."
 
 #, python-brace-format
 msgid "Whoops ... {name} is gone."
-msgstr "Whoops ...{name} ist weg."
+msgstr "Whoops... {name} ist weg."
 
 #, python-brace-format
 msgid "{name} missed!"
@@ -111,17 +133,17 @@ msgid "I love my friends!"
 msgstr "Ich liebe meine Freunde!"
 
 msgid "Nobody wants to play with me ..."
-msgstr "Niemand will mit mir spielen ..."
+msgstr "Niemand will mit mir spielen..."
 
 msgid "I feel so alone ..."
-msgstr "Ich fühl michso alleine ..."
+msgstr "Ich fühl' mich so allein..."
 
 msgid "Where's everybody?!"
-msgstr "Wo sind denn alle?"
+msgstr "Wo sind denn alle?!"
 
 #, python-brace-format
 msgid "Napping for {secs}s ..."
-msgstr "Schlafe für {secs}s"
+msgstr "Schlafe für {secs}s..."
 
 msgid "Zzzzz"
 msgstr ""
@@ -138,7 +160,7 @@ msgstr ""
 
 #, python-brace-format
 msgid "Waiting for {secs}s ..."
-msgstr "Warte für {secs}s ..."
+msgstr "Warte für {secs}s..."
 
 #, python-brace-format
 msgid "Looking around ({secs}s)"
@@ -158,7 +180,7 @@ msgstr "Jo {what}!"
 
 #, python-brace-format
 msgid "Just decided that {mac} needs no WiFi!"
-msgstr "Ich denke, dass {mac} kein WiFi brauch!"
+msgstr "Ich denke, dass {mac} kein WiFi braucht!"
 
 #, python-brace-format
 msgid "Deauthenticating {mac}"
@@ -177,7 +199,7 @@ msgid "You have {count} new message{plural}!"
 msgstr "Cool, wir haben {num} neue Handshake{plural}!"
 
 msgid "Ops, something went wrong ... Rebooting ..."
-msgstr "Ops, da ist etwas schief gelaufen ...Starte neu ..."
+msgstr "Ops, da ist was schief gelaufen... Starte neu..."
 
 #, python-brace-format
 msgid "Kicked {num} stations\n"
diff --git a/pwnagotchi/locale/voice.pot b/pwnagotchi/locale/voice.pot
index c72d131..93f75b9 100644
--- a/pwnagotchi/locale/voice.pot
+++ b/pwnagotchi/locale/voice.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-11-04 12:57+0100\n"
+"POT-Creation-Date: 2019-11-18 18:29+0100\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"

From 3351c251ef84630a3781f49fb9240156ed212c3c Mon Sep 17 00:00:00 2001
From: xenDE <daniel@mameso.com>
Date: Tue, 19 Nov 2019 00:56:59 +0100
Subject: [PATCH 02/20] fix gps timestamp parsing

problem on timestamp parsing if microseconds are more then 6 numbers.

will fix bug reported in this pr: https://github.com/evilsocket/pwnagotchi/pull/619

tested with testdata from https://github.com/xenDE/pwnagotchi-plugin-webgpsmap/tree/master/handshakes.gps-map-test

before:

[2019-11-19 00:37:51,946] [INFO] webgpsmap: scanning /root/handshakes.gps-map-test
[2019-11-19 00:37:52,022] [INFO] webgpsmap: Found 4 .(geo|gps).json files from 5 handshakes. Fetching positions ...
[2019-11-19 00:37:52,144] [ERROR] Lng is 0
[2019-11-19 00:37:52,241] [ERROR] Invalid isoformat string: '2019-11-14T12:30:41.097414739+01:00'
[2019-11-19 00:37:52,280] [ERROR] Lng is 0
[2019-11-19 00:37:52,329] [INFO] webgpsmap loaded 2 positions

after:

[2019-11-19 00:48:04,652] [INFO] webgpsmap: scanning /root/handshakes.gps-map-test
[2019-11-19 00:48:04,693] [INFO] webgpsmap: Found 5 .(geo|gps).json files from 6 handshakes. Fetching positions ...
[2019-11-19 00:48:04,760] [ERROR] Lng is 0
[2019-11-19 00:48:04,822] [ERROR] Lng is 0
[2019-11-19 00:48:04,850] [INFO] webgpsmap loaded 3 positions
---
 pwnagotchi/plugins/default/webgpsmap.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pwnagotchi/plugins/default/webgpsmap.py b/pwnagotchi/plugins/default/webgpsmap.py
index fc8cedc..cb1a2ad 100644
--- a/pwnagotchi/plugins/default/webgpsmap.py
+++ b/pwnagotchi/plugins/default/webgpsmap.py
@@ -293,9 +293,9 @@ class PositionFile:
         elif 'Updated' in self._json:
             # convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
             date_iso_formated = self._json['Updated']
-            # fill milliseconds to 6 numbers
+            # fill/cut microseconds to 6 numbers
             part1, part2, part3 = re.split('\.|\+', date_iso_formated)
-            part2 = part2.ljust(6, '0')
+            part2 = part2.ljust(6, '0')[:6]
             date_iso_formated = part1 + "." + part2 + "+" + part3
             dateObj = datetime.datetime.fromisoformat(date_iso_formated)
             return_ts = int("%.0f" % dateObj.timestamp())

From cfc0ad1b486bb501ba835694f6653bb0efddec80 Mon Sep 17 00:00:00 2001
From: daniel156161 <d.dolezal97@protonmail.com>
Date: Tue, 19 Nov 2019 05:03:21 +0100
Subject: [PATCH 03/20] fix backup.sh (find with type f for no zero byte files
 into archive)

---
 scripts/backup.sh | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/backup.sh b/scripts/backup.sh
index 526d5bb..6b91917 100755
--- a/scripts/backup.sh
+++ b/scripts/backup.sh
@@ -54,4 +54,4 @@ ping -c 1 "${UNIT_HOSTNAME}" > /dev/null 2>&1 || {
 }
 
 echo "@ backing up $UNIT_HOSTNAME to $OUTPUT ..."
-ssh "${UNIT_USERNAME}@${UNIT_HOSTNAME}" "sudo find ${FILES_TO_BACKUP} -print0 | xargs -0 sudo tar cv" | gzip -9 > "$OUTPUT"
+ssh "${UNIT_USERNAME}@${UNIT_HOSTNAME}" "sudo find ${FILES_TO_BACKUP} -type f -print0 | xargs -0 sudo tar cv" | gzip -9 > "$OUTPUT"

From 0ee0aaff37b890393ae0de31554502109f005f32 Mon Sep 17 00:00:00 2001
From: Arttumiro <miro.tammelin@hotmail.fi>
Date: Tue, 19 Nov 2019 15:38:11 +0200
Subject: [PATCH 04/20] Update to using .paw-gps.json files

Remove old link due to it not being too good, update file extension to .paw-gps.json for better support on problems with webgpsmap.
---
 pwnagotchi/plugins/default/paw-gps.py | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/pwnagotchi/plugins/default/paw-gps.py b/pwnagotchi/plugins/default/paw-gps.py
index 0b477d7..e92805b 100644
--- a/pwnagotchi/plugins/default/paw-gps.py
+++ b/pwnagotchi/plugins/default/paw-gps.py
@@ -4,10 +4,7 @@ import pwnagotchi.plugins as plugins
 
 '''
 You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk
-NEW BETTER GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
-
-Old guide here, (not recommended if you plan on using it with the webgpsmap plugin)
-https://raw.githubusercontent.com/systemik/pwnagotchi-bt-tether/master/GPS-via-PAW
+GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
 '''
 
 
@@ -30,7 +27,7 @@ class PawGPS(plugins.Plugin):
             ip = self.options['ip']
 
         gps = requests.get('http://' + ip + '/gps.xhtml')
-        gps_filename = filename.replace('.pcap', '.gps.json')
+        gps_filename = filename.replace('.pcap', '.paw-gps.json')
 
         logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
         with open(gps_filename, 'w+t') as f:

From d045ed5afa91a9cdd1076619b95e4f444fdada73 Mon Sep 17 00:00:00 2001
From: root <root@localhost>
Date: Tue, 19 Nov 2019 22:58:49 +0000
Subject: [PATCH 05/20] Added romanian language

---
 pwnagotchi/locale/ro/LC_MESSAGES/voice.mo | Bin 0 -> 5184 bytes
 pwnagotchi/locale/ro/LC_MESSAGES/voice.po | 249 ++++++++++++++++++++++
 2 files changed, 249 insertions(+)
 create mode 100644 pwnagotchi/locale/ro/LC_MESSAGES/voice.mo
 create mode 100644 pwnagotchi/locale/ro/LC_MESSAGES/voice.po

diff --git a/pwnagotchi/locale/ro/LC_MESSAGES/voice.mo b/pwnagotchi/locale/ro/LC_MESSAGES/voice.mo
new file mode 100644
index 0000000000000000000000000000000000000000..09bb9c44e2023dbcdb906c6956a8ea4538dbe6d1
GIT binary patch
literal 5184
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
a)ng1B4uquF<xO>C)q6=(&ylf&rvCxOiHHIK

literal 0
HcmV?d00001

diff --git a/pwnagotchi/locale/ro/LC_MESSAGES/voice.po b/pwnagotchi/locale/ro/LC_MESSAGES/voice.po
new file mode 100644
index 0000000..8f770d4
--- /dev/null
+++ b/pwnagotchi/locale/ro/LC_MESSAGES/voice.po
@@ -0,0 +1,249 @@
+# Pwnagotchi translation.
+# Copyright (C) 2019
+# This file is distributed under the same license as the pwnagotchi package.
+# FIRST AUTHOR <radu.ungureanu@techie.com>, 2019.
+#
+#, 
+msgid ""
+msgstr ""
+"Project-Id-Version: 0.0.1\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2019-11-04 12:57+0100\n"
+"PO-Revision-Date: 2019-11-20 00:18+594\n"
+"Last-Translator: Ungureanu Radu-Andrei <radu.ungureanu@techie.com>\n"
+"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github."
+"com>\n"
+"Language: ro\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+msgid "ZzzzZZzzzzZzzz"
+msgstr ""
+
+msgid "Hi, I'm Pwnagotchi! Starting ..."
+msgstr "Buna, sunt Pwnagotchi! Pornesc..."
+
+msgid "New day, new hunt, new pwns!"
+msgstr "O noua zi, o noua vanatoare, noi pwn-uri!"
+
+msgid "Hack the Planet!"
+msgstr "Pirateaza planeta!"
+
+msgid "AI ready."
+msgstr "AI-ul e gata."
+
+msgid "The neural network is ready."
+msgstr "Rețeaua neuronală este gata."
+
+msgid "Generating keys, do not turn off ..."
+msgstr "Se generează chei, nu închide..."
+
+#, python-brace-format
+msgid "Hey, channel {channel} is free! Your AP will say thanks."
+msgstr "Hey, canalul {channel} este liber! AP-ul tău îti va mulțumi."
+
+msgid "Reading last session logs ..."
+msgstr "Se citesc log-urile din sesiunea anterioara..."
+
+#, python-brace-format
+msgid "Read {lines_so_far} log lines so far ..."
+msgstr "Am citit {lines_so_far} linii din log pana acum..."
+
+msgid "I'm bored ..."
+msgstr "Sunt plictisit..."
+
+msgid "Let's go for a walk!"
+msgstr "Hai să ne plimbăm!"
+
+msgid "This is the best day of my life!"
+msgstr "Asta este cea mai buna zi din viața mea!"
+
+msgid "Shitty day :/"
+msgstr "O zi proasta :/"
+
+msgid "I'm extremely bored ..."
+msgstr "Sunt extrem de plictisit..."
+
+msgid "I'm very sad ..."
+msgstr "Sunt foarte trist..."
+
+msgid "I'm sad"
+msgstr "Sunt trist"
+
+msgid "Leave me alone ..."
+msgstr "Lasă-mă in pace..."
+
+msgid "I'm mad at you!"
+msgstr "Sunt supărat pe tine!"
+
+msgid "I'm living the life!"
+msgstr "Trăiesc viața!"
+
+msgid "I pwn therefore I am."
+msgstr "Eu pwn-ez, deci aici sunt."
+
+msgid "So many networks!!!"
+msgstr "Atât de multe rețele!"
+
+msgid "I'm having so much fun!"
+msgstr "Mă distrez așa de mult!"
+
+msgid "My crime is that of curiosity ..."
+msgstr "Crima mea este una de curiozitate..."
+
+#, python-brace-format
+msgid "Hello {name}! Nice to meet you."
+msgstr "Bună {name}! Mă bucur să te cunosc."
+
+#, python-brace-format
+msgid "Yo {name}! Sup?"
+msgstr "Yo {name}! Cmf?"
+
+#, python-brace-format
+msgid "Hey {name} how are you doing?"
+msgstr "Hey {nume} ce mai faci?"
+
+#, python-brace-format
+msgid "Unit {name} is nearby!"
+msgstr "Unitatea {name} este aproape!"
+
+#, python-brace-format
+msgid "Uhm ... goodbye {name}"
+msgstr "Uhm... Pa {name}"
+
+#, python-brace-format
+msgid "{name} is gone ..."
+msgstr "{name} a dispărut."
+
+#, python-brace-format
+msgid "Whoops ... {name} is gone."
+msgstr "Oops... {name} a dispărut."
+
+#, python-brace-format
+msgid "{name} missed!"
+msgstr "{name} ratat!"
+
+msgid "Missed!"
+msgstr "Ratat!"
+
+msgid "Good friends are a blessing!"
+msgstr "Prietenii buni sunt o binecuvântare!"
+
+msgid "I love my friends!"
+msgstr "Îmi iubesc prietenii!"
+
+msgid "Nobody wants to play with me ..."
+msgstr "Nimeni nu vrea sa se joace cu mine..."
+
+msgid "I feel so alone ..."
+msgstr "Mă simt așa de singuratic..."
+
+msgid "Where's everybody?!"
+msgstr "Unde-i toată lumea?!"
+
+#, python-brace-format
+msgid "Napping for {secs}s ..."
+msgstr "Dorm pentru {secs}s..."
+
+msgid "Zzzzz"
+msgstr "Zzzzz"
+
+#, python-brace-format
+msgid "ZzzZzzz ({secs}s)"
+msgstr "ZzzZzzz ({secs}s)"
+
+msgid "Good night."
+msgstr "Noapte bună."
+
+msgid "Zzz"
+msgstr "Zzz"
+
+#, python-brace-format
+msgid "Waiting for {secs}s ..."
+msgstr "Aștept pentru {secs}s..."
+
+#, python-brace-format
+msgid "Looking around ({secs}s)"
+msgstr "Mă uit împrejur ({secs}s)"
+
+#, python-brace-format
+msgid "Hey {what} let's be friends!"
+msgstr "Hey {what} hai să fim prieteni!"
+
+#, python-brace-format
+msgid "Associating to {what}"
+msgstr "Mă asociez cu {what}"
+
+#, python-brace-format
+msgid "Yo {what}!"
+msgstr "Yo {what}"
+
+#, python-brace-format
+msgid "Just decided that {mac} needs no WiFi!"
+msgstr "Am decis că lui {mac} nu-i trebuie WiFi!"
+
+#, python-brace-format
+msgid "Deauthenticating {mac}"
+msgstr "Îl deautentific pe {mac}"
+
+#, python-brace-format
+msgid "Kickbanning {mac}!"
+msgstr "Îi dau kickban lui {mac}"
+
+#, python-brace-format
+msgid "Cool, we got {num} new handshake{plural}!"
+msgstr "Șmecher, avem {num} de handshake-uri noi!"
+
+#, python-brace-format
+msgid "You have {count} new message{plural}!"
+msgstr "Ai {count} mesaj(e) nou/noi!"
+
+msgid "Ops, something went wrong ... Rebooting ..."
+msgstr "OOps, ceva s-a întamplat... Îmi dau reboot...+"
+
+#, python-brace-format
+msgid "Kicked {num} stations\n"
+msgstr "Am dat afară {num} de stații\n"
+
+#, python-brace-format
+msgid "Made {num} new friends\n"
+msgstr "Am făcut {num} prieteni noi \n"
+
+#, python-brace-format
+msgid "Got {num} handshakes\n"
+msgstr "Am primit {num} de handshake-uri\n"
+
+msgid "Met 1 peer"
+msgstr "Am întalnit un peer"
+
+#, python-brace-format
+msgid "Met {num} peers"
+msgstr "Am întalnit {num} de peer-uri"
+
+#, python-brace-format
+msgid ""
+"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
+"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
+"#pwnlog #pwnlife #hacktheplanet #skynet"
+msgstr "Eu am făcut pwning pentru {duration} și am dat afara {deauthed} clienți! "
+"De asemenea, am întalnit {associated} prieteni noi și am mancat {handshakes} de "
+"handshake-uri! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
+
+msgid "hours"
+msgstr "ore"
+
+msgid "minutes"
+msgstr "minute"
+
+msgid "seconds"
+msgstr "secunde"
+
+msgid "hour"
+msgstr "oră"
+
+msgid "minute"
+msgstr "minut"
+
+msgid "second"
+msgstr "secundă"

From c4a007e72aa2760a618d1bfb8784eb59ccae8ba1 Mon Sep 17 00:00:00 2001
From: xenDE <daniel@mameso.com>
Date: Wed, 20 Nov 2019 15:45:16 +0100
Subject: [PATCH 06/20] cleanup, fixes and add handling of .paw-gps.json

cleanup logging, increase cache item count from 1024 to 2048, add handling of corrupt long/lat position null, added handling of .paw-gps.json files - tested with master
---
 pwnagotchi/plugins/default/webgpsmap.py | 162 +++++++++++++++---------
 1 file changed, 99 insertions(+), 63 deletions(-)

diff --git a/pwnagotchi/plugins/default/webgpsmap.py b/pwnagotchi/plugins/default/webgpsmap.py
index cb1a2ad..85479f9 100644
--- a/pwnagotchi/plugins/default/webgpsmap.py
+++ b/pwnagotchi/plugins/default/webgpsmap.py
@@ -9,7 +9,7 @@ from functools import lru_cache
 
 '''
 2do:
-    - make the cache handling multiple clients
+    - make+test the cache handling multiple clients
     - cleanup the javascript in a class and handle "/newest" additions
     - create map filters (only cracked APs, only last xx days, between 2 days with slider)
         http://www.gistechsolutions.com/leaflet/DEMO/filter/filter.html
@@ -22,16 +22,10 @@ from functools import lru_cache
 
 class Webgpsmap(plugins.Plugin):
     __author__ = 'https://github.com/xenDE and https://github.com/dadav'
-    __version__ = '1.2.2'
+    __version__ = '1.3.0'
     __name__ = 'webgpsmap'
     __license__ = 'GPL3'
     __description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
-    __help__ = """
-- install: copy "webgpsmap.py" and "webgpsmap.html" to your configured "custom_plugins" directory
-- add webgpsmap.yml to your config
-- connect your PC/Smartphone/* with USB, BT or other to your pwnagotchi and browse to http://pwnagotchi.local:8080/plugins/webgpsmap/
-  (change pwnagotchi.local to your pwnagotchis IP, if needed)
-"""
 
     ALREADY_SENT = list()
     SKIP = list()
@@ -47,7 +41,7 @@ class Webgpsmap(plugins.Plugin):
         """
         Plugin got loaded
         """
-        logging.info("webgpsmap plugin loaded")
+        logging.info("[webgpsmap]: plugin loaded")
 
     def on_webhook(self, path, request):
         """
@@ -68,8 +62,8 @@ class Webgpsmap(plugins.Plugin):
                 response_status = 500
                 response_mimetype = "application/xhtml+xml"
                 response_header_contenttype = 'text/html'
-            except Exception as ex:
-                logging.error(ex)
+            except Exception as error:
+                logging.error(f"[webgpsmap] error: {error}")
                 return
         else:
             if request.method == "GET":
@@ -78,8 +72,8 @@ class Webgpsmap(plugins.Plugin):
                     self.ALREADY_SENT = list()
                     try:
                         response_data = bytes(self.get_html(), "utf-8")
-                    except Exception as ex:
-                        logging.error(ex)
+                    except Exception as error:
+                        logging.error(f"[webgpsmap] error: {error}")
                         return
                     response_status = 200
                     response_mimetype = "application/xhtml+xml"
@@ -92,8 +86,8 @@ class Webgpsmap(plugins.Plugin):
                         response_status = 200
                         response_mimetype = "application/json"
                         response_header_contenttype = 'application/json'
-                    except Exception as ex:
-                        logging.error(ex)
+                    except Exception as error:
+                        logging.error(f"[webgpsmap] error: {error}")
                         return
                 # elif path.startswith('/newest'):
                 #     # returns all positions newer then timestamp
@@ -118,7 +112,7 @@ class Webgpsmap(plugins.Plugin):
                     <meta charset="utf-8"/>
                     <style>body{font-size:1000%;}</style>
                     </head>
-                    <body>4😋4</body>
+                    <body>4😋4 for bad boys</body>
                     </html>''', "utf-8")
                 response_status = 404
         try:
@@ -126,12 +120,12 @@ class Webgpsmap(plugins.Plugin):
             if response_header_contenttype is not None:
                 r.headers["Content-Type"] = response_header_contenttype
             return r
-        except Exception as ex:
-            logging.error(ex)
+        except Exception as error:
+            logging.error(f"[webgpsmap] error: {error}")
             return
 
-    # cache 1024 items
-    @lru_cache(maxsize=1024, typed=False)
+    # cache 2048 items
+    @lru_cache(maxsize=2048, typed=False)
     def _get_pos_from_file(self, path):
         return PositionFile(path)
 
@@ -144,7 +138,7 @@ class Webgpsmap(plugins.Plugin):
         handshake_dir = gpsdir
         gps_data = dict()
 
-        logging.info("webgpsmap: scanning %s", handshake_dir)
+        logging.info(f"[webgpsmap] scanning {handshake_dir}")
 
 
         all_files = os.listdir(handshake_dir)
@@ -156,33 +150,40 @@ class Webgpsmap(plugins.Plugin):
         all_geo_or_gps_files = []
         for filename_pcap in all_pcap_files:
             filename_base = filename_pcap[:-5]  # remove ".pcap"
-            logging.debug("webgpsmap: found: " + filename_base)
+            logging.debug(f"[webgpsmap] found: {filename_base}")
             filename_position = None
 
+            logging.debug("[webgpsmap] search for .gps.json")
             check_for = os.path.basename(filename_base) + ".gps.json"
             if check_for in all_files:
                 filename_position = str(os.path.join(handshake_dir, check_for))
 
+            logging.debug("[webgpsmap] search for .geo.json")
             check_for = os.path.basename(filename_base) + ".geo.json"
             if check_for in all_files:
                 filename_position = str(os.path.join(handshake_dir, check_for))
 
+            logging.debug("[webgpsmap] search for .paw-gps.json")
+            check_for = os.path.basename(filename_base) + ".paw-gps.json"
+            if check_for in all_files:
+                filename_position = str(os.path.join(handshake_dir, check_for))
+
+            logging.debug(f"[webgpsmap] end search for position data files and use {filename_position}")
+
             if filename_position is not None:
-    #            logging.debug("webgpsmap: -- found: %s %d" % (check_for, len(all_geo_or_gps_files)) )
                 all_geo_or_gps_files.append(filename_position)
 
-    #    all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP)   # remove skiped networks? No!
+    #    all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP)   # remove skipped networks? No!
 
         if newest_only:
             all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT)
 
-        logging.info("webgpsmap: Found %d .(geo|gps).json files from %d handshakes. Fetching positions ...",
-                     len(all_geo_or_gps_files), len(all_pcap_files))
+        logging.info(f"[webgpsmap] Found {len(all_geo_or_gps_files)} position-data files from {len(all_pcap_files)} handshakes. Fetching positions ...")
 
         for pos_file in all_geo_or_gps_files:
             try:
                 pos = self._get_pos_from_file(pos_file)
-                if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO:
+                if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO and not pos.type() == PositionFile.PAWGPS:
                     continue
 
                 ssid, mac = pos.ssid(), pos.mac()
@@ -190,10 +191,17 @@ class Webgpsmap(plugins.Plugin):
                 # invalid mac is strange and should abort; ssid is ok
                 if not mac:
                     raise ValueError("Mac can't be parsed from filename")
+                pos_type = 'unknown'
+                if pos.type() == PositionFile.GPS:
+                    pos_type = 'gps'
+                elif pos.type() == PositionFile.GEO:
+                    pos_type = 'geo'
+                elif pos.type() == PositionFile.PAWGPS:
+                    pos_type = 'paw'
                 gps_data[ssid+"_"+mac] = {
                     'ssid': ssid,
                     'mac': mac,
-                    'type': 'gps' if pos.type() == PositionFile.GPS else 'geo',
+                    'type': pos_type,
                     'lng': pos.lng(),
                     'lat': pos.lat(),
                     'acc': pos.accuracy(),
@@ -201,24 +209,25 @@ class Webgpsmap(plugins.Plugin):
                     'ts_last': pos.timestamp_last(),
                     }
 
+                # get ap password if exist
                 check_for = os.path.basename(pos_file[:-9]) + ".pcap.cracked"
                 if check_for in all_files:
                     gps_data[ssid + "_" + mac]["pass"] = pos.password()
 
                 self.ALREADY_SENT += pos_file
-            except json.JSONDecodeError as js_e:
+            except json.JSONDecodeError as error:
                 self.SKIP += pos_file
-                logging.error(js_e)
+                logging.error(f"[webgpsmap] JSONDecodeError in: {error}")
                 continue
-            except ValueError as v_e:
+            except ValueError as error:
                 self.SKIP += pos_file
-                logging.error(v_e)
+                logging.error(f"[webgpsmap] ValueError: {error}")
                 continue
-            except OSError as os_e:
+            except OSError as error:
                 self.SKIP += pos_file
-                logging.error(os_e)
+                logging.error(f"[webgpsmap] OSError: {error}")
                 continue
-        logging.info("webgpsmap loaded %d positions", len(gps_data))
+        logging.info(f"[webgpsmap] loaded {len(gps_data)} positions")
         return gps_data
 
     def get_html(self):
@@ -226,11 +235,10 @@ class Webgpsmap(plugins.Plugin):
         Returns the html page
         """
         try:
-            template_file = os.path.dirname(os.path.realpath(__file__))+"/"+"webgpsmap.html"
+            template_file = os.path.dirname(os.path.realpath(__file__)) + "/" + "webgpsmap.html"
             html_data = open(template_file, "r").read()
-        except Exception as ex:
-            logging.error("error loading template file: %s", template_file)
-            logging.error(ex)
+        except Exception as error:
+            logging.error(f"[webgpsmap] error loading template file {template_file} - error: {error}")
         return html_data
 
 
@@ -238,15 +246,18 @@ class PositionFile:
     """
     Wraps gps / net-pos files
     """
-    GPS = 0
-    GEO = 1
+    GPS = 1
+    GEO = 2
+    PAWGPS = 3
 
     def __init__(self, path):
         self._file = path
         self._filename = os.path.basename(path)
         try:
+            logging.debug(f"[webgpsmap] loading {path}")
             with open(path, 'r') as json_file:
                 self._json = json.load(json_file)
+            logging.debug(f"[webgpsmap] loaded {path}")
         except json.JSONDecodeError as js_e:
             raise js_e
 
@@ -254,7 +265,7 @@ class PositionFile:
         """
         Returns the mac from filename
         """
-        parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo)\.json', self._filename)
+        parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo|paw-gps)\.json', self._filename)
         if parsed_mac:
             mac = parsed_mac.groups()[0]
             return mac
@@ -264,7 +275,7 @@ class PositionFile:
         """
         Returns the ssid from filename
         """
-        parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo)\.json', self._filename)
+        parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo|paw-gps)\.json', self._filename)
         if parsed_ssid:
             return parsed_ssid.groups()[0]
         return None
@@ -293,32 +304,37 @@ class PositionFile:
         elif 'Updated' in self._json:
             # convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
             date_iso_formated = self._json['Updated']
-            # fill/cut microseconds to 6 numbers
+            # bad microseconds fix: fill/cut microseconds to 6 numbers
             part1, part2, part3 = re.split('\.|\+', date_iso_formated)
             part2 = part2.ljust(6, '0')[:6]
+            # timezone fix: 0200 >>> 02:00
+            if len(part3) == 4:
+                part3 = part3[1:2].rjust(2, '0') + ':' + part3[3:4].rjust(2, '0')
             date_iso_formated = part1 + "." + part2 + "+" + part3
             dateObj = datetime.datetime.fromisoformat(date_iso_formated)
             return_ts = int("%.0f" % dateObj.timestamp())
         else:
-            # use file timestamp last modification of the pcap file
+            # use file timestamp last modification of the json file
             return_ts = int("%.0f" % os.path.getmtime(self._file))
         return return_ts
 
     def password(self):
         """
-        returns the password from file.pcap.cracked od None
+        returns the password from file.pcap.cracked or None
         """
         return_pass = None
-        password_file_path = self._file[:-9] + ".pcap.cracked"
+        # 2do: make better filename split/remove extension because this one has problems with "." in path
+        base_filename, ext1, ext2 = re.split('\.', self._file)
+        password_file_path = base_filename + ".pcap.cracked"
         if os.path.isfile(password_file_path):
             try:
                 password_file = open(password_file_path, 'r')
                 return_pass = password_file.read()
                 password_file.close()
-            except OSError as err:
-                print("OS error: {0}".format(err))
+            except OSError as error:
+                logging.error(f"[webgpsmap] OS error: {format(error)}")
             except:
-                print("Unexpected error:", sys.exc_info()[0])
+                logging.error(f"[webgpsmap] Unexpected error: {sys.exc_info()[0]}")
                 raise
         return return_pass
 
@@ -330,37 +346,57 @@ class PositionFile:
             return PositionFile.GPS
         if self._file.endswith('.geo.json'):
             return PositionFile.GEO
+        if self._file.endswith('.paw-gps.json'):
+            return PositionFile.PAWGPS
         return None
 
     def lat(self):
         try:
-            if self.type() == PositionFile.GPS:
+            lat = None
+            # try to get value from known formats
+            if 'Latitude' in self._json:
                 lat = self._json['Latitude']
-            if self.type() == PositionFile.GEO:
-                lat = self._json['location']['lat']
-            if lat != 0:
-                return lat
-            raise ValueError("Lat is 0")
+            if 'lat' in self._json:
+                lat = self._json['lat']  # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
+            if 'location' in self._json:
+                if 'lat' in self._json['location']:
+                    lat = self._json['location']['lat']
+            # check value
+            if lat is None:
+                raise ValueError(f"Lat is None in {self._filename}")
+            if lat == 0:
+                raise ValueError(f"Lat is 0 in {self._filename}")
+            return lat
         except KeyError:
             pass
         return None
 
     def lng(self):
         try:
-            if self.type() == PositionFile.GPS:
+            lng = None
+            # try to get value from known formats
+            if 'Longitude' in self._json:
                 lng = self._json['Longitude']
-            if self.type() == PositionFile.GEO:
-                lng = self._json['location']['lng']
-            if lng != 0:
-                return lng
-            raise ValueError("Lng is 0")
+            if 'long' in self._json:
+                lng = self._json['long']  # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
+            if 'location' in self._json:
+                if 'lng' in self._json['location']:
+                    lng = self._json['location']['lng']
+            # check value
+            if lng is None:
+                raise ValueError(f"Lng is None in {self._filename}")
+            if lng == 0:
+                raise ValueError(f"Lng is 0 in {self._filename}")
+            return lng
         except KeyError:
             pass
         return None
 
     def accuracy(self):
         if self.type() == PositionFile.GPS:
-            return 50.0
+            return 50.0 # a default
+        if self.type() == PositionFile.PAWGPS:
+            return 50.0 # a default
         if self.type() == PositionFile.GEO:
             try:
                 return self._json['accuracy']

From 83f741bbb0713bbb541dc4390bd08c9b5069d772 Mon Sep 17 00:00:00 2001
From: xenDE <daniel@mameso.com>
Date: Sat, 23 Nov 2019 02:05:01 +0100
Subject: [PATCH 07/20] fix: gpio needs to be a number

fixes gpio id as string in config
https://github.com/evilsocket/pwnagotchi/issues/643
---
 pwnagotchi/plugins/default/gpio_buttons.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/pwnagotchi/plugins/default/gpio_buttons.py b/pwnagotchi/plugins/default/gpio_buttons.py
index 10075ef..cec7cf7 100644
--- a/pwnagotchi/plugins/default/gpio_buttons.py
+++ b/pwnagotchi/plugins/default/gpio_buttons.py
@@ -32,6 +32,7 @@ class GPIOButtons(plugins.Plugin):
         GPIO.setmode(GPIO.BCM)
 
         for gpio, command in gpios.items():
+            gpio = int(gpio)
             self.ports[gpio] = command
             GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP)
             GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=600)

From 93e06d7f59285f60b9fa2d4e63237efc83ea40c0 Mon Sep 17 00:00:00 2001
From: xenDE <daniel@mameso.com>
Date: Sun, 24 Nov 2019 18:49:32 +0100
Subject: [PATCH 08/20] add filter for: SSID, MAC, isCracked, Password

---
 pwnagotchi/plugins/default/webgpsmap.html | 134 +++++++++++++++-------
 1 file changed, 95 insertions(+), 39 deletions(-)

diff --git a/pwnagotchi/plugins/default/webgpsmap.html b/pwnagotchi/plugins/default/webgpsmap.html
index 13bfacb..6c401ec 100644
--- a/pwnagotchi/plugins/default/webgpsmap.html
+++ b/pwnagotchi/plugins/default/webgpsmap.html
@@ -9,7 +9,7 @@
   <script type='text/javascript' src='http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js'></script>
   <style type="text/css">
     /* for map */
-    html, body, #mapdiv {   height: 100%; width: 100%; margin:0; background-color:#000;}
+    html, body {   height: 100%; width: 100%; margin:0; background-color:#000;}
     .pwnAPPin path {
       fill: #ce7575;
     }
@@ -85,11 +85,19 @@
       display: none;
     }
     #loading .face { font-size:8vw; }
-    #loading .text {position:absolute;bottom:0;text-align:center; font-size: 1vw;color:#a0a0a0;}
+    #loading .text { position:absolute;bottom:0;text-align:center; font-size: 2vw;color:#a0a0a0;}
+    #filterbox { position: fixed;top:0px;left:0px;z-index:999999;margin-left:55px;width:100%;height:20px;border-bottom:2px solid #303030;display: grid;grid-template-columns: 1fr 0.1fr;grid-template-rows: 1fr;grid-template-areas: ". .";}
+      #search { grid-area: 1 / 1 / 2 / 2;height:30px;padding:3px;background-color:#000;color:#e0e0e0;border:none;}
+      #matchcount { grid-area: 1 / 2 / 2 / 3;height:30px;margin-right:55px;padding-right:5px;background-color:#000;color:#a0a0a0;font-weight:bold;}
+      #mapdiv { width:100%; height: 100%; }
   </style>
 </head>
 <body>
   <div id="mapdiv"></div>
+  <div id="filterbox">
+    <input type="text" id="search" placeholder="filter: #cracked #notcracked AA:BB:CC aabbcc AndroidAP ..."/>
+    <div id="matchcount">0&nbsp;APs</div>
+  </div>
   <div id="loading"><div class="face"><nobr>(⌐■&nbsp;<span id="loading_ap_img"></span>&nbsp;■)</nobr></div><div class="text" id="loading_infotext">loading positions...</div></div>
   <script type="text/javascript">
     function loadJSON(url, callback) {
@@ -133,11 +141,9 @@
       attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
       subdomains: 'abcd',
       opacity:0.8,
-      maxZoom: 19
+//      maxZoom: 19
     });
     var mymap = L.map('mapdiv');
-    Esri_WorldImagery.addTo(mymap);
-    CartoDB_DarkMatter.addTo(mymap);
 
     var svg     = '<svg class="pwnAPPin" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#931F1F" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#931F1F" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#931F1F" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#931F1F" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
     var svgOpen = '<svg class="pwnAPPinOpen" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#1f9321" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#1f9321" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#1f9321" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#1f9321" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
@@ -157,54 +163,88 @@
       popupAnchor : [0, -30],
     });
 
+    var positionsLoaded = false;
+    var positions = [];
     var accuracys = [];
     var markers = [];
     var marker_pos = [];
     var markerClusters = L.markerClusterGroup();
 
-    loadJSON("/plugins/webgpsmap/all", function(response) {
-      var positions = JSON.parse(response);
+    function drawPositions() {
       count = 0;
+      //mymap.removeLayer(markerClusters);
+      mymap.eachLayer(function (layer) {
+        mymap.removeLayer(layer);
+      });
+      Esri_WorldImagery.addTo(mymap);
+      CartoDB_DarkMatter.addTo(mymap);
+      markerClusters = L.markerClusterGroup();
+      accuracys = [];
+      markers = [];
+      marker_pos = [];
+      filterText = document.getElementById("search").value;
+      //console.log(filterText);
       Object.keys(positions).forEach(function(key) {
-        count++;
         if(positions[key].lng){
-          new_marker_pos = [positions[key].lat, positions[key].lng];
-          if (positions[key].acc) {
-            radius = Math.round(Math.min(positions[key].acc, 500));
-            markerColor = 'red';
-            markerColorCode = '#f03';
-            fillOpacity = 0.002;
-            if (positions[key].pass) {
-              markerColor = 'green';
-              markerColorCode = '#1aff00';
-              fillOpacity = 0.1;
-            }
-            accuracys.push(
-              L.circle(new_marker_pos, {
-                color: markerColor,
-                fillColor: markerColorCode,
-                fillOpacity: fillOpacity,
-                weight: 1,
-                opacity: 0.1,
-                radius: Math.min(positions[key].acc, 500),
-              }).setStyle({'className': 'radar'}).addTo(mymap)
-            );
-          }
+          filterPattern =
+            positions[key].ssid + ' ' +
+            formatMacAddress(positions[key].mac) + ' ' +
+            positions[key].mac
+          ;
           if (positions[key].pass) {
-            newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
+            filterPattern += positions[key].pass + ' #cracked';
           } else {
-            newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
+            filterPattern += ' #notcracked';
           }
-          passInfo = '';
-          if (positions[key].pass) {
+          filterPattern = filterPattern.toLowerCase();
+          //console.log(filterPattern);
+          var matched = true;
+          if (filterText) {
+              filterText.split(" ").forEach(function (item) {
+                if (!filterPattern.includes(item.toLowerCase())) {
+                  matched = false;
+                }
+              });
+          }
+          if (matched) {
+            count++;
+            new_marker_pos = [positions[key].lat, positions[key].lng];
+            if (positions[key].acc) {
+              radius = Math.round(Math.min(positions[key].acc, 500));
+              markerColor = 'red';
+              markerColorCode = '#f03';
+              fillOpacity = 0.002;
+              if (positions[key].pass) {
+                markerColor = 'green';
+                markerColorCode = '#1aff00';
+                fillOpacity = 0.1;
+              }
+              accuracys.push(
+                L.circle(new_marker_pos, {
+                  color: markerColor,
+                  fillColor: markerColorCode,
+                  fillOpacity: fillOpacity,
+                  weight: 1,
+                  opacity: 0.1,
+                  radius: Math.min(positions[key].acc, 500),
+                }).setStyle({'className': 'radar'}).addTo(mymap)
+              );
+            }
+            passInfo = '';
+            if (positions[key].pass) {
               passInfo = '<br/><b>Pass:</b> '+escapeHtml(positions[key].pass);
+              newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
+            } else {
+              newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
+            }
+            newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
+            markers.push(newMarker);
+            marker_pos.push(new_marker_pos);
+            markerClusters.addLayer( newMarker );
           }
-          newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
-          markers.push(newMarker);
-          marker_pos.push(new_marker_pos);
-          markerClusters.addLayer( newMarker );
         }
       });
+      document.getElementById("matchcount").innerHTML = count + "&nbsp;APs";
       if (count > 0) {
         mymap.addLayer( markerClusters );
         var bounds = new L.LatLngBounds(marker_pos);
@@ -213,6 +253,22 @@
       } else {
         document.getElementById("loading_infotext").innerHTML = "NO POSITION DATA FOUND :(";
       }
+    }
+
+    // draw map on Enter in FilterInputField
+    const node = document.getElementById("search").addEventListener("keyup", function(event) {
+      if (event.key === "Enter") {
+        if (positionsLoaded) {
+          drawPositions();
+        }
+      }
+    });
+
+    // load positions
+    loadJSON("/plugins/webgpsmap/all", function(response) {
+      positions = JSON.parse(response);
+      positionsLoaded = true;
+      drawPositions();
     });
   </script>
-</body></html>
\ No newline at end of file
+</body></html>

From a7ea499fac82731ef3f28556f689a6799247160a Mon Sep 17 00:00:00 2001
From: dadav <33197631+dadav@users.noreply.github.com>
Date: Mon, 25 Nov 2019 19:47:23 +0100
Subject: [PATCH 09/20] Should fail before write

---
 pwnagotchi/plugins/default/webcfg.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/pwnagotchi/plugins/default/webcfg.py b/pwnagotchi/plugins/default/webcfg.py
index 52532f2..0570833 100644
--- a/pwnagotchi/plugins/default/webcfg.py
+++ b/pwnagotchi/plugins/default/webcfg.py
@@ -500,8 +500,9 @@ class WebConfig(plugins.Plugin):
         elif request.method == "POST":
             if path == "save-config":
                 try:
+                    parsed_yaml = yaml.safe_load(request.get_json())
                     with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
-                        yaml.safe_dump(request.get_json(), config_file, encoding='utf-8',
+                        yaml.safe_dump(parsed_yaml, config_file, encoding='utf-8',
                                 allow_unicode=True, default_flow_style=False)
 
                     _thread.start_new_thread(restart, (self.mode,))

From a03443986bc3047681bd0955560ba9bcf8cfec26 Mon Sep 17 00:00:00 2001
From: dadav <33197631+dadav@users.noreply.github.com>
Date: Mon, 25 Nov 2019 20:08:20 +0100
Subject: [PATCH 10/20] Parse to str

---
 pwnagotchi/plugins/default/webcfg.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pwnagotchi/plugins/default/webcfg.py b/pwnagotchi/plugins/default/webcfg.py
index 0570833..40f7477 100644
--- a/pwnagotchi/plugins/default/webcfg.py
+++ b/pwnagotchi/plugins/default/webcfg.py
@@ -500,7 +500,7 @@ class WebConfig(plugins.Plugin):
         elif request.method == "POST":
             if path == "save-config":
                 try:
-                    parsed_yaml = yaml.safe_load(request.get_json())
+                    parsed_yaml = yaml.safe_load(str(request.get_json()))
                     with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
                         yaml.safe_dump(parsed_yaml, config_file, encoding='utf-8',
                                 allow_unicode=True, default_flow_style=False)

From d2c44797e5f4f1a938790f4bcdd746708714967e Mon Sep 17 00:00:00 2001
From: Sayak Brahmachari <sayak-brm@users.noreply.github.com>
Date: Tue, 26 Nov 2019 14:05:10 +0530
Subject: [PATCH 11/20] Prevent duplicate entries for reported networks

Due to duplicate entries in `/root/.api-report.json`, [this code](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/plugins/default/grid.py#L90) incorrectly reports the number of pwned networks, resulting in incorrect stats on the [pwnagotchi.ai website](https://pwnagotchi.ai/).
---
 pwnagotchi/plugins/default/grid.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/pwnagotchi/plugins/default/grid.py b/pwnagotchi/plugins/default/grid.py
index a727e24..23181ae 100644
--- a/pwnagotchi/plugins/default/grid.py
+++ b/pwnagotchi/plugins/default/grid.py
@@ -67,7 +67,8 @@ class Grid(plugins.Plugin):
         logging.info("grid plugin loaded.")
 
     def set_reported(self, reported, net_id):
-        reported.append(net_id)
+        if net_id not in reported:
+            reported.append(net_id)
         self.report.update(data={'reported': reported})
 
     def check_inbox(self, agent):

From 167f559d73f4eb0d529fc66083e6c2adcd416bee Mon Sep 17 00:00:00 2001
From: Evgeny Zelenin <m-b-g@yandex.ru>
Date: Wed, 27 Nov 2019 02:54:48 +0500
Subject: [PATCH 12/20] Update voice.po


From 3c154ffe0cb5ef113727269676a5b1ee8f00deb8 Mon Sep 17 00:00:00 2001
From: Evgeny Zelenin <m-b-g@yandex.ru>
Date: Wed, 27 Nov 2019 02:58:16 +0500
Subject: [PATCH 13/20] Update voice.po

---
 pwnagotchi/locale/ru/LC_MESSAGES/voice.po | 84 +++++++++++++++--------
 1 file changed, 56 insertions(+), 28 deletions(-)

diff --git a/pwnagotchi/locale/ru/LC_MESSAGES/voice.po b/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
index 146527f..ce3ca75 100644
--- a/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
@@ -1,45 +1,51 @@
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
+# Pwnagotchi Russian translation.
+# Copyright (C) 2019
+# This file is distributed under the same license as the pwnagotchi package.
 # FIRST AUTHOR <25989971+adolfaka@users.noreply.github.com>, 2019.
-#
+# Second author <https://github.com/mbgroot>, 2019
 msgid ""
 msgstr ""
-"Project-Id-Version: \n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2019-10-03 16:47+0200\n"
-"PO-Revision-Date: 2019-10-05 18:50+0300\n"
-"Language-Team: \n"
+"Project-Id-Version: 0.0.2"
+"Report-Msgid-Bugs-To: m-b-g@yandex.ru"
+"POT-Creation-Date: 2019-11-27 16:47+0200"
+"PO-Revision-Date: 2019-11-27 18:50+0300"
+"Last-Translator: Evgeny Zelenin <m-b-g@yandex.ru>"
+"Language-Team: ru"
+"Language: ru"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 2.2.4\n"
-"Last-Translator: Elliot Manson\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
-"Language: ru_RU\n"
+
 
 msgid "ZzzzZZzzzzZzzz"
-msgstr "ZzzzZZzzzzZzzz"
+msgstr "Хрррр..."
 
 msgid "Hi, I'm Pwnagotchi! Starting ..."
-msgstr "Привет, я Pwnagotchi! Поехали …"
+msgstr "Привет, я Pwnagotchi! Стартуем!"
 
 msgid "New day, new hunt, new pwns!"
 msgstr "Новый день, новая охота, новые взломы!"
 
 msgid "Hack the Planet!"
-msgstr "Хак зе планет!"
+msgstr "Взломай эту Планету!"
 
 msgid "AI ready."
-msgstr "AI готов."
+msgstr "A.I. готов."
 
 msgid "The neural network is ready."
 msgstr "Нейронная сеть готова."
+msgid "Generating keys, do not turn off ..."
+msgstr "Генерация ключей, не выключайте..."
 
 #, python-brace-format
 msgid "Hey, channel {channel} is free! Your AP will say thanks."
 msgstr "Эй, канал {channel} свободен! Ваша точка доступа скажет спасибо."
+msgid "Reading last session logs ..."
+msgstr "Чтение логов последнего сеанса..."
+
+#, python-brace-format
+msgid "Read {lines_so_far} log lines so far ..."
+msgstr "Чтение {lines_so_far} строк журнала..."
 
 msgid "I'm bored ..."
 msgstr "Мне скучно …"
@@ -61,9 +67,14 @@ msgstr "Мне очень грустно …"
 
 msgid "I'm sad"
 msgstr "Мне грустно"
+msgid "Leave me alone ..."
+msgstr "Оставь меня в покое..."
+
+msgid "I'm mad at you!"
+msgstr "Я зол на тебя!"
 
 msgid "I'm living the life!"
-msgstr "Угараю по полной!"
+msgstr "Живу полной жизнью!"
 
 msgid "I pwn therefore I am."
 msgstr "Я взламываю, поэтому я существую."
@@ -75,15 +86,22 @@ msgid "I'm having so much fun!"
 msgstr "Мне так весело!"
 
 msgid "My crime is that of curiosity ..."
-msgstr "Моe преступление - это любопытство …"
+msgstr "Моe преступление - это из любопытства…"
 
 #, python-brace-format
 msgid "Hello {name}! Nice to meet you. {name}"
-msgstr "Привет, {name}! Приятно познакомиться. {name}"
+msgstr "Привет, {name}! Рад встрече с тобой!"
 
 #, python-brace-format
 msgid "Unit {name} is nearby! {name}"
-msgstr "Цель {name} близко! {name}"
+msgstr "Цель {name} близко!"
+#, python-brace-format
+msgid "Hey {name} how are you doing?"
+msgstr "Хэй {nume}! Как дела?"
+
+#, python-brace-format
+msgid "Unit {name} is nearby!"
+msgstr "Цель {name} рядом!"
 
 #, python-brace-format
 msgid "Uhm ... goodbye {name}"
@@ -91,11 +109,11 @@ msgstr "Хм … до свидания {name}"
 
 #, python-brace-format
 msgid "{name} is gone ..."
-msgstr "{name} исчезла …"
+msgstr "{name} ушла…"
 
 #, python-brace-format
 msgid "Whoops ... {name} is gone."
-msgstr "Упс … {name} исчезла."
+msgstr "Упс… {name} исчезла."
 
 #, python-brace-format
 msgid "{name} missed!"
@@ -103,12 +121,17 @@ msgstr "{name} упустил!"
 
 msgid "Missed!"
 msgstr "Промахнулся!"
+msgid "Good friends are a blessing!"
+msgstr "Хорошие друзья - это благословение!"
+
+msgid "I love my friends!"
+msgstr "Я люблю своих друзей!"
 
 msgid "Nobody wants to play with me ..."
 msgstr "Никто не хочет со мной играть ..."
 
 msgid "I feel so alone ..."
-msgstr "Мне так одиноко …"
+msgstr "Я так одинок…"
 
 msgid "Where's everybody?!"
 msgstr "Где все?!"
@@ -118,11 +141,16 @@ msgid "Napping for {secs}s ..."
 msgstr "Дремлет {secs}с …"
 
 msgid "Zzzzz"
-msgstr "Zzzzz"
+msgstr "Хррр..."
 
 #, python-brace-format
 msgid "ZzzZzzz ({secs}s)"
-msgstr "ZzzZzzz ({secs}c)"
+msgstr "Хррррр.. ({secs}c)"
+msgid "Good night."
+msgstr "Доброй ночи."
+
+msgid "Zzz"
+msgstr "Хрррр"
 
 #, python-brace-format
 msgid "Waiting for {secs}s ..."
@@ -130,7 +158,7 @@ msgstr "Ждем {secs}c …"
 
 #, python-brace-format
 msgid "Looking around ({secs}s)"
-msgstr "Оглядываюсь вокруг ({secs}с)"
+msgstr "Осматриваюсь вокруг ({secs}с)"
 
 #, python-brace-format
 msgid "Hey {what} let's be friends!"

From 48dc751d130d7c5ef639e5a0f550998cce9bec55 Mon Sep 17 00:00:00 2001
From: Evgeny Zelenin <m-b-g@yandex.ru>
Date: Wed, 27 Nov 2019 12:09:24 +0500
Subject: [PATCH 14/20] Update voice.po

---
 pwnagotchi/locale/ru/LC_MESSAGES/voice.po | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pwnagotchi/locale/ru/LC_MESSAGES/voice.po b/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
index ce3ca75..d06a4f9 100644
--- a/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
@@ -86,7 +86,7 @@ msgid "I'm having so much fun!"
 msgstr "Мне так весело!"
 
 msgid "My crime is that of curiosity ..."
-msgstr "Моe преступление - это из любопытства…"
+msgstr "Моё преступление - это любопытство…"
 
 #, python-brace-format
 msgid "Hello {name}! Nice to meet you. {name}"

From 07f8e7bd4ad01ea712ca1077f925608e2c39c320 Mon Sep 17 00:00:00 2001
From: Evgeny Zelenin <m-b-g@yandex.ru>
Date: Wed, 27 Nov 2019 22:01:43 +0500
Subject: [PATCH 15/20] Update voice.po

---
 pwnagotchi/locale/ru/LC_MESSAGES/voice.po | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pwnagotchi/locale/ru/LC_MESSAGES/voice.po b/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
index d06a4f9..4103dae 100644
--- a/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
+++ b/pwnagotchi/locale/ru/LC_MESSAGES/voice.po
@@ -201,7 +201,7 @@ msgstr "Заимел {num} новых друзей\n"
 
 #, python-brace-format
 msgid "Got {num} handshakes\n"
-msgstr "Получил {num} рукопожатие\n"
+msgstr "Получил {num} рукопожатий\n"
 
 msgid "Met 1 peer"
 msgstr "Встретился один знакомый"

From 7cb52ba33a0cdbcd395b76e0db26141b1bec093f Mon Sep 17 00:00:00 2001
From: dadav <33197631+dadav@users.noreply.github.com>
Date: Mon, 25 Nov 2019 20:51:25 +0100
Subject: [PATCH 16/20] Add wpa-sec password download

---
 pwnagotchi/defaults.yml               |  1 +
 pwnagotchi/plugins/default/wpa-sec.py | 99 ++++++++++++++++++++-------
 2 files changed, 74 insertions(+), 26 deletions(-)

diff --git a/pwnagotchi/defaults.yml b/pwnagotchi/defaults.yml
index c3093ce..596ac67 100644
--- a/pwnagotchi/defaults.yml
+++ b/pwnagotchi/defaults.yml
@@ -39,6 +39,7 @@ main:
             enabled: false
             api_key: ~
             api_url: "https://wpa-sec.stanev.org"
+            download_results: false
         wigle:
             enabled: false
             api_key: ~
diff --git a/pwnagotchi/plugins/default/wpa-sec.py b/pwnagotchi/plugins/default/wpa-sec.py
index ee99f10..7dd8a84 100644
--- a/pwnagotchi/plugins/default/wpa-sec.py
+++ b/pwnagotchi/plugins/default/wpa-sec.py
@@ -1,19 +1,27 @@
 import os
 import logging
+import threading
 import requests
+from datetime import datetime
 from pwnagotchi.utils import StatusFile
-import pwnagotchi.plugins as plugins
+from pwnagotchi import plugins
+from json.decoder import JSONDecodeError
 
 
 class WpaSec(plugins.Plugin):
     __author__ = '33197631+dadav@users.noreply.github.com'
-    __version__ = '2.0.1'
+    __version__ = '2.1.0'
     __license__ = 'GPL3'
     __description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org'
 
     def __init__(self):
         self.ready = False
-        self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
+        self.lock = threading.Lock()
+        try:
+            self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
+        except JSONDecodeError as json_err:
+            os.remove("/root/.wpa_sec_uploads")
+            self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
         self.options = dict()
         self.skip = list()
 
@@ -35,6 +43,29 @@ class WpaSec(plugins.Plugin):
             except requests.exceptions.RequestException as req_e:
                 raise req_e
 
+
+    def _download_from_wpasec(self, output, timeout=30):
+        """
+        Downloads the results from wpasec and safes them to output
+
+        Output-Format: bssid, station_mac, ssid, password
+        """
+        api_url = self.options['api_url']
+        if not api_url.endswith('/'):
+            api_url = f"{api_url}/"
+        api_url = f"{api_url}?api&dl=1"
+
+        cookie = {'key': self.options['api_key']}
+        try:
+            result = requests.get(api_url, cookies=cookie, timeout=timeout)
+            with open(output, 'wb') as output_file:
+                output_file.write(result.content)
+        except requests.exceptions.RequestException as req_e:
+            raise req_e
+        except OSError as os_e:
+            raise os_e
+
+
     def on_loaded(self):
         """
         Gets called when the plugin gets loaded
@@ -53,32 +84,48 @@ class WpaSec(plugins.Plugin):
         """
         Called in manual mode when there's internet connectivity
         """
-        if self.ready:
-            config = agent.config()
-            display = agent.view()
-            reported = self.report.data_field_or('reported', default=list())
+        with self.lock:
+            if self.ready:
+                config = agent.config()
+                display = agent.view()
+                reported = self.report.data_field_or('reported', default=list())
 
-            handshake_dir = config['bettercap']['handshakes']
-            handshake_filenames = os.listdir(handshake_dir)
-            handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
-                               filename.endswith('.pcap')]
-            handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
+                handshake_dir = config['bettercap']['handshakes']
+                handshake_filenames = os.listdir(handshake_dir)
+                handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
+                                   filename.endswith('.pcap')]
+                handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
 
-            if handshake_new:
-                logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
+                if handshake_new:
+                    logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
+
+                    for idx, handshake in enumerate(handshake_new):
+                        display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
+                        display.update(force=True)
+                        try:
+                            self._upload_to_wpasec(handshake)
+                            reported.append(handshake)
+                            self.report.update(data={'reported': reported})
+                            logging.info("WPA_SEC: Successfully uploaded %s", handshake)
+                        except requests.exceptions.RequestException as req_e:
+                            self.skip.append(handshake)
+                            logging.error("WPA_SEC: %s", req_e)
+                            continue
+                        except OSError as os_e:
+                            logging.error("WPA_SEC: %s", os_e)
+                            continue
+
+                if 'download_results' in self.options and self.options['download_results']:
+                    cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')
+                    if os.path.exists(cracked_file):
+                        last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
+                        if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
+                            return
 
-                for idx, handshake in enumerate(handshake_new):
-                    display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
-                    display.update(force=True)
                     try:
-                        self._upload_to_wpasec(handshake)
-                        reported.append(handshake)
-                        self.report.update(data={'reported': reported})
-                        logging.info("WPA_SEC: Successfully uploaded %s", handshake)
+                        self._download_from_wpasec(os.path.join(handshake_dir, 'wpa-sec.cracked.potfile'))
+                        logging.info("WPA_SEC: Downloaded cracked passwords.")
                     except requests.exceptions.RequestException as req_e:
-                        self.skip.append(handshake)
-                        logging.error("WPA_SEC: %s", req_e)
-                        continue
+                        logging.debug("WPA_SEC: %s", req_e)
                     except OSError as os_e:
-                        logging.error("WPA_SEC: %s", os_e)
-                        continue
+                        logging.debug("WPA_SEC: %s", os_e)

From cc5c46906f869a62d64ad74f61d8ccc539b845bd Mon Sep 17 00:00:00 2001
From: dadav <33197631+dadav@users.noreply.github.com>
Date: Wed, 27 Nov 2019 18:31:41 +0100
Subject: [PATCH 17/20] Add plugins page

---
 pwnagotchi/plugins/__init__.py           | 25 +++++++++++++--
 pwnagotchi/plugins/default/bt-tether.py  |  4 +++
 pwnagotchi/plugins/default/example.py    |  4 +++
 pwnagotchi/ui/web/handler.py             | 21 +++++++------
 pwnagotchi/ui/web/static/css/style.css   | 35 ++++++++++++++++++++-
 pwnagotchi/ui/web/templates/base.html    |  3 +-
 pwnagotchi/ui/web/templates/plugins.html | 39 ++++++++++++++++++++++++
 7 files changed, 118 insertions(+), 13 deletions(-)
 create mode 100644 pwnagotchi/ui/web/templates/plugins.html

diff --git a/pwnagotchi/plugins/__init__.py b/pwnagotchi/plugins/__init__.py
index 44c2787..c5efaf7 100644
--- a/pwnagotchi/plugins/__init__.py
+++ b/pwnagotchi/plugins/__init__.py
@@ -6,7 +6,7 @@ import logging
 
 default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
 loaded = {}
-
+database = {}
 
 class Plugin:
     @classmethod
@@ -18,6 +18,26 @@ class Plugin:
         logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
         loaded[plugin_name] = plugin_instance
 
+def toggle_plugin(name, enable=True):
+    """
+    Load or unload a plugin
+
+    returns True if changed, otherwise False
+    """
+    global loaded, database
+    if not enable and name in loaded:
+        if getattr(loaded[name], 'on_unload', None):
+            loaded[name].on_unload()
+        del loaded[name]
+        return True
+
+    if enable and name in database and name not in loaded:
+        load_from_file(database[name])
+        one(name, 'loaded')
+        return True
+
+    return False
+
 
 def on(event_name, *args, **kwargs):
     for plugin_name, plugin in loaded.items():
@@ -48,10 +68,11 @@ def load_from_file(filename):
 
 
 def load_from_path(path, enabled=()):
-    global loaded
+    global loaded, database
     logging.debug("loading plugins from %s - enabled: %s" % (path, enabled))
     for filename in glob.glob(os.path.join(path, "*.py")):
         plugin_name = os.path.basename(filename.replace(".py", ""))
+        database[plugin_name] = filename
         if plugin_name in enabled:
             try:
                 load_from_file(filename)
diff --git a/pwnagotchi/plugins/default/bt-tether.py b/pwnagotchi/plugins/default/bt-tether.py
index 6acaf27..5f986f8 100644
--- a/pwnagotchi/plugins/default/bt-tether.py
+++ b/pwnagotchi/plugins/default/bt-tether.py
@@ -466,7 +466,11 @@ class BTTether(plugins.Plugin):
         logging.info("BT-TETHER: Successfully loaded ...")
         self.ready = True
 
+    def on_unload(self):
+        self.ui.remove_element('bluetooth')
+
     def on_ui_setup(self, ui):
+        self.ui = ui
         ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
                                                  label_font=fonts.Bold, text_font=fonts.Medium))
 
diff --git a/pwnagotchi/plugins/default/example.py b/pwnagotchi/plugins/default/example.py
index 035a30d..37eccb4 100644
--- a/pwnagotchi/plugins/default/example.py
+++ b/pwnagotchi/plugins/default/example.py
@@ -25,6 +25,10 @@ class Example(plugins.Plugin):
     def on_loaded(self):
         logging.warning("WARNING: this plugin should be disabled! options = " % self.options)
 
+    # called before the plugin is unloaded
+    def on_unload(self):
+        pass
+
     # called hen there's internet connectivity
     def on_internet_available(self, agent):
         pass
diff --git a/pwnagotchi/ui/web/handler.py b/pwnagotchi/ui/web/handler.py
index c11b173..788a3d1 100644
--- a/pwnagotchi/ui/web/handler.py
+++ b/pwnagotchi/ui/web/handler.py
@@ -179,16 +179,19 @@ class Handler:
 
     def plugins(self, name, subpath):
         if name is None:
-            # show plugins overview
-            abort(404)
+            return render_template('plugins.html', loaded=plugins.loaded, database=plugins.database)
+
+        if name == 'toggle' and request.method == 'POST':
+            checked = True if 'enabled' in request.form else False
+            return 'success' if plugins.toggle_plugin(request.form['plugin'], checked) else 'failed'
+
+        if name in plugins.loaded and plugins.loaded[name] is not None and hasattr(plugins.loaded[name], 'on_webhook'):
+            try:
+                return plugins.loaded[name].on_webhook(subpath, request)
+            except Exception:
+                abort(500)
         else:
-            if name in plugins.loaded and hasattr(plugins.loaded[name], 'on_webhook'):
-                try:
-                    return plugins.loaded[name].on_webhook(subpath, request)
-                except Exception:
-                    abort(500)
-            else:
-                abort(404)
+            abort(404)
 
     # serve a message and shuts down the unit
     def shutdown(self):
diff --git a/pwnagotchi/ui/web/static/css/style.css b/pwnagotchi/ui/web/static/css/style.css
index 8acc267..a005a1d 100644
--- a/pwnagotchi/ui/web/static/css/style.css
+++ b/pwnagotchi/ui/web/static/css/style.css
@@ -31,4 +31,37 @@ a.read {
 
 p.messagebody {
     padding: 1em;
-}
\ No newline at end of file
+}
+
+li.navitem {
+    width: 16.66% !important;
+    clear: none !important;
+}
+
+/* Custom indentations are needed because the length of custom labels differs from
+   the length of the standard labels */
+.custom-size-flipswitch.ui-flipswitch .ui-btn.ui-flipswitch-on {
+    text-indent: -5.9em;
+}
+
+.custom-size-flipswitch.ui-flipswitch .ui-flipswitch-off {
+    text-indent: 0.5em;
+}
+
+/* Custom widths are needed because the length of custom labels differs from
+   the length of the standard labels */
+.custom-size-flipswitch.ui-flipswitch {
+    width: 8.875em;
+}
+
+.custom-size-flipswitch.ui-flipswitch.ui-flipswitch-active {
+    padding-left: 7em;
+    width: 1.875em;
+}
+
+@media (min-width: 28em) {
+    /*Repeated from rule .ui-flipswitch above*/
+    .ui-field-contain > label + .custom-size-flipswitch.ui-flipswitch {
+        width: 1.875em;
+    }
+}
diff --git a/pwnagotchi/ui/web/templates/base.html b/pwnagotchi/ui/web/templates/base.html
index c39e080..be2d618 100644
--- a/pwnagotchi/ui/web/templates/base.html
+++ b/pwnagotchi/ui/web/templates/base.html
@@ -47,6 +47,7 @@
         ( '/inbox/new', 'new', 'mail', 'New' ),
         ( '/inbox/profile', 'profile', 'info', 'Profile' ),
         ( '/inbox/peers', 'peers', 'user', 'Peers' ),
+        ( '/plugins', 'plugins', 'grid', 'Plugins' ),
     ] %}
     {% set active_page = active_page|default('inbox') %}
 
@@ -54,7 +55,7 @@
         <div data-role="navbar" data-iconpos="left">
             <ul>
                 {% for href, id, icon, caption in navigation %}
-                    <li>
+                    <li class="navitem">
                        <a href="{{ href }}" id="{{ id }}" data-icon="{{ icon }}" class="{{ 'ui-btn-active' if active_page == id }}">{{ caption }}</a>
                     </li>
                 {% endfor %}
diff --git a/pwnagotchi/ui/web/templates/plugins.html b/pwnagotchi/ui/web/templates/plugins.html
new file mode 100644
index 0000000..e96d244
--- /dev/null
+++ b/pwnagotchi/ui/web/templates/plugins.html
@@ -0,0 +1,39 @@
+{% extends "base.html" %}
+{% set active_page = "plugins" %}
+
+{% block title %}
+Plugins
+{% endblock %}
+
+{% block script %}
+$(function(){
+    $("input[type=checkbox]").change(function(e) {
+          var checkbox = $(this);
+          var form = checkbox.closest("form");
+          var url = form.attr('action');
+
+          $.ajax({
+             type: "POST",
+             url: url,
+             data: form.serialize(),
+             success: function(data) {
+                  if( data.indexOf('failed') != -1 ) {
+                      alert('Could not be toggled.');
+                  }
+            }
+        });
+    });
+});
+{% endblock %}
+{% block content %}
+<div style="padding: 1em">
+    {% for name in database.keys() %}
+      <h4>{{name}}</h4>
+      <form method="POST" action="/plugins/toggle">
+        <input type="checkbox" data-role="flipswitch" name="enabled" id="flip-checkbox-{{name}}" data-on-text="Enabled" data-off-text="Disabled" data-wrapper-class="custom-size-flipswitch" {% if name in loaded %} checked {% endif %}>
+        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+        <input type="hidden" name="plugin" value="{{ name }}"/>
+      </form>
+    {% endfor %}
+</div>
+{% endblock %}

From e72fd08fb4985462123f3175cc831b4b50207c88 Mon Sep 17 00:00:00 2001
From: Casey Diemel <diemelcw@gmail.com>
Date: Thu, 28 Nov 2019 14:39:15 -0500
Subject: [PATCH 18/20] added on_unfiltered_wifi_list

Signed-off-by: Casey Diemel <diemelcw@gmail.com>
---
 pwnagotchi/plugins/default/example.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/pwnagotchi/plugins/default/example.py b/pwnagotchi/plugins/default/example.py
index 37eccb4..58fe448 100644
--- a/pwnagotchi/plugins/default/example.py
+++ b/pwnagotchi/plugins/default/example.py
@@ -122,6 +122,11 @@ class Example(plugins.Plugin):
     def on_wifi_update(self, agent, access_points):
         pass
 
+    # called when the agent refreshed an unfiltered access point list
+    # this list contains all access points that were detected BEFORE filtering
+    def on_unfiltered_ap_list(self, agent, access_points):
+        pass
+
     # called when the agent is sending an association frame
     def on_association(self, agent, access_point):
         pass

From 855bda9104fd1587caf5d4ffd7c11f274b958e87 Mon Sep 17 00:00:00 2001
From: Evg33 <evg33@evg33.ru>
Date: Thu, 28 Nov 2019 23:02:22 +0300
Subject: [PATCH 19/20] feature/plugin/web/reboot

---
 pwnagotchi/ui/web/handler.py           | 9 +++++++++
 pwnagotchi/ui/web/templates/index.html | 7 +++++++
 2 files changed, 16 insertions(+)

diff --git a/pwnagotchi/ui/web/handler.py b/pwnagotchi/ui/web/handler.py
index 788a3d1..f855253 100644
--- a/pwnagotchi/ui/web/handler.py
+++ b/pwnagotchi/ui/web/handler.py
@@ -34,6 +34,7 @@ class Handler:
         self._app.add_url_rule('/ui', 'ui', self.with_auth(self.ui))
 
         self._app.add_url_rule('/shutdown', 'shutdown', self.with_auth(self.shutdown), methods=['POST'])
+        self._app.add_url_rule('/reboot', 'reboot', self.with_auth(self.reboot), methods=['POST'])
         self._app.add_url_rule('/restart', 'restart', self.with_auth(self.restart), methods=['POST'])
 
         # inbox
@@ -201,6 +202,14 @@ class Handler:
         finally:
             _thread.start_new_thread(pwnagotchi.shutdown, ())
 
+    # serve a message and reboot the unit
+    def reboot(self):
+          try:
+              return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
+                                     message='Rebooting ...')
+          finally:
+              _thread.start_new_thread(pwnagotchi.reboot, ())
+
     # serve a message and restart the unit in the other mode
     def restart(self):
         mode = request.form['mode']
diff --git a/pwnagotchi/ui/web/templates/index.html b/pwnagotchi/ui/web/templates/index.html
index 8463cd2..475a97a 100644
--- a/pwnagotchi/ui/web/templates/index.html
+++ b/pwnagotchi/ui/web/templates/index.html
@@ -26,6 +26,13 @@ window.onload = function() {
 				<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
 			</form>
 		</li>
+		<li>
+			<form class="action" method="post" action="/reboot"
+          onsubmit="return confirm('this will reboot the unit, continue?');">
+				<input type="submit" class="button ui-btn ui-corner-all" value="Reboot"/>
+				<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+			</form>
+		</li>
 		<li>
 			<form class="action" method="post" action="/restart"
           onsubmit="return confirm('This will restart the service in {{ other_mode }} mode, continue?');">

From f5a94fde9639e9e4afe6fa4938b5ec749046f2ad Mon Sep 17 00:00:00 2001
From: dadav <33197631+dadav@users.noreply.github.com>
Date: Thu, 28 Nov 2019 21:55:20 +0100
Subject: [PATCH 20/20] Add url to plugin

---
 pwnagotchi/ui/web/templates/plugins.html | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/pwnagotchi/ui/web/templates/plugins.html b/pwnagotchi/ui/web/templates/plugins.html
index e96d244..82714d5 100644
--- a/pwnagotchi/ui/web/templates/plugins.html
+++ b/pwnagotchi/ui/web/templates/plugins.html
@@ -7,13 +7,13 @@ Plugins
 
 {% block script %}
 $(function(){
-    $("input[type=checkbox]").change(function(e) {
+    $('input[type=checkbox]').change(function(e) {
           var checkbox = $(this);
-          var form = checkbox.closest("form");
+          var form = checkbox.closest('form');
           var url = form.attr('action');
 
           $.ajax({
-             type: "POST",
+             type: 'POST',
              url: url,
              data: form.serialize(),
              success: function(data) {
@@ -28,7 +28,9 @@ $(function(){
 {% block content %}
 <div style="padding: 1em">
     {% for name in database.keys() %}
-      <h4>{{name}}</h4>
+      <h4>
+        <a {% if name in loaded and loaded[name].on_webhook is defined %} href="/plugins/{{name}}" {% endif %}>{{name}}</a>
+      </h4>
       <form method="POST" action="/plugins/toggle">
         <input type="checkbox" data-role="flipswitch" name="enabled" id="flip-checkbox-{{name}}" data-on-text="Enabled" data-off-text="Disabled" data-wrapper-class="custom-size-flipswitch" {% if name in loaded %} checked {% endif %}>
         <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>