From 24185f677e1fc84d4522c8e7d87ff68f1bca110c Mon Sep 17 00:00:00 2001 From: Aleksandr Trushkin Date: Sun, 26 Feb 2023 17:59:35 +0300 Subject: [PATCH] initial commit --- .cargo/config.toml | 2 + .gitmodules | 3 + Cargo.toml | 20 ++++ alterego/.DS_Store | Bin 0 -> 6148 bytes alterego/.drone.jsonnet | 93 +++++++++++++++++ alterego/.drone.yml | 98 ++++++++++++++++++ alterego/Cargo.toml | 16 +++ alterego/build.rs | 41 ++++++++ alterego/makefile | 40 +++++++ alterego/src/lib.rs | 1 + alterego/src/main.rs | 70 +++++++++++++ alterego/src/telegram/bot.rs | 184 +++++++++++++++++++++++++++++++++ alterego/src/telegram/mod.rs | 2 + alterego/src/telegram/types.rs | 63 +++++++++++ rustree | 1 + 15 files changed, 634 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitmodules create mode 100644 Cargo.toml create mode 100644 alterego/.DS_Store create mode 100644 alterego/.drone.jsonnet create mode 100644 alterego/.drone.yml create mode 100644 alterego/Cargo.toml create mode 100644 alterego/build.rs create mode 100644 alterego/makefile create mode 100644 alterego/src/lib.rs create mode 100644 alterego/src/main.rs create mode 100644 alterego/src/telegram/bot.rs create mode 100644 alterego/src/telegram/mod.rs create mode 100644 alterego/src/telegram/types.rs create mode 160000 rustree diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..99a616d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu] +rustflags = ["-C", "linker=clang", "-C", "link-arg=-fuse-ld=lld"] diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..72e71a9 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "rustree"] + path = rustree + url = git@git.loyso.art:frx/rustree.git diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..535ca96 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[workspace] +members = [ + "alterego", + "rustree", +] + +[workspace.dependencies] +anyhow = "1.0.69" +futures = "0.3.26" +log = "0.4.17" +reqwest = "0.11.14" +serde = { version = "1.0.152", features = ["derive"]} +serde_json = "1.0.93" +tokio = { version = "1.25.0", features = ["full"]} +chrono = "0.4.23" +hex = "0.4.3" +hmac = "0.12.1" +percent-encoding = "2.2.0" +sha2 = "0.10.6" +urlencoding = "2.1.2" diff --git a/alterego/.DS_Store b/alterego/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..e37c505f05ca9a0bed32985f1295c565c55b653f GIT binary patch literal 6148 zcmeHK%}N6?5T0z8vSJSfFCOy*Lf>F5>p{E-#d~clRq3*&*0cK(zJNY}R}sAU7@`N? z#BY+pCa(3UA~P`gvh$PKd|Q$Z5t;twq({^xq72HIY+(38*w5OKL@XVk;WcK{-e6pn z^G2i^{wf3f?NZvIDV@E;0q(d>TI@Y%?oJ{Uf zM`b`6h#6@4$1dOhd+YoEI7y$B0cGG{F<{bSQ1r1Sxmz2XEhfo%dt37_CVB)S~ f#PU_V4Rr#$"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = {workspace = true} +futures = {workspace = true} +log = {workspace = true} +reqwest = {workspace = true} +serde = {workspace = true, features = ["derive"]} +serde_json = {workspace = true} +tokio = {workspace = true} diff --git a/alterego/build.rs b/alterego/build.rs new file mode 100644 index 0000000..9b79ad7 --- /dev/null +++ b/alterego/build.rs @@ -0,0 +1,41 @@ +use std::env; + +fn main() { + let rev = get_value_from_env("GIT_VERSION") + .or_else(|| get_value_from_command("git", &["rev-parse", "--short", "HEAD"])) + .unwrap_or_else(|| "unknown".to_owned()); + + let branch = get_value_from_env("GIT_BRANCH") + .or_else(|| get_value_from_command("git", &["rev-parse", "--abbrev-ref", "HEAD"])) + .unwrap_or_else(|| "unknown".to_owned()); + + println!("cargo:rustc-env=GIT_REVISION={}", rev); + println!("cargo:rustc-env=GIT_BRANCH={}", branch); + println!("cargo:rerun-if-env-changed=GIT_REVISION"); +} + +fn get_value_from_env(key: &str) -> Option { + env::var(key).map_or_else(|_| None, Some) +} + +fn get_value_from_command, S: AsRef>( + cmd: &str, + args: I, +) -> Option { + std::process::Command::new(cmd) + .args(args) + .output() + .map_or_else( + |_| None, + |out| { + if !out.status.success() { + return None; + } + + match std::str::from_utf8(&out.stdout) { + Ok(value) => Some(value.to_owned()), + Err(_) => None, + } + }, + ) +} diff --git a/alterego/makefile b/alterego/makefile new file mode 100644 index 0000000..f9fa345 --- /dev/null +++ b/alterego/makefile @@ -0,0 +1,40 @@ +export DOCKER_BUILDKIT=1 + +DOCKERFLAGS:=-it --rm \ + -v "${PWD}":"/app" \ + --workdir "/app" \ + -e "PWD=/app" + +DOCKERIMG:="rust-build-env:V1" + +APP_NAME:=altherego +IMAGE:=rust:1.49 +TARGET_ARCH:=armv7-unknown-linux-gnueabihf + +image: + docker build -t rust-build-env:V1 . +.PHONY: image + +ARM_PREFIX:=CARGO_TARGET_ARM_UNKNOWN_LINUX_MUSLEABIHF_LINKER=arm-linux-gnueabihf-ld \ + REALGCC=arm-linux-gnueabihf-gcc-8 \ + TARGET_CC=musl-gcc + +build_debug_arm: + ${ARM_PREFIX} cargo build --target=armv7-unknown-linux-musleabihf +.PHONY: build_debug_arm + +build_release_arm: + docker run ${DOCKERFLAGS} ${DOCKERIMG} /bin/sh -c 'cargo build --release --target=armv7-unknown-linux-gnueabihf' +.PHONY: build_release_arm + +docker_build_release_arm: + docker run ${DOCKERFLAGS} ${DOCKERIMG} make build_release_arm + +dronefile: + drone jsonnet \ + --format \ + -V app_name=${APP_NAME} \ + -V image=${IMAGE} \ + -V target_arch=${TARGET_ARCH} + drone sign frx/altherego --save +.PHONY: dronefile diff --git a/alterego/src/lib.rs b/alterego/src/lib.rs new file mode 100644 index 0000000..e894b90 --- /dev/null +++ b/alterego/src/lib.rs @@ -0,0 +1 @@ +pub mod telegram; diff --git a/alterego/src/main.rs b/alterego/src/main.rs new file mode 100644 index 0000000..bd8abe8 --- /dev/null +++ b/alterego/src/main.rs @@ -0,0 +1,70 @@ +use alterego::telegram; + +use tokio::runtime; +const BOT_TOKEN: &str = "170515067:AAElDJ8Sq_oIqo9WaL4DKvUr13nSEIdHCYs"; + +fn main() -> anyhow::Result<()> { + println!("Hello, world!"); + + let rt = runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + .expect("making runtime"); + + rt.block_on(app())?; + + Ok(()) +} + +use futures::StreamExt; + +async fn app() -> anyhow::Result<()> { + let bot = telegram::bot::Client::new(BOT_TOKEN.to_owned()); + + let mut stream = bot.updates_stream(); + while let Some(update) = stream.next().await { + let update = update?; + + println!("{:?}", update); + + handle_update(update); + } + + Ok(()) +} + +use log::{debug, trace, warn}; +use telegram::types::UpdateKind; + +fn handle_update(update: telegram::types::Update) { + match update.kind { + UpdateKind::Message(msg) => { + let text = msg.text.unwrap_or_default(); + + if !text.starts_with('/') { + trace!("it's not a command, skipping"); + return; + } + + match text.as_str() { + "/help" => { + println!("help command called"); + } + "/temp" => { + println!("temp command called"); + } + "/versionrequest" => {} + other => { + println!("unknown command {}", other); + } + } + } + UpdateKind::EditedMessage(msg) => { + debug!("edited message: {:?}", msg); + } + UpdateKind::Undefined => { + warn!("message udentified"); + } + } +} diff --git a/alterego/src/telegram/bot.rs b/alterego/src/telegram/bot.rs new file mode 100644 index 0000000..0550bf0 --- /dev/null +++ b/alterego/src/telegram/bot.rs @@ -0,0 +1,184 @@ +use super::types::*; + +use std::{ + cmp::max, + collections::VecDeque, + future::Future, + pin::Pin, + sync::atomic::{AtomicI32, Ordering}, + task::{Context, Poll}, +}; + +use log::{debug, trace}; +use reqwest; + +const TELEGRAM_URL: &str = "https://api.telegram.org"; +const HTTP_CLIENT: &str = "alterego-http-client/1.0"; + +#[derive(Debug)] +pub enum BotError { + API(String), +} + +impl std::fmt::Display for BotError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let text = match self { + BotError::API(msg) => format!("api error: {}", msg), + }; + + write!(f, "{}", text) + } +} + +impl std::error::Error for BotError {} + +#[derive(Debug, Clone)] +/// Client provides API for communicating with Telegram Bot API. +pub struct Client { + pub token: String, + pub client: reqwest::Client, +} + +impl Client { + pub fn new(token: String) -> Self { + let client = reqwest::ClientBuilder::default() + .connect_timeout(std::time::Duration::from_secs(5)) + .https_only(true) + .user_agent(HTTP_CLIENT) + .build() + .expect("building http client"); + + Self { token, client } + } + + fn make_url(&self, method: &str) -> String { + format!("{}/bot{}/{}", TELEGRAM_URL, self.token, method) + } + + fn fetch_updates( + &self, + offset: i32, + ) -> impl Future>>> { + let client = self.clone(); + async move { client.get_updates(offset).await } + } + + pub async fn get_updates(&self, offset: i32) -> anyhow::Result>> { + const METHOD: &str = "getUpdates"; + + trace!("getting updates"); + + let url = { + let mut url = reqwest::Url::parse(&self.make_url(METHOD))?; + url.set_query(Some(format!("offset={}", offset).as_str())); + url + }; + + debug!("requesting: {}", url.as_str()); + let body = self.client.get(url).send().await?.bytes().await?; + + let response: Response = serde_json::from_reader(&body[..])?; + if response.is_err() { + debug!("response finished with error"); + return Err(anyhow::anyhow!(BotError::API( + response.description.unwrap() + ))); + }; + + Ok(response.result) + } + + pub fn updates_stream(&self) -> ClientUpdateStream { + ClientUpdateStream::new(self) + } + + pub async fn reply(&self) -> anyhow::Result<()> { + todo!("soon") + } +} + +type PinnedRequest = Pin>>> + Send>>; + +pub struct ClientUpdateStream { + client: Client, + next_id: AtomicI32, + buffer: VecDeque, + pinned_request: Option, +} + +impl ClientUpdateStream { + pub fn new(client: &Client) -> Self { + Self { + client: client.clone(), + next_id: AtomicI32::new(0), + buffer: VecDeque::new(), + pinned_request: None, + } + } +} + +impl futures::Stream for ClientUpdateStream { + type Item = anyhow::Result; + + /// poll_next fetches updates from client and appends it to + /// buffer. + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let myself = self.get_mut(); + + let mut current_id = myself.next_id.load(Ordering::SeqCst); + + if let Some(value) = myself.buffer.pop_front() { + return Poll::Ready(Some(Ok(value))); + }; + + // Check the current state of myself. + // In case request is being processed, return Pending; + // In case request is ready, insert updates and return Ok(true); + // In case there is no request or updates are empty, return Ok(false); + // Ok(state), where states defines whether new request should be + // created or not. + let result = match myself.pinned_request { + None => Ok(false), + Some(ref mut request) => { + let request = request.as_mut(); + let polled_request = request.poll(cx); + + match polled_request { + Poll::Pending => return Poll::Pending, + Poll::Ready(Ok(None)) => Ok(false), + Poll::Ready(Ok(Some(ref updates))) if updates.is_empty() => Ok(false), + Poll::Ready(Ok(Some(updates))) => { + for update in updates { + current_id = max(current_id, update.id + 1); + myself.buffer.push_back(update); + } + + Ok(true) + } + Poll::Ready(Err(err)) => Err(err), + } + } + }; + + myself.next_id.store(current_id, Ordering::SeqCst); + + match result { + Ok(true) => { + myself.pinned_request = None; + Pin::new(myself).poll_next(cx) + } + Ok(false) => { + let next_request = myself.client.fetch_updates(current_id); + myself.pinned_request = Some(Box::pin(next_request)); + + Pin::new(myself).poll_next(cx) + } + Err(err) => { + let next_request = myself.client.fetch_updates(current_id); + myself.pinned_request = Some(Box::pin(next_request)); + + Poll::Ready(Some(Err(err))) + } + } + } +} diff --git a/alterego/src/telegram/mod.rs b/alterego/src/telegram/mod.rs new file mode 100644 index 0000000..33547fc --- /dev/null +++ b/alterego/src/telegram/mod.rs @@ -0,0 +1,2 @@ +pub mod bot; +pub mod types; diff --git a/alterego/src/telegram/types.rs b/alterego/src/telegram/types.rs new file mode 100644 index 0000000..77d9fb0 --- /dev/null +++ b/alterego/src/telegram/types.rs @@ -0,0 +1,63 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Response { + pub ok: bool, + pub error_code: Option, + pub description: Option, + pub result: Option>, +} + +impl Response { + pub fn is_err(&self) -> bool { + !self.ok + } +} + +#[derive(Debug, Deserialize)] +pub struct Update { + #[serde(rename = "update_id")] + pub id: i32, + + #[serde(flatten)] + pub kind: UpdateKind, +} + +#[derive(Debug, Deserialize)] +pub enum UpdateKind { + #[serde(rename = "message")] + Message(Message), + + #[serde(rename = "edited_message")] + EditedMessage(Message), + + Undefined, +} + +#[derive(Debug, Deserialize)] +pub struct Message { + pub message_id: i32, + pub from: Option, + pub sender_chat: Option, + pub date: i64, + pub chat: Chat, + pub text: Option, +} + +#[derive(Debug, Deserialize)] +pub struct User { + pub id: i32, + pub is_bot: bool, + pub first_name: String, + pub last_name: Option, + pub username: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Chat { + pub id: i32, + #[serde(alias = "type")] + pub chat_type: String, + pub title: Option, + pub username: Option, +} diff --git a/rustree b/rustree new file mode 160000 index 0000000..12e86d7 --- /dev/null +++ b/rustree @@ -0,0 +1 @@ +Subproject commit 12e86d70799633acf3e44e036fb6da19f41f3a2a