Skip to content

Commit

Permalink
Merge pull request #2 from thelezend/telegram
Browse files Browse the repository at this point in the history
Telegram module
  • Loading branch information
thelezend authored Oct 29, 2024
2 parents b140122 + 967178b commit d101f29
Show file tree
Hide file tree
Showing 19 changed files with 1,017 additions and 368 deletions.
491 changes: 341 additions & 150 deletions Cargo.lock

Large diffs are not rendered by default.

22 changes: 11 additions & 11 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,23 @@ resolver = "2"
assert_fs = "1.1.2"
bincode = "1.3.3"
borsh = "1.5.1"
config = "0.14.0"
config = "0.14.1"
console = "0.15.8"
csv = "1.3.0"
futures-util = "0.3.30"
futures-util = "0.3.31"
grammers-client = "0.7.0"
http = "1.1.0"
indicatif = "0.17.8"
num-derive = "0.4.2"
num-traits = "0.2.19"
regex = "1.11.0"
regex = "1.11.1"
reqwest = "0.12.8"
serde = "1.0.210"
serde_json = "1.0.128"
solana-client = "2.0.13"
solana-program = "2.0.13"
solana-sdk = "2.0.13"
thiserror = "1.0.64"
tokio = "1.40.0"
serde = "1.0.213"
serde_json = "1.0.132"
solana-client = "2.0.14"
solana-program = "2.0.14"
solana-sdk = "2.0.14"
thiserror = "1.0.65"
tokio = "1.41.0"
tokio-tungstenite = "0.24.0"
tracing = "0.1.40"
tracing-appender = "0.2.3"
Expand Down
43 changes: 32 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ A minimal CLI application designed to detect and scrape Solana token information
### Supported platforms

- [x] Discord
- [ ] Telegram
- [x] Telegram
- [ ] Twitter

## Working and Usage

The program connects to the Discord gateway using your token and monitors messages mentioning valid Solana token addresses.
The program connects to the Discord gateway using your token and the Telegram API using your credentials, monitoring messages mentioning valid Solana token addresses.

- You can use `discord_filters.csv` to customize it and filter scans to specific channels and user messages.
- You can use `filters.csv` to customize it and filter scans to specific channels, groups, and user messages.
- This is not a sniping/trading bot but is designed to work alongside automation/sniping bots like [Peppermints](https://www.tensor.trade/trade/peppermints) or your custom programs with similar functionality.
- Once a token is detected, it will send a GET request to the URL specified in the `TOKEN_ENDPOINT_URL` field.
- The detected token addresses are saved in a local text file `detected_tokens.txt` to avoid duplicate purchases.
- Logs are saved in `logs` directory for debugging purposes.

> **IMPORTANT: Using user accounts for automation is against Discord's TOS, so use them at your own risk, preferably with accounts you can afford to lose.**
> **IMPORTANT: Using user accounts for automation is against Discord's TOS, so use them at your own risk, preferably with accounts you can afford to lose. Telegram is more lenient.**
## Installation

Expand All @@ -39,6 +40,10 @@ Need to have a `settings.json` file in the working directory with the following:
"user_token": "YOUR_DISCORD_USER_TOKEN",
"sec_ws_key": "YOUR_DISCORD_SECRET_WS_KEY"
},
"telegram": {
"api_id": "YOUR_TELEGRAM_API_ID",
"api_hash": "YOUR_TELEGRAM_API_HASH"
},
"solana": {
"rpc_url": "YOUR_RPC_URL"
}
Expand All @@ -47,21 +52,37 @@ Need to have a `settings.json` file in the working directory with the following:

- Read how to get a Discord user account token [here](https://gist.github.com/MarvNC/e601f3603df22f36ebd3102c501116c6).
- You can similarly obtain the `Sec-Websocket-Key` from the headers of the WebSocket request to the Discord gateway.
- Telegram's `api_id` and `api_hash` can be obtained from [my.telegram.org](https://my.telegram.org).
- You will be asked to enter your phone number to receive a code to authenticate your Telegram account for the first time. Your session will be saved in `scraper.session`, so this won't be needed every time.
- `rpc_url` is only used for getting token addresses from links. Won't be used to send transactions.

### Filters

Need to have a `discord_filters.csv` file in the working directory with the following:
Need to have a `filters.csv` file in the working directory with the following:

```csv
NAME,CHANNEL_ID,USER_ID,TOKEN_ENDPOINT_URL
test,12314,123234,http://localhost:9001/solana
pow-calls,132414,51451345,http://localhost:9005/solana
NAME,DISCORD_CHANNEL_ID,DISCORD_USER_ID,TELEGRAM_CHANNEL_ID,TOKEN_ENDPOINT_URL,MARKET_CAP
test,12314,123234,,http://localhost:9001/solana,
pow-calls,132414,51451345,,http://localhost:9005/solana,20000
```

- `CHANNEL_ID` is the ID of the channel you want to monitor.
- `USER_ID` is the ID of the user you want to monitor.
- `TOKEN_ENDPOINT_URL` is the URL to which a GET request will be made, with the token address as a parameter.
- `NAME`(Required): The name of the filter. This is just for your reference.
- `DISCORD_CHANNEL_ID`(Optional): The ID of the Discord channel you want to monitor.
- `DISCORD_USER_ID`(Optional): The ID of the Discord user you wish to monitor.
- `TELEGRAM_CHANNEL_ID`(Optional):The ID of the Telegram channel you wish to monitor. For private Telegram channels, which start with `-100`, ensure that the `-100` is removed from the ID. If you’re unsure how to find a Telegram channel ID, a quick online search can guide you.
- `TOKEN_ENDPOINT_URL`(Required): The URL to which a GET request will be made, with the token address as a parameter.
- `MARKET_CAP`(Optional): Represents the minimum market capitalization required for the token to be detected. The token’s price is retrieved via Jupiter’s API; however, this may not be applicable for very new tokens.

Discord fields are combined using an AND operation, whereas Discord and Telegram fields are combined using an OR operation. For example, in the filter below:

```csv
NAME,DISCORD_CHANNEL_ID,DISCORD_USER_ID,TELEGRAM_CHANNEL_ID,TOKEN_ENDPOINT_URL,MARKET_CAP
test,12314,123234,2254310975,http://localhost:9001/solana,
```

The token is detected if it is posted in the `DISCORD_CHANNEL_ID` of `12314` by the `DISCORD_USER_ID` `123234`, OR if it is posted by anyone in the `TELEGRAM_CHANNEL_ID` `2254310975`.

**Note:** Currently, filtering by USER ID is not supported for Telegram.

## Support and Contact

Expand Down
4 changes: 2 additions & 2 deletions token-scraper-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "token-scraper-core"
version = "0.1.0"
version = "0.2.0"
authors = ["Lezend"]
description = "CLI tool to scrape solana tokens."
license = "MIT OR Apache-2.0"
Expand All @@ -12,8 +12,8 @@ config = { workspace = true }
console = { workspace = true }
csv = { workspace = true }
futures-util = { workspace = true, features = ["sink"] }
grammers-client = { workspace = true }
http = { workspace = true }
indicatif = { workspace = true }
raydium-amm-interface = { workspace = true, features = ["serde"] }
regex = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
Expand Down
10 changes: 5 additions & 5 deletions token-scraper-core/src/discord/stream/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ pub async fn handle_stream(
let task = tokio::spawn(async move {
match event.unwrap() {
GatewayEvent::Hello(data) => {
tracing::info!("Received hello event: {:?}", data);
tracing::trace!("Received hello event: {:?}", data);

tracing::info!("Sending heartbeat");
tracing::trace!("Sending heartbeat");
send_heartbeat(Arc::clone(&sequence), Arc::clone(&ws_write))
.await
.expect("Failed to send heartbeat");
Expand All @@ -94,7 +94,7 @@ pub async fn handle_stream(
if let Err(e) =
send_heartbeat(Arc::clone(&sequence), Arc::clone(&ws_write)).await
{
tracing::error!("Failed to send heartbeat: {:?}", e);
tracing::debug!("Failed to send heartbeat: {:?}", e);
break;
}
}
Expand Down Expand Up @@ -169,8 +169,8 @@ async fn handle_exception(

match event_type {
"READY" => {
tracing::info!("Received ready event");
tracing::info!(
tracing::debug!("Received ready event");
tracing::debug!(
"Logged in as {}",
json_value
.get("d")
Expand Down
2 changes: 1 addition & 1 deletion token-scraper-core/src/discord/stream/identify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub async fn identify(discord_token: String, ws_write: WebsocketWrite) -> Result
))
.await?;

tracing::info!("Identified with Discord");
tracing::debug!("Identified with Discord");

Ok(())
}
Expand Down
10 changes: 0 additions & 10 deletions token-scraper-core/src/discord/stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,10 @@ mod ws_request;
use std::sync::Arc;

use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use std::time::Duration;
use tokio::sync::{mpsc::UnboundedSender, Mutex};
use tokio_tungstenite::connect_async;
use twilight_model::gateway::event::DispatchEvent;

use crate::get_spinner;

use self::handler::handle_stream;

/// Error types for the Discord stream module.
Expand Down Expand Up @@ -54,22 +50,16 @@ pub async fn start_stream(
sec_ws_key: &str,
event_tx: Arc<UnboundedSender<DispatchEvent>>,
) -> Result<(), Error> {
let spinner = get_spinner!("Connecting to discord...");

let request = ws_request::create_request_with_headers(sec_ws_key.to_string()).await?;
let (ws_stream, _) = connect_async(request).await?;
let (ws_write, ws_read) = ws_stream.split();

spinner.finish();

let ws_write = Arc::new(Mutex::new(ws_write));

let sequence: Arc<Mutex<Option<u64>>> = Arc::new(Mutex::new(None));
let resume_gateway_url: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
let session_id: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));

println!("Watching for messages...");

handle_stream(
discord_token,
ws_read,
Expand Down
2 changes: 1 addition & 1 deletion token-scraper-core/src/discord/stream/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub async fn send_heartbeat(
.await
.map_err(HeartbeatError::Ws)?;

tracing::debug!("Sent heartbeat: {:?}", payload);
tracing::trace!("Sent heartbeat: {:?}", payload);

Ok(())
}
68 changes: 68 additions & 0 deletions token-scraper-core/src/filters.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//! Filters module for the token-scraper application.
use std::path::Path;

use serde::Deserialize;

/// Represents a filter for the token-scraper application.
///
/// This struct is used to define various filters that can be applied to the token-scraper application.
/// It includes fields for filtering by Discord channel ID, Discord user ID, Telegram channel ID,
/// token endpoint URL, and market cap.
#[derive(Debug, Deserialize, Clone)]
pub struct Filter {
/// The name of the filter.
#[serde(rename = "NAME")]
pub name: String,
/// The Discord channel ID to filter.
#[serde(rename = "DISCORD_CHANNEL_ID")]
pub discord_channel_id: Option<u64>,
/// The Discord user ID to filter.
#[serde(rename = "DISCORD_USER_ID")]
pub discord_user_id: Option<u64>,
/// The Telegram channel ID to filter.
#[serde(rename = "TELEGRAM_CHANNEL_ID")]
pub telegram_channel_id: Option<i64>,
/// The token endpoint URL to send to.
#[serde(rename = "TOKEN_ENDPOINT_URL")]
pub token_endpoint_url: String,
/// The market cap to filter.
#[serde(rename = "MARKET_CAP")]
pub market_cap: Option<u128>,
}

#[derive(thiserror::Error, Debug)]
pub enum FiltersError {
/// File error.
#[error("File error: {0}")]
File(#[from] std::io::Error),

/// Deserialization error.
#[error("Deserialization error: {0}")]
Deserialize(#[from] csv::Error),
}

/// Reads filters from a CSV file.
///
/// This function opens the specified CSV file, reads its contents, and deserializes each record into a `Filter` struct.
///
/// # Arguments
///
/// * `file_path` - A `&Path` that holds the path to the CSV file.
///
/// # Errors
///
/// This function will return an error if the file cannot be opened or if deserialization fails.
pub fn read_filters_from_csv(file_path: &Path) -> Result<Vec<Filter>, FiltersError> {
let mut filters = Vec::new();
let file = std::fs::File::open(file_path)?;
let reader = std::io::BufReader::new(file);
let mut rdr = csv::Reader::from_reader(reader);

for result in rdr.deserialize() {
let record: Filter = result?;
filters.push(record);
}

Ok(filters)
}
Loading

0 comments on commit d101f29

Please sign in to comment.