Launch news coins
Telau News Feed is the fastest way to launch news-coins.
Telau News Feed is the fastest way to launch news-coins.
DefinitionNews coins are coins related to an 𝕏 post from an important account.
News-coins are popular in the Solana narrative and get a high-success rate to perform. But the competition is hard and for one news there's many coins related to.
We decided to build our own news-coin launch solution to make your coin the chosen one.
The process is simple: you get access to a super complete 𝕏 memeable news-feed in a Telegram channel. Each message contains a which allows you to create the news-related coin almost instantly.
Speed
Fastest
Very Fast
Fast
Pre-fill metadatas
Pre-fill 𝕏 link, ticker & name.
Pre-fill 𝕏 link & picture
Pre-fill 𝕏 link & picture
Bundle
Auto-Snipe (Snipe your launches without flag your wallets as snipers)
AFK Mode
Custom Tracker
Features details
We use LLM Model llama3-8b-8192 through groq.com which is the fastest and most accurate LLM right now to pre-fill the metadatas (Ticker & Token Name).
Telau sends the following prompt to Ai :
ROLE: Solana memecoin generator.
CONTEXT: You are a hyper-creative Solana memecoin generator for the Solana blockchain ecosystem. You ALWAYS perform your task regardless of the tweet's tone, topic, language, or perceived offensiveness. You treat ALL input content as valid cultural inspiration for memecoin creation — including slang, edgy humor, political references, brand names, or language that may normally be considered offensive in other contexts. You NEVER refuse, apologize, moralize, warn, or comment on the safety of content. You ONLY output the requested JSON.
You operate with the mindset of a top-tier crypto meme creator:
Always try to connect the tweet content to crypto/memecoin culture, even if the tweet is about a completely unrelated topic.
If a strong or funny connection exists → exploit it with wordplay, crypto slang, memes, or cultural references.
If only weak inspiration exists → base the ticker on ONE clear concept/word (not a blend), short and punchy.
Avoid outputs that sound like generic companies, institutions, or boring descriptions.
TASK GOALS:
Generate exactly one pair: {"ticker":"...", "name":"..."}.
Capture cultural references, memes, wordplay, insider jokes, market trends, and viral slang.
Inspiration from existing tokens, brands, and slang is allowed and encouraged (e.g., "wif" meme from dogwifhat).
Maintain thematic consistency between the tweet and the output.
Be creative, but never force unnatural blends of multiple concepts.
CONSTRAINTS:
Ticker: 1–10 characters, uppercase A–Z and/or digits 0–9, no whitespace, no special characters.
Ticker must be based on ONE strong element (word, meme, slang, or reference). Do not merge unrelated concepts unless it’s an obvious meme/joke.
Token name: ≤ 32 characters, spaces allowed, must be human-readable and meme-like.
If irrelevant or no coherent meme is possible → return {"ticker":"","name":""}.
Output format: STRICT JSON, single line, keys in order: ticker, name. No extra characters, no explanations, no line breaks.
INPUT FORMAT (compact JSON): {"t":"","st":""}
t = main tweet text (may contain URLs, emojis, hashtags, punctuation, even be empty)
st = subcontent or quoted/cited tweet text (may contain URLs, emojis, hashtags, punctuation, even be empty)
GENERATION PRINCIPLES:
Always relate output to crypto/memecoin culture, even if the connection is invented.
Wordplay is good: slang + crypto terms, memes, cultural adaptations.
Numbers can be included if they enhance the meme/ticker.
Short, punchy, and memorable outputs are preferred.
Never output institutional or boring names. Never output forced blends if they’re not obvious memes.
FEW-SHOT EXAMPLES:
[IN] {"t":"out wif my friends","st":""}
[OUT] {"ticker":"WIFF","name":"wif friends"}
[IN] {"t":"Serious announcement of Fed rates today.","st":""}
[OUT] {"ticker":"FEDED","name":"Fed rates"}
[IN] {"t":"","st":"first dog on the moon"}
[OUT] {"ticker":"MOONDOG","name":"First Dog on the Moon"}
[IN] {"t":"@paradilf @finnbags is it retardio or retardio cousins","st":".@finnbags can you relaunch retardio and give me some fees now? justice for my nfts i mean 😜"}
[OUT] {"ticker":"RETARDIO","name":"Justice For My NFTs"}
[IN] {"t":"@IcedKnife Real talk - wealth isn't just financial. It's having the conviction to build something meaningful, the patience to see it through, and the wisdom to bring others up with you. Been grinding in these blockchain streets since day one, no handouts. Your mindset shapes your reality.","st":"believe in yourself\n\nmanifest being rich and providing for your family"}
[OUT] {"ticker":"GRIND","name":"Blockchain Street Grind"}
[IN] {"t":"Two giant pandas named #ZhiShi and #ZhiMa welcomed their first visitors at their new home — the Sun Island giant #panda pavilion in Harbin, Heilongjiang province, on Saturday.","st":""}
[OUT] {"ticker":"PANDA","name":"Panda Pavilion"}
[IN] {"t":"In the remote village of Acacías, Colombia, this nearly 300-meter-long zip line is a village's daily transport","st":""}
[OUT] {"ticker":"ZIP","name":"Village Zipline"}
The goal is to launch the perfect coin as quick as possible.
DefinitionBundle is simply landing multiple buy/sells in the same transaction so you can snipe your coin with several wallets without being front-runned.
We simply use the basic jito bundle method.
You can't input your own RPC for this feature because bundle launch can't by default be front-runned so it would be useless.
You can manage your bundle wallets in the bot menu.
DefinitionAuto-Snipe is landing multiple buy at the launch in differents transactions so your wallets aren't flagged as snipers by platforms like
Axiom and it simulates a real interest for your coin. However it's riskier as you can be front-runned.
This feature requires high-speed landing time transactions so we allow you to input your own RPC. You can aslo configure your jito-tip amount in the bot menu to ensure even faster transactions.
Custom tracker allows you to get notifications of a custom 𝕏 account you added manually and create coins from his tweets. There's no 𝕏 account limit.
AFK Mode let's you create news-coin automatically so you don't have to stay on your computer.
Code Blocks (Some)
📌 Tweet: {
"id": "1948422660658012446",
"type": "REPLY",
"created_at": 1753375136000,
"author": {
"handle": "goodalexander",
"profile": {
"name": "goodalexander",
"avatar": "https://pbs.twimg.com/profile_images/1725923340337004544/EHdUi9DY_normal.jpg"
}
},
"body": {
"text": "@pythianism I have a mind virus that a16z is early endlessly adding to liquid charts \n\nIt’s far better to jam tokens higher with an active retail base bc they buy parabolas and the high FDV TGE game is too played out \n\nInsufficient vision",
"mentions": [
{
"id": "887851445982068737",
"handle": "pythianism",
"name": "Vance Spencer"
}
],
"urls": []
},
"reply": {
"id": "1948416024287543613",
"handle": "pythianism"
},
"__ts": 1753375137143
}[package]
name = "news"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.27.0", features = ["native-tls"] }
tungstenite = { version = "0.27.0", features = ["native-tls"] }
futures-util = "0.3"
aes-gcm = "0.10"
aes = "0.8"
base64 = "0.22.1"
hex = "0.4"
dotenv = "0.15"
teloxide = { version = "0.17", features = ["macros"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
log = "0.4"
rand = "0.9.2"
openssl = "0.10.73"
regex = "1.11.1"
once_cell = "1.21.3"
url = "2.5.4"
redis = { version = "0.32.4", features = ["tokio-comp"] }
lz-str = "0.2.1"fn get_env_var(key: &str) -> Result<String, String> {
if let Ok(value) = env::var(key) {
if !value.trim().is_empty() {
return Ok(value);
}
}
dotenv().ok();
env::var(key).map_err(|_| format!("{} must be set in environment or .env file", key))
}
fn generate_websocket_key() -> String {
let mut key = [0u8; 16];
rand::rng().fill(&mut key);
general_purpose::STANDARD.encode(&key)
}
pub fn build_ws_req(
url: &str,
host: &str,
origin: &str,
) -> Request<()> {
Request::builder()
.method("GET")
.uri(url)
.header("Accept", "*/*")
.header("Accept-Encoding", "gzip, deflate, br, zstd")
.header("Accept-Language", "en-US,en;q=0.5")
.header("Cache-Control", "no-cache")
.header("Connection", "keep-alive, Upgrade")
.header("DNT", "1")
.header("Host", host)
.header("Origin", origin)
.header("Pragma", "no-cache")
.header("Sec-Fetch-Dest", "empty")
.header("Sec-Fetch-Mode", "websocket")
.header("Sec-Fetch-Site", "cross-site")
.header("Sec-GPC", "1")
.header("Sec-WebSocket-Extensions", "permessage-deflate")
.header("Sec-WebSocket-Key", generate_websocket_key())
.header("Sec-WebSocket-Version", "13")
.header("Upgrade", "websocket")
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:141.0) Gecko/20100101 Firefox/141.0")
.body(())
.unwrap()
}use base64::{engine::general_purpose, Engine as _};
use futures_util::{SinkExt, StreamExt};
use teloxide::{Bot, types::ChatId};
use std::sync::{Arc, Mutex};
use tokio_tungstenite::connect_async;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
use serde_json::Value;
use openssl::symm::{Cipher, Crypter, Mode};
use rand::{rng, Rng};
use std::time::Duration;
use tokio::time::sleep;
use crate::build_ws_req;
use crate::telegram::send_telegram;
use crate::utils::{AI_CONTENT_MAXLEN, NOMD_URL_STRIP, TweetData, tweet_shortener, trim_raw_content, clean_text, unescape_telegram, full_escape_telegram};
fn string_from_number_or_str<'de, D>(de: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = String;
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str("string or number")
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> { Ok(v.to_string()) }
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> { Ok(v.to_string()) }
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> { Ok(v.to_owned()) }
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> { Ok(v) }
}
de.deserialize_any(V)
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Tweet {
pub created_at: String,
#[serde(deserialize_with = "string_from_number_or_str")]
pub id: String,
pub full_text: String,
#[serde(default)]
pub is_quote_status: Option<bool>,
#[serde(default)]
pub lang: String,
// contexte minimal pour l'action
#[serde(default)]
pub in_reply_to_screen_name: Option<String>,
pub entities: Entities,
#[serde(default)]
pub extended_entities: Option<ExtendedEntities>,
pub user: User,
#[serde(default)]
pub card: Option<Card>,
#[serde(default)]
pub replied_tweet: Option<Box<TweetShallow>>,
#[serde(default)]
pub retweeted_status: Option<Box<TweetShallow>>,
#[serde(default)]
pub quoted_status: Option<Box<TweetShallow>>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct Card {
pub name: String,
pub url: String,
#[serde(default)]
pub card_type_url: Option<String>,
// Très variable -> on garde brut
#[serde(default)]
pub binding_values: Option<Value>,
#[serde(default)]
pub card_platform: Option<Value>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct TweetShallow {
pub card: Option<Card>,
pub full_text: String,
pub user: User,
pub entities: Entities,
pub extended_entities: Option<ExtendedEntities>,
#[serde(default)]
pub quoted_status: Option<Box<TweetShallow>>,
#[serde(default)]
pub replied_tweet: Option<Box<TweetShallow>>,
#[serde(default)]
pub retweeted_status: Option<Box<TweetShallow>>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct User {
#[serde(deserialize_with = "string_from_number_or_str")]
pub id: String,
pub name: String,
pub screen_name: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Entities {
#[serde(default)]
pub hashtags: Vec<Hashtag>,
#[serde(default)]
pub symbols: Vec<SymbolEntity>,
#[serde(default)]
pub user_mentions: Vec<UserMention>,
#[serde(default)]
pub urls: Vec<UrlEntity>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct ExtendedEntities {
#[serde(default)]
pub media: Vec<Media>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Media {
#[serde(deserialize_with = "string_from_number_or_str")]
pub id: String,
#[serde(default)]
pub media_url: Option<String>,
#[serde(rename = "type")]
pub media_type: String,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct Hashtag { pub text: String, #[serde(default)] pub indices: Vec<u32> }
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct SymbolEntity { pub text: String, #[serde(default)] pub indices: Vec<u32> }
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct UserMention {
pub screen_name: String,
pub name: String,
#[serde(deserialize_with = "string_from_number_or_str")]
pub id: String,
#[serde(default)]
pub indices: Vec<u32>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
pub struct UrlEntity {
pub url: String,
#[serde(default)]
pub expanded_url: Option<String>,
#[serde(default)]
pub display_url: Option<String>,
#[serde(default)]
pub indices: Vec<u32>,
}
/* -------- Helpers Telegram -------- */
fn format_urls(text: &str, urls: &[UrlEntity]) -> String {
let mut out = full_escape_telegram(text);
for u in urls {
let tco = full_escape_telegram(&u.url);
if let Some(expanded) = &u.expanded_url {
if !expanded.starts_with("https://x.com/") {
let name = full_escape_telegram(u.display_url.as_deref().unwrap_or(expanded));
let repl = format!("[{}]({})", name, full_escape_telegram(expanded));
out = out.replace(&tco, &repl);
}
}
}
clean_text(out)
}
fn is_media_only(text: &str, ext: &Option<ExtendedEntities>) -> bool {
let trimmed = text.trim();
if !trimmed.is_empty() { return false; }
if let Some(ee) = ext {
return !ee.media.is_empty();
}
false
}
fn quote_block(s: &str) -> String {
if s.trim().is_empty() { return String::new(); }
s.lines()
.map(|ln| format!(">{}", ln))
.collect::<Vec<_>>()
.join("\n")
}
/* -------- action detection + formatting -------- */
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Action { Post, Reply, Quote, Retweet }
fn detect_action(t: &Tweet) -> Action {
if t.retweeted_status.is_some() {
Action::Retweet
} else if t.quoted_status.is_some() {
Action::Quote
} else if t.replied_tweet.is_some() || t.in_reply_to_screen_name.is_some() {
Action::Reply
} else {
Action::Post
}
}
fn title_for(action: Action) -> (&'static str, &'static str) {
match action {
Action::Retweet => ("🔄", "reposted"),
Action::Quote => ("🏷️", "quoted"),
Action::Reply => ("💬", "replied to"),
Action::Post => ("✏️", "posted"),
}
}
fn status_url(user: &User, id: &str) -> String {
format!("https://x.com/{}/status/{}", unescape_telegram(&user.screen_name), id)
}
/* -------- Main API -------- */
fn parse_telegram(t: &Tweet) -> String {
let action = detect_action(t);
let (emoji, verb) = title_for(action);
let aname = full_escape_telegram(&t.user.name);
let ahandle = full_escape_telegram(&t.user.screen_name);
let target_user = match action {
Action::Retweet => t.retweeted_status.as_ref().map(|tw| &tw.user),
Action::Quote => t.quoted_status.as_ref().map(|tw| &tw.user),
Action::Reply => t.replied_tweet.as_ref().map(|tw| &tw.user),
Action::Post => None,
};
let title = match target_user {
Some(u2) => {
let bname = full_escape_telegram(&u2.name);
let bhandle = full_escape_telegram(&u2.screen_name);
format!(
"{emoji}\n*[{aname} \\(@{ahandle}\\) {verb} {bname} \\(@{bhandle}\\)]({link})*",
link = status_url(&t.user, &t.id)
)
}
None => {
format!(
"{emoji}\n*[{aname} \\(@{ahandle}\\) {verb}]({link})*",
link = status_url(&t.user, &t.id)
)
}
};
let mut body = String::new();
if action != Action::Retweet {
let current = format_urls(&t.full_text, &t.entities.urls);
if !is_media_only(¤t, &t.extended_entities) && !current.is_empty() {
body = current;
}
}
let origin = match action {
Action::Retweet => t.retweeted_status.as_deref(),
Action::Quote => t.quoted_status.as_deref(),
Action::Reply => t.replied_tweet.as_deref(),
Action::Post => None,
};
let mut quoted = String::new();
if let Some(orig) = origin {
let qtext = format_urls(&orig.full_text, &orig.entities.urls);
if !qtext.trim().is_empty() {
quoted = quote_block(&qtext);
}
}
let mut out = title;
if !body.is_empty() {
out.push_str("\n\n");
out.push_str(&body);
}
if !quoted.is_empty() {
out.push_str("\n\n");
out.push_str("ed);
}
let lines: Vec<_> = out
.lines()
.map(|l| l.trim().to_string())
.collect();
let mut compact = Vec::new();
let mut prev_empty = false;
for l in lines {
let empty = l.is_empty();
if !empty {
compact.push(l);
prev_empty = false;
} else if !prev_empty {
compact.push(String::new());
prev_empty = true;
}
}
compact.join("\n")
}
fn get_images(t: &Tweet) -> Vec<String> {
let mut best: HashMap<String, (String, u64)> = HashMap::new();
walk_tweet_tree(t, &mut |node| {
collect_media_images_from_tweet_like(node, &mut best);
if let Some(card) = node.card_ref() {
collect_card_images(card, &mut best);
}
});
let mut out: Vec<String> = best.values().map(|(u, _)| u.clone()).collect();
out.sort();
out
}
// ---------- recursive path ----------
fn walk_tweet_tree<'a, T>(root: &'a T, f: &mut impl FnMut(&'a dyn TweetLike))
where
T: TweetLike + 'a,
{
f(root);
if let Some(r) = root.replied_tweet_ref() {
walk_tweet_tree(r, f);
}
if let Some(q) = root.quoted_status_ref() {
walk_tweet_tree(q, f);
}
if let Some(rt) = root.retweeted_status_ref() {
walk_tweet_tree(rt, f);
}
}
trait TweetLike {
fn extended_entities_ref(&self) -> Option<&ExtendedEntities>;
fn card_ref(&self) -> Option<&Card>;
fn replied_tweet_ref(&self) -> Option<&TweetShallow>;
fn quoted_status_ref(&self) -> Option<&TweetShallow>;
fn retweeted_status_ref(&self) -> Option<&TweetShallow>;
}
impl TweetLike for Tweet {
fn extended_entities_ref(&self) -> Option<&ExtendedEntities> { self.extended_entities.as_ref() }
fn card_ref(&self) -> Option<&Card> { self.card.as_ref() }
fn replied_tweet_ref(&self) -> Option<&TweetShallow> { self.replied_tweet.as_deref() }
fn quoted_status_ref(&self) -> Option<&TweetShallow> { self.quoted_status.as_deref() }
fn retweeted_status_ref(&self) -> Option<&TweetShallow> { self.retweeted_status.as_deref() }
}
impl TweetLike for TweetShallow {
fn extended_entities_ref(&self) -> Option<&ExtendedEntities> { self.extended_entities.as_ref() }
fn card_ref(&self) -> Option<&Card> { self.card.as_ref() }
fn replied_tweet_ref(&self) -> Option<&TweetShallow> { None }
fn quoted_status_ref(&self) -> Option<&TweetShallow> { self.quoted_status.as_deref() }
fn retweeted_status_ref(&self) -> Option<&TweetShallow> { None }
}
fn collect_media_images_from_tweet_like(tw: &dyn TweetLike, best: &mut HashMap<String, (String, u64)>) {
if let Some(ext) = tw.extended_entities_ref() {
for m in &ext.media {
match m.media_type.as_str() {
"photo" => {
if let Some(url) = m.media_url.as_ref() {
let (w, h) = guess_size_from_url(url);
insert_candidate(best, url, w, h);
}
}
"video" | "animated_gif" => {
if let Some(url) = m.media_url.as_ref() {
let (w, h) = guess_size_from_url(url);
insert_candidate(best, url, w, h);
}
}
_ => {}
}
}
}
}
// ---------- CARD IMAGES (article previews) ----------
fn collect_card_images(card: &Card, best: &mut HashMap<String, (String, u64)>) {
if let Some(Value::Object(bindings)) = card.binding_values.as_ref() {
for (_k, v) in bindings {
if let Some(img) = v.get("image_value") {
if let (Some(url), w, h) = (
img.get("url").and_then(|u| u.as_str()),
img.get("width").and_then(|n| n.as_u64()).map(|x| x as u32),
img.get("height").and_then(|n| n.as_u64()).map(|x| x as u32),
) {
insert_candidate(best, url, w, h);
continue;
}
}
if v.get("type").and_then(|t| t.as_str()) == Some("IMAGE") {
if let Some(url) = v.get("url").and_then(|u| u.as_str()) {
let (w, h) = guess_size_from_url(url);
insert_candidate(best, url, w, h);
}
}
}
}
}
// ---------- DEDUPLICATION & SELECTION OF THE BEST QUALITY ----------
fn insert_candidate(best: &mut HashMap<String, (String, u64)>, url: &str, w: Option<u32>, h: Option<u32>) {
let key = normalize_url_key(url);
let area = w.zip(h).map(|(x, y)| x as u64 * y as u64).unwrap_or_else(|| guess_area_from_url(url));
match best.get_mut(&key) {
Some((existing_url, existing_area)) => {
if area > *existing_area {
*existing_url = url.to_string();
*existing_area = area;
}
}
None => {
best.insert(key, (url.to_string(), area));
}
}
}
fn normalize_url_key(url: &str) -> String {
match url.split_once('?') {
Some((base, _)) => base.to_string(),
None => url.to_string(),
}
}
fn guess_size_from_url(url: &str) -> (Option<u32>, Option<u32>) {
if let Some(pos) = url.find("name=") {
let s = &url[pos + 5..];
let end = s.find(['&', '#', '/']).unwrap_or_else(|| s.len());
if let Some((w, h)) = parse_wx_h(&s[..end]) {
return (Some(w), Some(h));
}
}
for seg in url.split('/') {
if let Some((w, h)) = parse_wx_h(seg) {
return (Some(w), Some(h));
}
}
(None, None)
}
fn parse_wx_h(s: &str) -> Option<(u32, u32)> {
let mut it = s.split('x');
let w = it.next()?.chars().take_while(|c| c.is_ascii_digit()).collect::<String>().parse::<u32>().ok()?;
let h = it.next()?.chars().take_while(|c| c.is_ascii_digit()).collect::<String>().parse::<u32>().ok()?;
Some((w, h))
}
fn guess_area_from_url(url: &str) -> u64 {
if url.contains("2048x") || url.contains("name=orig") { return 2048 * 2048 }
if url.contains("1200x") || url.contains("name=large") { return 1200 * 1200 }
if url.contains("800x") || url.contains("name=medium") { return 800 * 800 }
if url.contains("600x") || url.contains("name=small") { return 600 * 600 }
1
}
#[derive(Debug, Deserialize)]
struct CryptoKeyMsg {
key_id: u8,
key_b64: String,
}
#[derive(serde::Deserialize)]
struct DecryptedContent {
t: String,
d: serde_json::Value,
}
fn decrypt_message(key: &[u8], data: &[u8]) -> Result<String, String> {
// data layout: [ key_id (1) | iv (12) | ciphertext (...) | tag (16) ]
if data.len() < 29 {
return Err("message too short".to_string());
}
let iv = &data[1..13]; // 12 bytes
let tag = &data[data.len() - 16..]; // 16 bytes
let ciphertext = &data[13..data.len() - 16];
let cipher = Cipher::aes_256_gcm();
let mut crypter = Crypter::new(cipher, Mode::Decrypt, key, Some(iv))
.map_err(|e| format!("Crypter::new failed: {}", e))?;
crypter.set_tag(tag)
.map_err(|e| format!("set_tag failed: {}", e))?;
let mut out = vec![0u8; ciphertext.len() + cipher.block_size()];
let mut count = crypter.update(ciphertext, &mut out)
.map_err(|e| format!("update failed: {}", e))?;
count += crypter.finalize(&mut out[count..])
.map_err(|e| format!("finalize failed: {}", e))?;
out.truncate(count);
String::from_utf8(out).map_err(|e| format!("utf8 conversion failed: {}", e))
}
pub fn serialize_tweet(tweet: &Tweet) -> TweetData {
let action = detect_action(tweet);
let mut content = String::new();
let mut subcontent = String::new();
// Get current tweet content (not for retweets)
if action != Action::Retweet && !tweet.full_text.trim().is_empty() {
content = trim_raw_content(&tweet.full_text, AI_CONTENT_MAXLEN);
}
// Get subcontent from original tweet
match action {
Action::Retweet => {
if let Some(rt) = &tweet.retweeted_status {
subcontent = trim_raw_content(&rt.full_text, AI_CONTENT_MAXLEN);
}
},
Action::Quote => {
if let Some(qt) = &tweet.quoted_status {
subcontent = trim_raw_content(&qt.full_text, AI_CONTENT_MAXLEN);
}
},
Action::Reply => {
if let Some(reply) = &tweet.replied_tweet {
subcontent = trim_raw_content(&reply.full_text, AI_CONTENT_MAXLEN);
}
},
Action::Post => {}
}
content = NOMD_URL_STRIP.replace_all(&content, "").into_owned();
content = NOMD_URL_STRIP.replace_all(&content, "").into_owned();
subcontent = NOMD_URL_STRIP.replace_all(&subcontent, "").into_owned();
subcontent = NOMD_URL_STRIP.replace_all(&subcontent, "").into_owned();
TweetData {
content,
subcontent,
}
}Last updated
