Telegram – chat bot

Přeskočíme kecy o botech a rovnou se pustíme do práce. Největší platformou pro chatovací boty s otevřeným API nabízí Telegram. Dnes si ukážeme, jak si vytvořit bota, který bude interagovat na uživatelovo požadavky.

Vytvoření bota

Všechny boty vytváříme u toce botů – BotFather. Uděláme to tak, že v seznamu konverzací vyhledáme BotFather. V detailu konverzace vytvoříme nového bota příkazem /newbot a postupujeme podle pokynů.

Budete vyzváni na jméno bota (libovolné znaky včetně diakritiky) a následně na uživatelské jméno bota: slovo bez diakritiky, mezer s příponou bot nebo _bot. Uživatelské jméno musí být v celém Telegramu jedinečné.

Po vytvoření bota bude zobrazeno jeho API access token – dále jen token. Ten si pečlivě uchovejte a nikomu ho nesdělujte – slouží k ovládání bota.

Ovládání bota

V Telegramu existují dva způsoby, jak získat zprávy poslané botovi: long polling a webhooks

Long pooling

Při tomto přístupu se periodicky doptáváte API Telegramu na nové zprávy. Musíte vhodně nastavit periodu doptávání se na nové zprávy.

Webhooks

Při tomto přístupu je při každé interakci s botem zpráva odeslaná i na náš server. Tento přístup mám raději, protože vypadá jako komunikace v reálném čase.

V tomto článku si popíšeme použití webhooks. Pro použití webhooks potřebujeme php script, který běží na veřejné adrese s HTTPS certifikátem. Pro naši ukázku například: https://www.venca-x.cz/bot/pocasi.php

Teď trochu přeskočíme obsah souboru pocasi.php a rovnou tento script zaregistrujeme aby byl zavolán a přijal zprávu od našeho bota. To provedeme příkazem:

https://api.telegram.org:443/bot[token]/setwebhook?url=https://www.venca-x.cz/bot/pocasi.php 

Příkaz zadáme do URL v prohlížeči. Měli bychom obdržet hlášku s úspěšným nastavením webhooku:

 {"ok":true,"result":true,"description":"Webhook was set"}

Oživení bota

Bota máme vytvořeného, zaregistrovaný webhook, který nám přeposílá zprávy poslané botovi. Pojďme bota oživit aby zprávy zpracovával a reagoval na ně.

Pro práci s bot API se mi zalíbila knihovna telegram-bot/api nainstalujeme ji:

composer require telegram-bot/api

Super, konečně jsme se dostali na příjem zpráv a reagování na ně. Na zprávu /ping odpoví bot zprávou pong:

<?php

require_once "vendor/autoload.php";

try {
$bot = new \TelegramBot\Api\Client('YOUR_BOT_API_TOKEN');
$bot->command('ping', function ($message) use ($bot) {
$bot->sendMessage($message->getChat()->getId(), 'pong!');
}); $bot->run();
} catch (\TelegramBot\Api\Exception $e) {
$e->getMessage();
}

Nezapomeňte místo YOUR_BOT_API_TOKEN nastavit váš token z vytváření bota.

Klávesnice pro ovládání bota

Bota můžeme ovládát i přes speciální klávesnici pro rychlé volby. Po zadání příkazu /start přivítáme návštěvníka a zobrazíme mu klávesnici s rychlou volbou:

$bot->command('start', function ($message) use ($bot) {

$keyboard = new \TelegramBot\Api\Types\ReplyKeyboardMarkup(array(array("/pocasi", "/ping")), true);

$bot->sendMessage($message->getChat()->getId(), 'Vítejte počasí', null, false, null, $keyboard);

});

Nette ajax – Naja

Načítání obsahu webových stránek bez nutnosti obnovení stránky prostřednictvím AJAXového volání je dnes běžnou praxí.

Zavést toto funkčnost web postaveném na Nette frameworku, je velice jednoduché a rychlé. Ukážeme si jak na to s ajaxovou knihovnou s názvem Naja. Pokud děláte úplně nový projekt, doporučuji Nette 3.0, zapomeňte na nette.ajax.js ale rovnou použijte Naju. Nette,ajax.js je již “hotový” a dál se nevyvijí. Naja je jeho nástupce.

Instalace a zprovoznění Naja knihovny

npm install naja –save-dev

Pro sloučení všech js souborů a jejich minifikaci používám Gulp. V Gulp tasku přidáme Naja.js do našeho jednoho velkého minifikovaného main.min.js souboru:

var uglify = require('gulp-uglify-es').default;
 //…..
 gulp.task('jsMain', done => {
     gulp
         .src(['node_modules/jquery/dist/jquery.min.js',
             'node_modules/popper.js/dist/umd/popper.min.js',
             'node_modules/bootstrap/dist/js/bootstrap.min.js',
             'node_modules/tether/dist/js/tether.min.js',
             'node_modules/naja/dist/Naja.js',
             'venor/nette/forms/src/assets/netteForms.min.js',
             'src/js/main.js'], {allowEmpty: true})
         .pipe(uglify({
             mangle: false,//disable rename variables and functions
         }))
         .pipe(concat('main.min.js'))
         .pipe(gulp.dest('www/js'));
 done();
 });

TIP: je potřeba použít knihovnu gulp-uglify-es ( gulp-uglify nestačí)

npm gulp-uglify-es --save-dev

Pak jen jednoduše tento 1 minifikovaný JS vložíme do šablony:

<script src="{$basePath}/js/main.min.js"></script>

Do souboru /src/js/main.js (který se také přidává do main.min.js přidáme tento kód pro inicializaci Naji):

document.addEventListener(
     'DOMContentLoaded',
     () => {
         naja.initialize();
     }
);

A máme hotovo. Nyní jen přidáme class=”ajax” prvků, které chceme mít ajaxově, stejně jako u Nette.ajax.js

Demo signálu

Po kliknutí na tlačítko aktualizujeme ajaxem snippet, ve kterém se překreslí aktuální datum. Jednoduché 🙂

Presenter:

<?php
 declare(strict_types=1);
 namespace App\FrontModule\Presenters;
 use App;
 use Nette\Utils\DateTime;
 class HomepagePresenter extends BasePresenter
 {
    private $dateTime;
 public function handleActualiseDateTime()
    {
       $this->dateTime = new DateTime();
       if ($this->isAjax()) {
          $this->redrawControl('dateTimeSnippet');
       }
    }
 public function renderDefault(): void
    {
       if ($this->dateTime === null) {
          $this->dateTime = new DateTime();
       }
       $this->template->dateTime = $this->dateTime;
    }
 }

Šablona:

<div class="col-12">
   <hr/>
   {snippet dateTimeSnippet}
      {$dateTime|date:'j. n. Y H:i:s'}
   {/snippet}
   <a n:href="actualiseDateTime!" class="ajax">AKTUALIZOVAT</a>
</div>

Android Espresso – první jednoduchý test

Testujete? Já se o to pokouším. Pro komplexnější testování UI prvků aplikace doporučuji nástroj Espresso.

Pokud vytvoříte v Android Studiu nový projekt, máte základ připraven. Android Studio vytvoří základní strukturu a základní testy automaticky. Jedná se o složky:

  • androidTest – do této složky dáváme UI testy. Používáme: Espresso a JUnit
  • test – do teto složky dáváme Unit testy, pro tyto testy používáme: Mockito, Robolectric a JUnit

Naše Espresso testy tedy budeme umísťovat do složky androidTest. Pro první test stačí do souboru build.gradle (ve složce app) přidat závislost:

androidTestImplementation 'com.android.support.test:rules:1.0.2'

Můj build.gradle vypadá takto:

apply plugin: 'com.android.application'

android {
compileSdkVersion 28
defaultConfig {
applicationId "cz.vencax.hellobuild"
minSdkVersion 15
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
}

Pojďme vytvořit náš první Espresso test. Otestujeme, zda TextView obsahuje text Hello World!

package cz.vencax.hellobuild;

import android.support.test.filters.LargeTest;
import android.support.test.rule.ActivityTestRule;
import android.support.test.runner.AndroidJUnit4;

import org.junit.After;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;

import static android.support.test.espresso.Espresso.onView;
import static android.support.test.espresso.assertion.ViewAssertions.matches;
import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
import static android.support.test.espresso.matcher.ViewMatchers.withId;
import static android.support.test.espresso.matcher.ViewMatchers.withText;
import static org.hamcrest.Matchers.containsString;

@RunWith(AndroidJUnit4.class)
@LargeTest
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class FirstTest {

@Rule
public ActivityTestRule<MainActivity> mActivityRule = new ActivityTestRule<>(MainActivity.class);

@Before
public void setUp() throws Exception {
//Before Test case execution
}

@Test
public void test1ChatId() {
//onView(withId(R.id.helloTextView)).check(matches(isDisplayed()));
onView(withId(R.id.helloTextView)).check(matches(withText(containsString("Hello World!"))));
}

@After
public void tearDown() throws Exception {
//After Test case Execution
}
}

Test spustíme kliknutím pravým tlačítkem na soubor s testem v levé části Android Studia – Project. Nebo z příkazové řádky:

gradlew connectedAndroidTest

Azure DevOps – private agent pro Android build

Azure DevOps je sklělý nástroj pro vývojáře. Nabízí ZDARMA soukromé git repozitáře, nástroj pro tasky (řízení práce), a build nástroje. To vše “zdarma”. Kompletně jsem na tuto platformu přešel a jsem maximálně spokojený. Android aplikace automaticky po pushi do Gitu otestuji, provedu build a vydám novou vývojovou verzi. Build agent má však omezení na běh max. 10h/měsíc, na malé projekty bohatě dostačuje. Pokud jste náročnější a tento limit vám nestačí, jednoduše nainstalujete na svůj server svého soukromého agenta, který provádí build. Pak jste omezeni pouze výkonem vašeho serveru. Pojďme si zprovoznit soukromého agenta pro build Android aplikace:

Stáhneme a nainstalujete Java SDK

Stáhneme a nainstalujeme Android Studio

Přidání proměnných do Path

Pro build je nutné přidat do path následující proměnné: java, JAVA_HOME, JDK, AndroidSDK
Jak zjistím kde mám nainstalován Android SDK abych ho přidal do Path?
Android Studio -> Menu -> Tools -> SDK Manager -> položka Android SDK Location.
Do Uživatelsky definovaných proměnných v mém případě přidáme Název proměnné: ANDROID_HOME a Hodnota proměnné: C:\Users\Test\AppData\Local\Android\Sdk

do Path stejným způsobem přidáme ještě tyto hodnoty:
java : C:\Program Files\Java\jdk-11.0.2
JAVA_HOME : C:\Program Files\Java\jdk-11.0.2
JDK : C:\Program Files\Java\jdk-11.0.2

Po přidání mi build ještě nefungoval, bylo nutné provést restart zařízení.

Pokus o ruční build

Pro build by v cmd mělo fungovat (spustíme v root android projektu):
gradlew assembleDebug  nebo gradlew assembleRelease

Jak nainstalovat agenta

Nejprve musíme vytvořit token pro autentizaci agenta. Přihlásíme se do TFS a vpravo nahoře klikneme na uživatele -> Security ->Personal access tokens -> New Token. Vytvoříme nový token, kterému nastavíme expiraci (jde na max 1 rok?) a oprávnění. Zobrazí se nám token, který si uschovejte, budeme ho potřebovat pro agenta.

Na URL https://dev.azure.com/{your_organization}/_admin/_AgentPool (v mém případě je {your_organization} venca-x). Zde vidíme seznam agentů a jejich stav. Pro vytvoření nového agenta klikneme na tlačítko Download agent. Vybereme cílovou platformu a nainstalujeme podle postupu. Po nainstalování není nutné spouštět agenta jako administrátor. Instalace probíhá přes Windows PowerShell.

Jak zobrazím mé agenty a jejich dostupnost?

Jděte na adresu:
https://dev.azure.com/{your_organization}/_admin/_AgentPool kde v sekci Default vidíte vaše soukromé agenty a jejich stav. Po kliknutí na Capabilities vidíte všechny vlastnosti daného agenta. Zde je důležité aby byly vlastnosti:
java, JAVA_HOME, JDK, AndroidSDK

Composer – instalujeme fork balíčku

Dnes si ukážeme jak nainstalovat fork balíčku. Proč instalovat fork? Důvodů může být mnoho: nekompatibilita s PHP,
nekompatibilita závislostí balíčku, … Já konkrétně popíši řešení s knihovnou h4kuna/fio, která je závislá na “nette/utils”: “^2.2”, ovšem já používám vývojový “nette/utils”: “^3.0”, proto není možné balíček vůbec nainstalovat.

Na Githubu tedy uděláme fork h4kuna/fio, čímž se knihovna “zkopíruje” pod náš Github účet a můžeme do ni přispívat kódem(bez schválení původního autora). Uděláme tedy commit, který upraví h4kuna/fio aby byl použitelný: https://github.com/venca-x/fio/commit/627218b174f75f452b25c02b3699641865efaa02

Já konkrétně jsem upravil soubor composer.json (ve forknutém balíčku – v master větvi):
“nette/utils”: “^2.2” na “nette/utils”: “^3.0”
a jelikož používám vývojovou verzi Nette 3.0 přidal na konec:
“minimum-stability”: “dev”

Tím máme hotovo a můžeme tento forknutý balíček nainstalovat. Ale jak?
Do souboru composer.json v našem projektu přidáme závislost na balíček, který nám předtím předtím nešel nainstalovat (ve vývojové verzi, naši úpravu forknutého balíčku jsme pushli do master větve):
“h4kuna/fio”: “dev-master”
ale pro tento balíček dáme náš zdroj – forknutý repozitář:

"repositories": [
{
"type": "git",
"url": "https://github.com/venca-x/fio.git"
}
],

Tím máme hotovo. Snad někomu pomůže stejně dobře jako mne 🙂


Gson: parsrování lokálního json souboru na objekty

Dnes si ukážeme jednoduchý příklad jak parsrovat lokální .json na objetky. Na tuto práci je perfektní knihovna gson, přidáme ji mezi závislosti:

compile 'com.google.code.gson:gson:2.8.5'

Náš ukázkový file.json vypadá následovně:

{
  "list": [
    {
      "name": "Faraz Khonsari",
      "age": 24
    },
    {
      "name": "John Snow",
      "age": 28
    },
    {
      "name": "Alex Kindman",
      "age": 29
    }
  ]
}

Jedná se o list objektů. Jednoduché. Uděláme si objekt, který namapuje list jako ArrayList, ve kterém budou objekty s propertou name a age:

public class MyModel {

   @SerializedName("list")
   public ArrayList<MyObject> list;

   static public class MyObject {

      @SerializedName("name")
      public String name;

      @SerializedName("age")
       public int age;
   }
}

Načteme soubor:

public String inputStreamToString(InputStream inputStream) {
   try {
      byte[] bytes = new byte[inputStream.available()];
      inputStream.read(bytes, 0, bytes.length);
      String json = new String(bytes);
      return json;
   } catch (IOException e) {
      return null;
   }
}

Přečteme obsah souboru:

String myJson=inputStreamToString(mActivity.getResources().openRawResource(R.raw.file));

Obsah souboru konvertujeme na objekt:

MyModel myModel = new Gson().fromJson(myJson, MyModel.class);

 

Android: Cleartext HTTP traffic to downloads.bbc.co.uk not permitted

Od Android 8 (Oreo) je defaultně zakázáno komunikovat po http – je nutné komunikovat po https. Pokud stahujete data ze zdroje, kde nemůžete ovlivnit nasazení https, lze toto pravidlo obejít přidáním níže uvedeného kódu do vaší aplikace.

Vytvoříme soubor: res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

Do AndroidManifest.xm přidáme řádek: http://android:networkSecurityConfig=”@xml/network_security_config”

<application
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:networkSecurityConfig="@xml/network_security_config"
...

 

Retrofit: synchronní a asynchronní požadavky

Minule jsme si ukázali, jak použít Retrofit v Android studiu. Dnes se podíváme na synchronní a asynchronní požadavky.

Synchronní požadavek

Při synchronním požadavku, je na server odeslán požadavek, aplikace se “zastaví” a čeká, dokud neobdrží odpověď od serveru.

Asynchronní požadavek

Při asynchronním požadavku se používají tzv. callbacky. Na server se odešle požadavek (stejně jako při synchronním požadavku) ale nečeká se na odpověď a kód pokračuje dál. Na odpověď ze serveru čeká callback, který provede požadovanou operaci s odpovědí od serveru. Tento callback se může zavolat kdykoliv (odpověď od serveru trvá různě dlouho).

V kódu je vidět asynchronní volání, které je shodné jako v prvním seznámení s Retrofit, přibylo synchronní volání:

package cz.vencax.retrofit;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import java.io.IOException;

import cz.vencax.retrofit.model.Book;
import cz.vencax.retrofit.model.Books;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final Button button = findViewById(R.id.buttonAsynchronous);
        button.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                getBooksFromApiAsynchronous();
            }
        });

        final Button buttonPost = findViewById(R.id.buttonSynchronous);
        buttonPost.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                try {
                    getBooksFromApiSynchronous();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Asynchronous Retrofit request
     */
    private void getBooksFromApiAsynchronous() {

        Log.d("xxx", "getBooksFromApiAsynchronous");

        BookService bookService = RetrofitClient.getClient().create(BookService.class);
        Call<Books> books = bookService.getAllBooks();
        books.enqueue(new Callback<Books>() {

            @Override
            public void onResponse(Call<Books> call, Response<Books> response) {
                Log.d("xxx", "Asynchronous onResponse: " + response);
                if (response.isSuccessful()) {
                    Log.d("xxx", "Asynchronous response.isSuccessful()" + response.body());
                    Books books = response.body();

                    Log.d("xxx", "Asynchronous response.isSuccessful(), books count: " + books.getBooks().size());
                    for (Book book : books.getBooks()) {
                        Log.d("xxx", "Asynchronous Book: " + book.getTitle() + ", " + book.getAuthor());
                    }
                } else {
                    Log.d("xxx", "Asynchronous response NOT isSuccessful()" + response.errorBody().source());
                }
            }

            @Override
            public void onFailure(Call<Books> call, Throwable t) {
                Log.d("xxx", "Asynchronous onFailure: " + t);
            }
        });
    }

    /**
     * Synchronous Retrofit request
     */
    private void getBooksFromApiSynchronous() throws IOException {

        Log.d("xxx", "getBooksFromApiSynchronous");

        BookService bookService = RetrofitClient.getClient().create(BookService.class);
        Call<Books> call = bookService.getAllBooks();
        Books books = call.execute().body();

        Log.d("xxx", "Synchronous response.isSuccessful(), books count: " + books.getBooks().size());
        for (Book book : books.getBooks()) {
            Log.d("xxx", "Synchronous Book: " + book.getTitle() + ", " + book.getAuthor());
        }
    }
}