DEV Community

NOGIER Loïc
NOGIER Loïc

Posted on • Updated on

RUST - API for Influencers

Table Of Contents

English

Introduction

I had to make a Browser Extension that notifies the user when a new video is posted and when the Twitch streamer launch a stream.

For this extension I needed to set up an API to centralize my requests to the APIs of Google and Twitch.

I decided to innovate a bit, and to make this API in Rust rather than with NodeJS or PHP. With that, I decided to go further and separate my Twitch and Youtube requests in two different CLI applications.

My main application communicates with my two sub-applications with std::process::Command.

Twitch && Youtube CLI application

My goal was to make my queries to the Twitch API and the Youtube API with its two applications, which, once the queries were made, returned the results.
Results that I retrieve in my main application.

Here is an example to retrieve the last posted Youtube video:

async fn get_last_video(&self, channel_id: String) -> Result<serde_json::Value, reqwest::Error> {
    let base_url: String = "https://www.googleapis.com/youtube/v3/search".to_owned();
    let client = reqwest::Client::new();
    let resp = client.get(format!("{}?part=snippet&channelId={}&maxResults=1&order=date&type=video&key={}", &base_url, &channel_id, &self.youtube_api_key).as_str()).send().await?;
    let text = resp.text().await?;
    let video: serde_json::Value = serde_json::from_str(&text).unwrap();
    Ok(video)
}
Enter fullscreen mode Exit fullscreen mode

In this example, I use serde_json and reqwest.

Main application

To set up my API I used actix-web where I could define my routes like this:

use crate::api_error::ApiError;

use actix_web::{get, web, HttpResponse};
use std::fs;

#[get("/")]
async fn get_home() -> Result<HttpResponse, ApiError> {
    let error = ApiError::new(404, "Error".to_owned());
    Ok(HttpResponse::Ok().json(error))
}
#[get("/twitch/stream")]
async fn get_twitch() -> Result<HttpResponse, ApiError> {
    let file_data = read_file("paranoi4k_twitch.json".to_owned()).await;
    let serialized: serde_json::Value = serde_json::from_str(&file_data.as_str()).unwrap();

    Ok(HttpResponse::Ok().json(serialized))
}
pub fn init_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(get_home);
    cfg.service(get_twitch);
}
Enter fullscreen mode Exit fullscreen mode

We can see that for the /twitch/stream route, I retrieve data in a paranoi4k_twitch.json file at the root of my application.
Indeed, when I use my Twitch CLI, I save the data it returns to me in a json file, so that I can retrieve it in my route whenever I want.

This is how I save my json file:

    pub fn check_stream(&self) -> std::io::Result<()> {
        let path = self.make_path();
        let app_command = format!("isonlive-user={}", &self.uid); 

        let output = Command::new(path)
            .arg(app_command)
            .output()
            .unwrap();
        let encoded = String::from_utf8_lossy(output.stdout.as_slice());
        let serialized: serde_json::Value = serde_json::from_str(&encoded).unwrap();

        let mut buffer = File::create(&self.filename).unwrap();
        buffer.write_fmt(format_args!("{}", serialized)).unwrap();

        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

When I retrieve data from my CLI application, it is displayed as bytes, so I have to use the from_utf8_lossy method to convert to a string.

And Voilà, that's basically all I set up for this Api, it was more of a personal challenge in the end.

Extension

For my web extension written in JavaScript, I have a function like this to call my API :

    /**
     * @param {string} socialname
     * @return {output} { status: false, data: false }
     * @exemple let request = await rq("twitch");
     */
    async rq(socialname) {
        let output = { status: false, data: false };
        let url = socialname === "twitch" ? this.twitch_url : this.youtube_url;
        try {
            let response = await fetch(url, {
                headers: {
                    "Content-Type": "application/json"
                },
                method: "GET"
            });
            if (response.ok) {
                let responseTarget = await response.json();
                output.status = true;

                if (socialname === "youtube") {
                    output.data = responseTarget.items[0];
                } else if (socialname === "twitch") {
                    output.data = responseTarget.data;
                }
            } else {
                output.status = false;
                output.data = "Problème de serveur, vous pouvez me contacter ici twitter.com/zaekof";
            }
        } catch (error) {
            output.status = false;
            output.data = error;
        }
        return output;
    }
Enter fullscreen mode Exit fullscreen mode

I think I've explained the main thing, don't hesitate to correct me and make constructive criticism :)

Useful links:
RustYoutubeCli
RustTwitchCli
InfluencerApi


French

Introduction

J'ai eu à réaliser une Extension pour navigateur qui avertit l'utilisateur quand une nouvelle vidéo est postée et quand le streamer Twitch lance un live.

Pour cette extension j'ai eu besoin de mettre en place une API pour centraliser mes requêtes vers les API de Google et de Twitch.

J'ai alors décidé d’innover un peu, et de faire cette API en Rust plutôt qu'avec du NodeJS ou du PHP. Et en plus de ça, j'ai décidé d'aller plus loin et de séparer mes requêtes Twitch et mes requêtes Youtube dans deux sou applications CLI différentes.
Mon application principal communique avec elles avec std::process::Command.

Twitch && Youtube CLI application

Mon but était de faire mes requêtes vers l'API de Twitch et l'API de Youtube via ses deux applications, qui une fois les requêtes effectuées me retourne les résultats.
Résultats que je récupère dans mon application principale.

Voici un exemple pour récupérer la dernière vidéo Youtube posté :

async fn get_last_video(&self, channel_id: String) -> Result<serde_json::Value, reqwest::Error> {
    let base_url: String = "https://www.googleapis.com/youtube/v3/search".to_owned();
    let client = reqwest::Client::new();
    let resp = client.get(format!("{}?part=snippet&channelId={}&maxResults=1&order=date&type=video&key={}", &base_url, &channel_id, &self.youtube_api_key).as_str()).send().await?;
    let text = resp.text().await?;
    let video: serde_json::Value = serde_json::from_str(&text).unwrap();
    Ok(video)
}
Enter fullscreen mode Exit fullscreen mode

Dans cette exemple, j'utilise serde_json et reqwest.

Application principal

Pour la mise en place de mon API j'ai donc utilisé actix-web ou j'ai pu définir mes routes comme ceci :

use crate::api_error::ApiError;

use actix_web::{get, web, HttpResponse};
use std::fs;

#[get("/")]
async fn get_home() -> Result<HttpResponse, ApiError> {
    let error = ApiError::new(404, "Error".to_owned());
    Ok(HttpResponse::Ok().json(error))
}
#[get("/twitch/stream")]
async fn get_twitch() -> Result<HttpResponse, ApiError> {
    let file_data = read_file("paranoi4k_twitch.json".to_owned()).await;
    let serialized: serde_json::Value = serde_json::from_str(&file_data.as_str()).unwrap();

    Ok(HttpResponse::Ok().json(serialized))
}
pub fn init_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(get_home);
    cfg.service(get_twitch);
}
Enter fullscreen mode Exit fullscreen mode

On peut voir que pour la route /twitch/stream, je récupère des données dans un fichier paranoi4k_twitch.json à la racine de mon application.
En effet, quand je fais appel à mon Twitch CLI, j'enregistre les données qu'il me retourne dans un fichier json, de manière à pouvoir les récupérer dans ma route quand je veux.

Voici comment j'enregistre mon fichier json :

    pub fn check_stream(&self) -> std::io::Result<()> {
        let path = self.make_path();
        let app_command = format!("isonlive-user={}", &self.uid); 

        let output = Command::new(path)
            .arg(app_command)
            .output()
            .unwrap();
        let encoded = String::from_utf8_lossy(output.stdout.as_slice());
        let serialized: serde_json::Value = serde_json::from_str(&encoded).unwrap();

        let mut buffer = File::create(&self.filename).unwrap();
        buffer.write_fmt(format_args!("{}", serialized)).unwrap();

        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

Quand je récupère les données de mon application CLI, celles-ci sont affichées sous forme de bytes, je dois donc utiliser la méthode from_utf8_lossy pour convertir en String.

Voilà en gros un peu tout ce que j'ai mis en place pour cette Api, c'était plus un défi personnel au final.

Extension

Dans le cadre de mon extension web écrite en JavaScript, j'ai une fonction comme ceci pour faire appel à mon API :

    /**
     * @param {string} socialname
     * @return {output} { status: false, data: false }
     * @exemple let request = await rq("twitch");
     */
    async rq(socialname) {
        let output = { status: false, data: false };
        let url = socialname === "twitch" ? this.twitch_url : this.youtube_url;
        try {
            let response = await fetch(url, {
                headers: {
                    "Content-Type": "application/json"
                },
                method: "GET"
            });
            if (response.ok) {
                let responseTarget = await response.json();
                output.status = true;

                if (socialname === "youtube") {
                    output.data = responseTarget.items[0];
                } else if (socialname === "twitch") {
                    output.data = responseTarget.data;
                }
            } else {
                output.status = false;
                output.data = "Problème de serveur, vous pouvez me contacter ici twitter.com/zaekof";
            }
        } catch (error) {
            output.status = false;
            output.data = error;
        }
        return output;
    }
Enter fullscreen mode Exit fullscreen mode

Je pense vous avoir expliqué le principal, n'hésitez pas à me corriger et à faire des critiques constructives :)

Liens utiles :
RustYoutubeCli
RustTwitchCli
InfluencerApi

Go to the TOP

Discussion (2)

Collapse
tofraley profile image
Taylor Fraley

Very cool! If you have not already, you should check out StructOpt for parsing your command line arguments.

Collapse
zaekof profile image
NOGIER Loïc Author

Oh, Thanks. I really appreciate.