Bot SMS pod Linuksem

Ostatnio w firmie pojawił się temat stworzenia SMS bota. Zacząłem więc szukać rozwiązań. Jeden z pomysłów, to postawienienie komputera z Linuksem na pokładzie, podłączenie do niego telefonu z ofertą, w której jest miliard darmowych SMS-ów i uruchomienie czegoś magicznego, co zajmie się odbieraniem i wysyłaniem wiadomości. I to magiczne coś właśnie zamierzam opisać.

Rozwiązanie które można zastosować, okazało się prostsze niż myślałem. Można użyć do tego świetnego programu, który jest dostępny zarówno pod Linuksem jak i Windowsem - mowa o Gammu. Jest to program, który komunikuje się z telefonem, lub modemem i służy do różnego rodzaju cznności, takich jak odbieranie/wysyłanie SMS-ów, zarządzanie kontaktami, zadaniami, kalenarzem itp. Ja oczywiście skupię się na odbieraniu i wysyłaniu SMS-ów.

Tak więc do komunikacji użytkownika z botem posłużę się Gammu - to już ustaliliśmy. Żeby było ciekawiej, Gammu posiada demona do wysyłania i odbierania SMS-ów - gammu-smsd. Jako backend do przechowywania danych można użyć zwykłych plików lub bazy danych (obsługiwany jest MySQL, PostgreSQL i SQLite). Pliki odradzam, bo to nieco ogranicza funkcjonalność - przykładowo nie jest możliwe wysyłanie wiadomości przy użyciu polecenia gammu-smsd-inject lub nie można przekazać żadnego id wiadomości do skryptu, który można uruchomić po otrzymaniu wiadomości (o tym w dalszej części). Lepiej zatem skorzystać z bazy danych. Ja wybrałem PostgreSQL.

Konfiguracja Gammu oraz SMSD

Najpierw trzeba zacząć od komunikacji Gammu z telefonem. Mój telefon to Sony Ericsson k800i. Pomijam fakt, że trzeba podłączyć telefon do komputera przez kabel USB ;) Poniżej zamieszczam źródła plików konfiguracyjnych:

~/.gammurc

[gammu] 
port = /dev/ttyACM0 
model = 
connection = at19200 
synchronizetime = no 
logfile = 
logformat = errorsdate 
use_locking = 
gammuloc =

/etc/gammu_smsdrc

[gammu] 
port = /dev/ttyACM0 
model = 
connection = at19200 
synchronizetime = no 
logfile = 
logformat = errorsdate 
use_locking = 
gammuloc = 

[smsd] 
Service = pgsql 
LogFile = syslog 
DebugLevel = 255 
DeliveryReport = log 
RunOnReceive = /home/jaro/bin/sms_trigger.py 
User = uzytkownik_bazy 
Password = haslo_bazy 
PC = localhost 
Database = gammu

Poprawność konfiguracji połączenia z telefonem można sprawdzić przez polecenie gammu identify. U mnie wyjście wyglądało tak:

# gammu identify 
Manufacturer : Sony Ericsson 
Model : K800i (AAD-3022031-BV) 
Firmware : R8BF003 080130 2133 CXC1250212_ORANGE_WI 
IMEI : WOLE_NIE_POKAZYWAĆ 
Kod produktu : AAD-3022031-BV 
SIM IMSI : WOLE_NIE_POKAZYWAĆ

Przed uruchomieniem demona smsd, trzeba utworzyć odpowiednie tabele w bazie danych. W dokumentacji znajduje się przykładowy plik ze skryptem SQL, który tworzy te tabele. U mnie znajduje się on w lokalizacji: /usr/share/doc/gammu/examples/sql/pgsql.sql Wystarczy uruchomić skrypt i voila!

Skrypt (bot)

W pliku gammu_smsdrc jest opcja RunOnReceive, która, jak się można domyślić, wskazuje skrypt uruchamiany po odebraniu wiadomości. Skrypt jest uruchamiany z parametrem, który zawiera id wiersza w bazie danych w tabeli inbox. Wiersz zawiera dane przesłanej wiadomości. Napisałem w Pythonie przykładowy skrypt, który odczytuje odebraną wiadomość, sprawdza jaki tekst wysłano i na jego podstawie wysyła odpowiedź.

#!/usr/bin/python

import psycopg2 as db, sys, os

db_user = 'nazwa_uzytkownika'
db_password = 'haslo_do_bazy'
db_host = 'localhost'
db_name = 'gammu'


def main(argv=None):
        try:
                sms_id = sys.argv[1]
        except IndexError:
                print 'Za malo parametrow'
                sys.exit()
        
        try:
                conn = db.connect("dbname=%s user=%s password=%s host=%s" % (db_name, db_user, db_password, db_host))
        except:
                print 'Blad podczas polaczenia z baza'
                
        if not sms_id.isdigit():
                print 'Niepoprawny parametr'
                sys.exit()
        
        cur = conn.cursor()
        cur.execute("SELECT * FROM inbox WHERE id=%s" % sms_id)
        
        row = cur.fetchone()
        
        if not row:
                print 'Nie znaleziono wiadomosci sms o podanym id'
                sys.exit()
                
                
        rcp_num = row[3]
        command = row[8].strip().lower()
        
        if command == 'jezyk':
                response = "Python"
        elif command == 'system':
                response = "Linux"
        else:
                response = "Nieznana komenda"
        
        
        os.system("gammu-smsd-inject TEXT %s -text '%s'" % (rcp_num, response)) 
        


if __name__ == '__main__':
        main()
        

Finał - uruchomienie demona

Wreszcie przechodzimy do puenty - uruchomienia demona. Wystarczy krótkie:

gammu-smsd --daemon

I w ten oto sposób, telefon jest gotowy do odbierania i wysyłania wiadomości.

Debugowanie Django w Winpdb

Django posiada wbudowaną stronę służącą do zgłaszania błędów, dzięki której można zidentyfikować źródło błędu. Pojawia się zawsze gdy wystąpi nieobsłużony wyjątek, pod warunkiem, że w ustawieniach opcja DEBUG przyjmuje wartość True. Każdy developer Django pewnie widział ją nie raz. Jednak błędy oprogramowania, to nie tylko wyjątki, ale także niepoprawnie wyświetlane dane - tego Django już nie wyłapie. Czasami też zdaża się, że informacja ze strony błędu, to za mało, żeby zidentyfikować błąd. Można wtedy posiłkować się funkcją print, której zwróconą wartość widać w konsoli, na której został uruchomiony serwer developerski. Jest to jednak przydatne przy naprawie drobnych błędów, gdzie szybko można zorientować się co może być ich przyczyną. Korzystanie z print na pewno nie jest debugowaniem z prawdziwego zdarzenia. Żeby dokładnie prześledzić działanie stworzonego skryptu, najlepiej posłużyć się narzędziem przeznaczonym do tego. Takim narzędziem jest Winpdb - graficzna wersja pdb (Python Debugger).

Debugowanie aplikacji stworzonych w Django jest prostsze, niż mogłoby się to wydawać. Do prezentacji posłużyłem się bardzo prostym, testowym projektem:

test_project/
|-- __init__.py
|-- manage.py
|-- settings.py
|-- test_app
|   |-- __init__.py
|   |-- models.py
|   `-- views.py
`-- urls.py

Kod który chcę prześledzić w poszukiwaniu błędów znajduje się w pliku test_app/views.py

def test_view(request):
        a = 1
        b = 3
        c = a + b

        print c

Pierwszym krokiem do prześledzenia skryptu, jest uruchomienie developerskiego serwera Django z poziomu Winpdb. Najpierw wybieramy "File -> Launch" W oknie które się pojawi,trzeba wpisać polecenie, które uruchomi serwer developerski.

Launch command

W moim przypadku było to:

/home/jaro/strony/test_project/manage.py runserver --noreload

Opcja noreload umożliwia debugowanie i zatrzymywanie się wykonywania skryptu w breakpointach. Skrypt zostaje załadowany i żeby go uruchomić trzeba kliknąć na ikonkę play. Spowoduje to uruchomienie serwera developerskiego Django. W konsoli wyświetli się komunikat serwera.

Konsola Winpdb

Do zatrzymania wykonywania skryptu w wybranym miejscu, można posłużyć się breakpointem. W pierwszej kolejności trzeba otworzyć plik, w którym breakpoint będzie ustawiony (File -> Open Source). Plik załaduje się do okna "Source".

Breakpoint

Breakpoint ustawia się poprzez kliknięcie na linię kodu, która z kolei zostanie wyróżniona czerwonym kolorem, tak jak jest to widoczne na powyższym rysunku.

Już wszystko jest gotowe do debugowania, teraz wystarczy wejść w przeglądarce na stronę, która uruchomi interesujący nas widok (np. http://localhost:8000/). Strona nie załaduje się, a jak przejdziemy do debuggera, to zobaczymy skrypt zatrzymany we wskazanej wcześniej linii.

Debugowanie skryptu

W widocznych oknach można zobaczyć aktualne wartości zmiennych, uruchomione wątki, stos, konsolę oraz kod źródłowy, którego kolejne linie można wykonywać interaktywnie, korzystając z przycisków w pasku narzędzi.

Wysyłanie maila z konsoli - skrypt Pythona

Jakiś czas temu postawiłem subversion na serwerze developerskim, który służył do testowania aplikacji którą piszę. Chciałem żeby informacje o każdym commit-cie były wysyłane na maila. Postanowiłem zrobić to przy pomocy skryptu w Pythonie.

import smtplib
import getopt
import email.Message
import sys

def main(argv):
    opts, args = getopt.getopt(argv, "u:p:s:h:t:r:t:", ["server=", "port=", "sender=", password=", "to=", "subject=", "text="])
    for opt, arg in opts:
        if opt in ["-u", "--server"]:
            serverURL = arg
        elif opt in ["-p", "--port"]:
            port = arg
        elif opt in ["-s", "--sender"]:
            sender = arg
        elif opt in ["-h", "--password"]:
            password = arg
        elif opt in ["-t", "--to"]:
            to = arg
        elif opt in ["-r", "--subject"]:
            subject = arg
        elif opt in ["-t", "--text"]:
            text = arg

    message = email.Message.Message()
    message["To"]      = to
    message["From"]    = sender
    message["Subject"] = subject
    message.set_payload(text)
    if port:
        mailServer = smtplib.SMTP(serverURL, port)
    else:
        mailServer = smtplib.SMTP(serverURL)
    mailServer.ehlo()
    mailServer.starttls()
    mailServer.ehlo()
    mailServer.login(sender, password)
    
    mailServer.sendmail(sender, to, message.as_string())
    mailServer.quit()

Wiadomość ma wysyłać się z serwera SMTP na gmailu, więc istotna jest tu część kodu:

mailServer.ehlo()
mailServer.starttls()
mailServer.ehlo()

Żeby wysłać maila, wystarczy wpisać polecenie: python /srv/mail.py --server=smtp.gmail.com --port=587 --sender=<adres_nadawcy> --password=<hasło_nadawcy> --to=<adres_odbiorcy> --subject="temat" --text="tresc wiadomosci"

Rozwijanie nazw w standardowej konsoli pythona

Konsola Pythona najczęściej służy mi do testowania drobnych fragmentów kodu. Zdarzało się że zapominałem jak nazywa się jakaś metoda danej klasy, albo chciałem szybko zobaczyć, jakie metody dana klasa udostępnia. Odkąd włączyłem rozwijanie nazw, które działa Unixopodobnie (po naciśnięciu klawisza Tab), nie stanowi to żadnego problemu. Sprawa jest bardzo prosta. Wystarczy w katalogu domowym utworzyć plik .pythonrc:

try:
    import readline
except ImportError:
    print "Brak modułu readline"
else:
    import rlcompleter
    readline.parse_and_bind("tab: complete")

Na koniec należy ustawić zmienną środowiskową:

export PYTHONSTARTUP=~/.pythonrc

Żeby nie robić tego za każdym razem po ponownym uruchomieniu systemu, wystarczy dopisać powyższą linijkę kodu do pliku .bashrc znajdującego się w katalogu domowym.