Function bodies 355 total
select_media_url function · rust · L816-L829 (14 LOC)crates/nostrweet-cli/src/main.rs
fn select_media_url(media: &Media) -> Option<(HttpUrl, Option<String>)> {
if let Some(url) = &media.url {
return Some((url.clone(), None));
}
if matches!(media.kind, MediaKind::Video | MediaKind::AnimatedGif)
&& let Some(variant) = select_media_variant(media)
{
return Some((variant.url.clone(), Some(variant.content_type)));
}
media.preview_image_url.clone().map(|url| (url, None))
}media_downloads_for_tweet function · rust · L830-L836 (7 LOC)crates/nostrweet-cli/src/main.rs
fn media_downloads_for_tweet(tweet: &Tweet) -> Vec<MediaDownload> {
let mut downloads = Vec::new();
let mut seen = HashSet::new();
collect_media_downloads(tweet, &mut downloads, &mut seen);
downloads
}collect_media_downloads function · rust · L837-L895 (59 LOC)crates/nostrweet-cli/src/main.rs
fn collect_media_downloads(
tweet: &Tweet,
downloads: &mut Vec<MediaDownload>,
seen: &mut HashSet<String>,
) {
if let Some(includes) = &tweet.includes {
for media in &includes.media {
let key = media.media_key.as_str().to_string();
if !seen.insert(key.clone()) {
continue;
}
let Some((url, variant_content_type)) = select_media_url(media) else {
continue;
};
let media_key_clean = media.media_key.cleaned();
let username = if tweet.author.username.as_str().is_empty() {
"unknown"
} else {
tweet.author.username.as_str()
};
let extension = match media.kind {
MediaKind::Photo => Some("jpg".to_string()),
MediaKind::Video | MediaKind::AnimatedGif => Some("mp4".to_string()),
MediaKind::Other(_) => extension_from_url(&url),
}
fetch_media_assets function · rust · L896-L933 (38 LOC)crates/nostrweet-cli/src/main.rs
async fn fetch_media_assets(data_dir: &Path, tweet: &Tweet) -> Result<Vec<MediaAsset>> {
let mut assets = Vec::new();
for download in media_downloads_for_tweet(tweet) {
let path = data_dir.join(&download.filename);
let bytes = if path.exists() {
std::fs::read(&path).with_context(|| {
format!("Failed to read cached media file at {}", path.display())
})?
} else {
let response = reqwest::get(download.url.as_str())
.await
.with_context(|| format!("Failed to download media {}", download.url.as_str()))?;
let status = response.status();
if !status.is_success() {
anyhow::bail!(
"Media download failed with HTTP {status} for {}",
download.url.as_str()
);
}
let bytes = response
.bytes()
.await
.context("Failed to reabuild_blossom_client function · rust · L934-L949 (16 LOC)crates/nostrweet-cli/src/main.rs
async fn build_blossom_client(servers: &[String]) -> Result<Option<BlossomClient>> {
if servers.is_empty() {
return Ok(None);
}
let mut parsed = Vec::new();
for server in servers {
parsed.push(
nostrweet_core::BlossomUrl::parse(server)
.with_context(|| format!("Invalid Blossom server URL: {server}"))?,
);
}
// FIXME: generate NIP-98 auth header and call .with_auth_header() so uploads
// work on Blossom servers that require authentication.
Ok(Some(BlossomClient::new(parsed)?))
}parse_username function · rust · L950-L953 (4 LOC)crates/nostrweet-cli/src/main.rs
fn parse_username(input: &str) -> Result<Username> {
Username::parse(input).with_context(|| format!("Invalid username: {input}"))
}derive_keys_for_user function · rust · L954-L958 (5 LOC)crates/nostrweet-cli/src/main.rs
fn derive_keys_for_user(user_id: &UserId, mnemonic: &MnemonicPhrase) -> Result<Keys> {
let secret = derive_nostr_secret_key(user_id, mnemonic, None)?;
Keys::parse(&secret.to_hex()).context("Failed to derive Nostr keys")
}Repobility · MCP-ready · https://repobility.com
fetch_profile function · rust · L959-L967 (9 LOC)crates/nostrweet-cli/src/main.rs
async fn fetch_profile(data_dir: &Path, bearer_token: &str, username: &str) -> Result<()> {
let username = parse_username(username)?;
let twitter = TwitterClient::new(bearer_token)?;
let storage = FileStorage::new(data_dir)?;
let user = twitter.fetch_user_profile(&username).await?;
storage.save_user_profile(&user).await?;
Ok(())
}load_or_fetch_tweet function · rust · L968-L981 (14 LOC)crates/nostrweet-cli/src/main.rs
async fn load_or_fetch_tweet(
data_dir: &Path,
bearer_token: Option<&str>,
tweet_id: &TweetId,
) -> Result<Tweet> {
let storage = FileStorage::new(data_dir)?;
let twitter = if let Some(token) = bearer_token {
Some(TwitterClient::new(token)?)
} else {
None
};
load_or_fetch_tweet_with_ports(&storage, twitter.as_ref(), tweet_id).await
}load_or_fetch_tweet_with_ports function · rust · L982-L1014 (33 LOC)crates/nostrweet-cli/src/main.rs
async fn load_or_fetch_tweet_with_ports<S: StoragePort, T: TwitterPort>(
storage: &S,
twitter: Option<&T>,
tweet_id: &TweetId,
) -> Result<Tweet> {
if storage.is_tweet_not_found(tweet_id).await? {
bail!(
"Tweet {} was previously marked as not found",
tweet_id.as_str()
);
}
if let Some(tweet) = storage.load_tweet(tweet_id).await? {
return Ok(tweet);
}
let twitter = twitter.context("Twitter bearer token required to fetch tweet from API")?;
match twitter.fetch_tweet(tweet_id).await {
Ok(tweet) => {
storage.save_tweet(&tweet).await?;
Ok(tweet)
}
Err(err) => {
if let Some(TwitterAdapterError::TweetNotFound { .. }) =
err.downcast_ref::<TwitterAdapterError>()
{
storage.mark_tweet_not_found(tweet_id).await?;
}
Err(err)
}
}
}fetch_tweet function · rust · L1015-L1037 (23 LOC)crates/nostrweet-cli/src/main.rs
async fn fetch_tweet(
data_dir: &Path,
bearer_token: &str,
tweet_url_or_id: &str,
skip_profiles: bool,
) -> Result<()> {
let tweet_id = TweetId::parse(tweet_url_or_id)
.with_context(|| format!("Failed to parse tweet ID from {tweet_url_or_id}"))?;
let twitter = TwitterClient::new(bearer_token)?;
let storage = FileStorage::new(data_dir)?;
let mut tweet = load_or_fetch_tweet(data_dir, Some(bearer_token), &tweet_id).await?;
twitter.enrich_referenced_tweets(&mut tweet).await?;
storage.save_tweet(&tweet).await?;
let _ = fetch_media_assets(data_dir, &tweet).await?;
if !skip_profiles {
download_profiles_for_tweet(&twitter, &storage, &tweet).await?;
}
Ok(())
}user_tweets function · rust · L1038-L1069 (32 LOC)crates/nostrweet-cli/src/main.rs
async fn user_tweets(
data_dir: &Path,
bearer_token: &str,
username: &str,
count: u32,
days: Option<u32>,
skip_profiles: bool,
) -> Result<()> {
let username = parse_username(username)?;
let twitter = TwitterClient::new(bearer_token)?;
let storage = FileStorage::new(data_dir)?;
let query = UserTweetsQuery {
count,
days,
since_id: None,
};
let mut tweets = twitter.fetch_user_tweets(&username, query).await?;
let mut processed = Vec::new();
for tweet in &mut tweets {
twitter.enrich_referenced_tweets(tweet).await?;
storage.save_tweet(tweet).await?;
let _ = fetch_media_assets(data_dir, tweet).await?;
processed.push(tweet.clone());
}
if !skip_profiles {
download_profiles_for_tweets(&twitter, &storage, &processed).await?;
}
Ok(())
}download_profiles_for_tweets function · rust · L1070-L1081 (12 LOC)crates/nostrweet-cli/src/main.rs
async fn download_profiles_for_tweets(
twitter: &TwitterClient,
storage: &FileStorage<nostrweet_storage::SystemClock>,
tweets: &[Tweet],
) -> Result<()> {
let mut usernames = HashSet::new();
for tweet in tweets {
usernames.extend(collect_usernames_from_tweet(tweet));
}
download_profiles_for_usernames(twitter, storage, usernames).await
}download_profiles_for_tweet function · rust · L1082-L1090 (9 LOC)crates/nostrweet-cli/src/main.rs
async fn download_profiles_for_tweet(
twitter: &TwitterClient,
storage: &FileStorage<nostrweet_storage::SystemClock>,
tweet: &Tweet,
) -> Result<()> {
let usernames = collect_usernames_from_tweet(tweet);
download_profiles_for_usernames(twitter, storage, usernames).await
}download_profiles_for_usernames function · rust · L1091-L1106 (16 LOC)crates/nostrweet-cli/src/main.rs
async fn download_profiles_for_usernames(
twitter: &TwitterClient,
storage: &FileStorage<nostrweet_storage::SystemClock>,
usernames: HashSet<String>,
) -> Result<()> {
for username in usernames {
let Ok(username) = Username::parse(&username) else {
continue;
};
if let Ok(user) = twitter.fetch_user_profile(&username).await {
let _ = storage.save_user_profile(&user).await?;
}
}
Ok(())
}Repobility · code-quality intelligence platform · https://repobility.com
collect_usernames_from_tweet function · rust · L1107-L1131 (25 LOC)crates/nostrweet-cli/src/main.rs
fn collect_usernames_from_tweet(tweet: &Tweet) -> HashSet<String> {
let mut usernames = HashSet::new();
if !tweet.author.username.as_str().is_empty() {
usernames.insert(tweet.author.username.as_str().to_string());
}
if let Some(entities) = &tweet.entities {
for mention in &entities.mentions {
usernames.insert(mention.username.as_str().to_string());
}
}
for reference in &tweet.referenced_tweets {
if let Some(data) = &reference.data {
if !data.author.username.as_str().is_empty() {
usernames.insert(data.author.username.as_str().to_string());
}
if let Some(entities) = &data.entities {
for mention in &entities.mentions {
usernames.insert(mention.username.as_str().to_string());
}
}
}
}
usernames
}new function · rust · L1149-L1156 (8 LOC)crates/nostrweet-cli/src/main.rs
fn new(data_dir: Option<PathBuf>, mnemonic: Option<MnemonicPhrase>) -> Self {
Self {
username_to_pubkey: HashMap::new(),
user_id_to_pubkey: HashMap::new(),
data_dir,
mnemonic,
}
}add_known_user function · rust · L1157-L1177 (21 LOC)crates/nostrweet-cli/src/main.rs
fn add_known_user(&mut self, username: &Username, user_id: &UserId) -> Result<()> {
let Some(mnemonic) = &self.mnemonic else {
return Ok(());
};
if self.user_id_to_pubkey.contains_key(user_id.as_str()) {
if let Some(pubkey) = self.user_id_to_pubkey.get(user_id.as_str()) {
self.username_to_pubkey
.insert(username.as_str().to_string(), *pubkey);
}
return Ok(());
}
let keys = derive_keys_for_user(user_id, mnemonic)?;
let pubkey = keys.public_key();
self.user_id_to_pubkey
.insert(user_id.as_str().to_string(), pubkey);
self.username_to_pubkey
.insert(username.as_str().to_string(), pubkey);
Ok(())
}resolve_username function · rust · L1178-L1204 (27 LOC)crates/nostrweet-cli/src/main.rs
async fn resolve_username(&mut self, username: &str) -> Result<Option<nostr_sdk::PublicKey>> {
if let Some(pubkey) = self.username_to_pubkey.get(username) {
return Ok(Some(*pubkey));
}
let Some(mnemonic) = &self.mnemonic else {
return Ok(None);
};
let Some(data_dir) = &self.data_dir else {
return Ok(None);
};
let storage = FileStorage::new(data_dir)?;
let Ok(username_parsed) = Username::parse(username) else {
return Ok(None);
};
let user = storage.load_latest_user_profile(&username_parsed).await?;
let Some(user) = user else {
return Ok(None);
};
let keys = derive_keys_for_user(&user.id, mnemonic)?;
let pubkey = keys.public_key();
self.user_id_to_pubkey
.insert(user.id.as_str().to_string(), pubkey);
self.username_to_pubkey.insert(username.to_string(), pubkey);
Ok(Some(pubkey))extract_additional_mentions function · rust · L1206-L1223 (18 LOC)crates/nostrweet-cli/src/main.rs
fn extract_additional_mentions(text: &str, already_processed: &HashSet<String>) -> Vec<String> {
let mut mentions = Vec::new();
for word in text.split_whitespace() {
if !word.starts_with('@') || word.len() <= 1 {
continue;
}
let username = word[1..].trim_end_matches(|c: char| !c.is_alphanumeric() && c != '_');
if username.is_empty() || username.len() > 15 {
continue;
}
if already_processed.contains(username) {
continue;
}
mentions.push(username.to_string());
}
mentions
}pubkey_to_bech32 function · rust · L1224-L1230 (7 LOC)crates/nostrweet-cli/src/main.rs
fn pubkey_to_bech32(pubkey: &nostr_sdk::PublicKey) -> String {
match pubkey.to_bech32() {
Ok(npub) => npub,
Err(err) => match err {},
}
}process_mentions_in_text function · rust · L1231-L1269 (39 LOC)crates/nostrweet-cli/src/main.rs
async fn process_mentions_in_text(
text: &str,
entities: Option<&nostrweet_core::Entities>,
resolver: &mut NostrLinkResolver,
) -> Result<(String, Vec<nostr_sdk::PublicKey>)> {
let mut result = text.to_string();
let mut mentioned_pubkeys = Vec::new();
let mut processed = HashSet::new();
if let Some(entities) = entities {
for mention in &entities.mentions {
let username = mention.username.as_str();
if processed.contains(username) {
continue;
}
processed.insert(username.to_string());
if let Some(pubkey) = resolver.resolve_username(username).await? {
let npub = pubkey_to_bech32(&pubkey);
let old = format!("@{username}");
let new = format!("nostr:{npub}");
result = result.replace(&old, &new);
mentioned_pubkeys.push(pubkey);
}
}
}
for username in extract_additional_mentioformat_tweet_text_with_mentions function · rust · L1270-L1291 (22 LOC)crates/nostrweet-cli/src/main.rs
async fn format_tweet_text_with_mentions(
tweet: &Tweet,
media_urls: &[HttpUrl],
resolver: &mut NostrLinkResolver,
) -> Result<FormattedContent> {
resolver.add_known_user(&tweet.author.username, &tweet.author.id)?;
let raw_text = tweet
.note_tweet
.as_ref()
.map(|note| note.text.as_str())
.unwrap_or_else(|| tweet.text.as_str());
let decoded = decode_html_entities(raw_text);
let expanded = expand_urls_in_text(&decoded, tweet.entities.as_ref(), media_urls, tweet);
let (text_with_mentions, mentioned_pubkeys) =
process_mentions_in_text(&expanded.text, tweet.entities.as_ref(), resolver).await?;
Ok(FormattedContent {
text: text_with_mentions,
used_media_urls: expanded.used_media_urls,
mentioned_pubkeys,
})
}Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
is_simple_retweet function · rust · L1292-L1619 (328 LOC)crates/nostrweet-cli/src/main.rs
fn is_simple_retweet(tweet: &Tweet) -> (bool, Option<String>) {
if !tweet
.referenced_tweets
.iter()
.any(|rt| matches!(rt.kind, nostrweet_core::ReferenceKind::Retweeted))
{
return (false, None);
}
let raw_text = tweet
.note_tweet
.as_ref()
.map(|note| note.text.as_str())
.unwrap_or_else(|| tweet.text.as_str());
let is_simple = raw_text.starts_with("RT @")
&& raw_text.contains(':')
&& !raw_text.contains('\n')
&& !raw_text.contains(" // ");
let username = if is_simple {
raw_text.find(':').and_then(|end_idx| {
raw_text
.find('@')
.map(|start_idx| raw_text[(start_idx + 1)..end_idx].trim().to_string())
})
} else {
None
};
(is_simple, username)
}
async fn format_reply_section(
content: &mut String,
ref_tweet: &nostrweet_core::ReferencedTweet,
resolver: &mut NostrLinkResolver,
) -> Result<format_reply_section function · rust · L1321-L1362 (42 LOC)crates/nostrweet-cli/src/main.rs
async fn format_reply_section(
content: &mut String,
ref_tweet: &nostrweet_core::ReferencedTweet,
resolver: &mut NostrLinkResolver,
) -> Result<Vec<nostr_sdk::PublicKey>> {
let mut mentioned_pubkeys = Vec::new();
let tweet_url = build_twitter_status_url(&ref_tweet.id);
if let Some(data) = &ref_tweet.data {
resolver.add_known_user(&data.author.username, &data.author.id)?;
let author = if let Some(pubkey) = resolver
.resolve_username(data.author.username.as_str())
.await?
{
mentioned_pubkeys.push(pubkey);
format!("nostr:{}", pubkey_to_bech32(&pubkey))
} else {
format!("@{}", data.author.username.as_str())
};
content.push_str(&format!("↩️ Reply to {author}:\n"));
let media_urls = extract_media_urls(data);
let formatted = format_tweet_text_with_mentions(data, &media_urls, resolver).await?;
mentioned_pubkeys.extend(formatted.mentionedformat_quote_section function · rust · L1363-L1403 (41 LOC)crates/nostrweet-cli/src/main.rs
async fn format_quote_section(
content: &mut String,
ref_tweet: &nostrweet_core::ReferencedTweet,
resolver: &mut NostrLinkResolver,
) -> Result<Vec<nostr_sdk::PublicKey>> {
let mut mentioned_pubkeys = Vec::new();
let tweet_url = build_twitter_status_url(&ref_tweet.id);
if let Some(data) = &ref_tweet.data {
resolver.add_known_user(&data.author.username, &data.author.id)?;
let author = if let Some(pubkey) = resolver
.resolve_username(data.author.username.as_str())
.await?
{
mentioned_pubkeys.push(pubkey);
format!("nostr:{}", pubkey_to_bech32(&pubkey))
} else {
format!("@{}", data.author.username.as_str())
};
content.push_str(&format!("💬 Quote of {author}:\n"));
let media_urls = extract_media_urls(data);
let formatted = format_tweet_text_with_mentions(data, &media_urls, resolver).await?;
mentioned_pubkeys.extend(formatted.mentioned_pformat_retweet_section function · rust · L1404-L1444 (41 LOC)crates/nostrweet-cli/src/main.rs
async fn format_retweet_section(
content: &mut String,
ref_tweet: &nostrweet_core::ReferencedTweet,
retweeter: &str,
resolver: &mut NostrLinkResolver,
) -> Result<Vec<nostr_sdk::PublicKey>> {
let mut mentioned_pubkeys = Vec::new();
let tweet_url = build_twitter_status_url(&ref_tweet.id);
if let Some(data) = &ref_tweet.data {
resolver.add_known_user(&data.author.username, &data.author.id)?;
let author = if let Some(pubkey) = resolver
.resolve_username(data.author.username.as_str())
.await?
{
mentioned_pubkeys.push(pubkey);
format!("nostr:{}", pubkey_to_bech32(&pubkey))
} else {
format!("@{}", data.author.username.as_str())
};
content.push_str(&format!("🔁 @{retweeter} retweeted {author}:\n"));
let media_urls = extract_media_urls(data);
let formatted = format_tweet_text_with_mentions(data, &media_urls, resolver).await?;
mentionedformat_tweet_as_nostr_content_with_mentions function · rust · L1445-L1505 (61 LOC)crates/nostrweet-cli/src/main.rs
async fn format_tweet_as_nostr_content_with_mentions(
tweet: &Tweet,
media_urls: &[HttpUrl],
resolver: &mut NostrLinkResolver,
) -> Result<(String, Vec<nostr_sdk::PublicKey>)> {
let mut content = String::new();
let mut mentioned_pubkeys = Vec::new();
let (is_simple_retweet, rt_username) = is_simple_retweet(tweet);
if !is_simple_retweet {
if !tweet.author.username.as_str().is_empty() {
content.push_str(&format!("🐦 @{}: ", tweet.author.username.as_str()));
} else if let Some(author_id) = tweet.author_id.as_ref() {
content.push_str(&format!("🐦 User {}: ", author_id.as_str()));
} else {
content.push_str("🐦 Tweet: ");
}
let formatted = format_tweet_text_with_mentions(tweet, media_urls, resolver).await?;
mentioned_pubkeys.extend(formatted.mentioned_pubkeys);
content.push_str(&formatted.text);
content.push('\n');
for url in media_urls {
if !fobuild_nostr_event_tags function · rust · L1506-L1535 (30 LOC)crates/nostrweet-cli/src/main.rs
fn build_nostr_event_tags(
tweet_id: &TweetId,
original_media_urls: &[HttpUrl],
blossom_urls: &[HttpUrl],
mentioned_pubkeys: &[nostr_sdk::PublicKey],
) -> Result<Vec<NostrTag>> {
let mut tags = Vec::new();
let original_url = HttpUrl::parse(&build_twitter_status_url(tweet_id))?;
tags.push(NostrTag::r(&original_url));
for pubkey in mentioned_pubkeys {
let nostr_pubkey = NostrPubkey::parse(&pubkey.to_hex())?;
tags.push(NostrTag::p(&nostr_pubkey));
}
if blossom_urls.is_empty() {
for url in original_media_urls {
tags.push(NostrTag::media(url));
}
} else {
for (source, media) in original_media_urls.iter().zip(blossom_urls.iter()) {
tags.push(NostrTag::source(source));
tags.push(NostrTag::media(media));
}
}
tags.push(NostrTag::client());
Ok(tags)
}build_event_from_draft function · rust · L1536-L1553 (18 LOC)crates/nostrweet-cli/src/main.rs
async fn build_event_from_draft(draft: &NostrEventDraft, keys: &Keys) -> Result<nostr_sdk::Event> {
let mut builder = EventBuilder::new(Kind::TextNote, draft.content.clone());
if let Some(created_at) = draft.created_at {
builder = builder.custom_created_at(Timestamp::from(created_at.value()));
}
for tag in &draft.tags {
let mut values = Vec::with_capacity(1 + tag.values.len());
values.push(tag.name.clone());
values.extend(tag.values.clone());
builder = builder.tag(Tag::parse(values)?);
}
let event = builder
.sign(keys)
.await
.context("Failed to sign Nostr event")?;
Ok(event)
}build_nostr_client function · rust · L1554-L1565 (12 LOC)crates/nostrweet-cli/src/main.rs
async fn build_nostr_client(keys: &Keys, relays: &[String]) -> Result<NostrClient> {
let client = NostrClient::new(keys.clone());
for relay in relays {
client
.add_relay(relay)
.await
.with_context(|| format!("Failed to add relay: {relay}"))?;
}
client.connect().await;
Ok(client)
}All rows scored by the Repobility analyzer (https://repobility.com)
find_existing_event function · rust · L1566-L1587 (22 LOC)crates/nostrweet-cli/src/main.rs
async fn find_existing_event(
client: &NostrClient,
tweet_id: &TweetId,
keys: &Keys,
) -> Result<Option<nostr_sdk::Event>> {
let pubkey = keys.public_key();
let twitter_url = build_twitter_status_url(tweet_id);
let filter = Filter::new()
.author(pubkey)
.kind(Kind::TextNote)
.custom_tag(SingleLetterTag::lowercase(Alphabet::R), twitter_url)
.limit(10);
match client.fetch_events(filter, Duration::from_secs(10)).await {
Ok(events) => Ok(events.into_iter().next()),
Err(err) => {
warn!("Failed to check existing events for {tweet_id}: {err}");
Ok(None)
}
}
}save_nostr_event_json function · rust · L1588-L1602 (15 LOC)crates/nostrweet-cli/src/main.rs
fn save_nostr_event_json(data_dir: &Path, event: &nostr_sdk::Event) -> Result<()> {
let dir = data_dir.join("nostr_events");
std::fs::create_dir_all(&dir).with_context(|| {
format!(
"Failed to create nostr_events directory at {}",
dir.display()
)
})?;
let path = dir.join(format!("{}.json", event.id.to_hex()));
let json = serde_json::to_string_pretty(event).context("Failed to serialize Nostr event")?;
std::fs::write(&path, json)
.with_context(|| format!("Failed to write Nostr event to {}", path.display()))?;
Ok(())
}parse_relay_urls function · rust · L1603-L1612 (10 LOC)crates/nostrweet-cli/src/main.rs
fn parse_relay_urls(relays: &[String]) -> Result<Vec<nostrweet_core::RelayUrl>> {
relays
.iter()
.map(|relay| {
nostrweet_core::RelayUrl::parse(relay)
.with_context(|| format!("Invalid relay URL: {relay}"))
})
.collect()
}build_profile_metadata function · rust · L1620-L1647 (28 LOC)crates/nostrweet-cli/src/main.rs
fn build_profile_metadata(user: &User, username: &Username) -> Metadata {
let mut metadata = Metadata::new();
if let Some(name) = &user.name {
metadata = metadata.name(name);
}
let disclaimer = profile_disclaimer(username);
let about = match &user.description {
Some(desc) => format!("{desc}{disclaimer}"),
None => disclaimer,
};
metadata = metadata.about(&about);
if let Some(url) = &user.profile_image_url
&& let Ok(parsed) = url.as_str().parse()
{
metadata = metadata.picture(parsed);
}
if let Some(url) = &user.url
&& let Ok(parsed) = url.as_str().parse()
{
metadata = metadata.website(parsed);
}
metadata
}post_single_profile function · rust · L1648-L1674 (27 LOC)crates/nostrweet-cli/src/main.rs
async fn post_single_profile(
username: &Username,
adapter: &impl NostrAdapter,
data_dir: &Path,
mnemonic: &MnemonicPhrase,
) -> Result<nostr_sdk::EventId> {
let storage = FileStorage::new(data_dir)?;
let Some(user) = storage.load_latest_user_profile(username).await? else {
bail!(
"No profile found for user '@{username}'",
username = username.as_str()
);
};
let keys = derive_keys_for_user(&user.id, mnemonic)?;
let metadata = build_profile_metadata(&user, username);
adapter
.publish_profile(metadata, &keys)
.await
.with_context(|| {
format!(
"Failed to publish profile for @{username}",
username = username.as_str()
)
})
}post_relay_list_for_user function · rust · L1675-L1694 (20 LOC)crates/nostrweet-cli/src/main.rs
async fn post_relay_list_for_user(
username: &Username,
adapter: &impl NostrAdapter,
data_dir: &Path,
mnemonic: &MnemonicPhrase,
relays: &[String],
) -> Result<()> {
let storage = FileStorage::new(data_dir)?;
let Some(user) = storage.load_latest_user_profile(username).await? else {
bail!(
"No profile found for user '@{username}'",
username = username.as_str()
);
};
let keys = derive_keys_for_user(&user.id, mnemonic)?;
adapter.update_relay_list(relays, &keys).await?;
Ok(())
}post_user_profile_with_relay_list function · rust · L1695-L1710 (16 LOC)crates/nostrweet-cli/src/main.rs
async fn post_user_profile_with_relay_list(
username: &Username,
adapter: &impl NostrAdapter,
data_dir: &Path,
mnemonic: &MnemonicPhrase,
relays: &[String],
) -> Result<()> {
let event_id = post_single_profile(username, adapter, data_dir, mnemonic).await?;
debug!(
"Posted profile for @{username} with event ID {event_id}",
username = username.as_str()
);
post_relay_list_for_user(username, adapter, data_dir, mnemonic, relays).await?;
Ok(())
}check_profile_exists function · rust · L1711-L1725 (15 LOC)crates/nostrweet-cli/src/main.rs
async fn check_profile_exists(
username: &Username,
adapter: &impl NostrAdapter,
data_dir: &Path,
mnemonic: &MnemonicPhrase,
) -> Result<bool> {
let storage = FileStorage::new(data_dir)?;
let Some(user) = storage.load_latest_user_profile(username).await? else {
return Ok(false);
};
let keys = derive_keys_for_user(&user.id, mnemonic)?;
adapter.profile_exists(&keys.public_key()).await
}Repobility · MCP-ready · https://repobility.com
filter_profiles_to_post function · rust · L1726-L1761 (36 LOC)crates/nostrweet-cli/src/main.rs
async fn filter_profiles_to_post(
usernames: HashSet<String>,
adapter: &impl NostrAdapter,
data_dir: &Path,
force: bool,
mnemonic: &MnemonicPhrase,
) -> Result<HashSet<String>> {
if force {
return Ok(usernames);
}
let storage = FileStorage::new(data_dir)?;
let mut profiles_to_post = HashSet::new();
for username in usernames {
let Ok(username_parsed) = Username::parse(&username) else {
continue;
};
let Some(user) = storage.load_latest_user_profile(&username_parsed).await? else {
continue;
};
let keys = derive_keys_for_user(&user.id, mnemonic)?;
let has_existing_profile = adapter
.profile_exists(&keys.public_key())
.await
.unwrap_or(false);
if !has_existing_profile {
profiles_to_post.insert(username);
}
}
Ok(profiles_to_post)
}post_referenced_profiles function · rust · L1762-L1794 (33 LOC)crates/nostrweet-cli/src/main.rs
async fn post_referenced_profiles(
usernames: &HashSet<String>,
adapter: &impl NostrAdapter,
data_dir: &Path,
mnemonic: &MnemonicPhrase,
) -> Result<usize> {
if usernames.is_empty() {
return Ok(0);
}
let mut posted_count = 0;
let mut failed_count = 0;
for username in usernames {
let Ok(username_parsed) = Username::parse(username) else {
continue;
};
match post_single_profile(&username_parsed, adapter, data_dir, mnemonic).await {
Ok(_) => posted_count += 1,
Err(err) => {
debug!("Failed to post profile for @{username}: {err}");
failed_count += 1;
}
}
}
if failed_count > 0 {
debug!("Posted {posted_count} profiles, {failed_count} failed");
}
Ok(posted_count)
}post_tweet_to_nostr function · rust · L1797-L1936 (140 LOC)crates/nostrweet-cli/src/main.rs
async fn post_tweet_to_nostr(
data_dir: &Path,
bearer_token: Option<&str>,
mnemonic: &str,
tweet_url_or_id: &str,
relays: &[String],
blossom_servers: &[String],
force: bool,
skip_profiles: bool,
) -> Result<()> {
let tweet_id = TweetId::parse(tweet_url_or_id)
.with_context(|| format!("Failed to parse tweet ID from {tweet_url_or_id}"))?;
let storage = FileStorage::new(data_dir)?;
if let Some(_existing) = storage.load_nostr_event_info(&tweet_id).await?
&& !force
{
return Ok(());
}
let twitter = if let Some(token) = bearer_token {
Some(TwitterClient::new(token)?)
} else {
None
};
let mut tweet = load_or_fetch_tweet_with_ports(&storage, twitter.as_ref(), &tweet_id).await?;
if let Some(twitter) = twitter.as_ref() {
twitter.enrich_referenced_tweets(&mut tweet).await?;
if !skip_profiles {
download_profiles_for_tweet(twitter, &storage, &tweet).await?;
post_user_to_nostr function · rust · L1937-L1994 (58 LOC)crates/nostrweet-cli/src/main.rs
async fn post_user_to_nostr(
data_dir: &Path,
mnemonic: &str,
username: &str,
relays: &[String],
blossom_servers: &[String],
force: bool,
skip_profiles: bool,
) -> Result<()> {
let username = parse_username(username)?;
let storage = FileStorage::new(data_dir)?;
let summaries = storage.list_tweets().await?;
let mut referenced_users = HashSet::new();
let mut matched = 0usize;
for summary in summaries {
let tweet = summary.tweet;
if tweet.author.username.normalized() != username.normalized() {
continue;
}
matched += 1;
if !skip_profiles {
referenced_users.extend(collect_usernames_from_tweet(&tweet));
}
post_tweet_to_nostr(
data_dir,
None,
mnemonic,
tweet.id.as_str(),
relays,
blossom_servers,
force,
true,
)
.await?;
}
ensure!(
matcpost_profile_to_nostr function · rust · L1995-L2017 (23 LOC)crates/nostrweet-cli/src/main.rs
async fn post_profile_to_nostr(
data_dir: &Path,
mnemonic: &str,
username: &str,
relays: &[String],
) -> Result<()> {
let username = parse_username(username)?;
let storage = FileStorage::new(data_dir)?;
let Some(user) = storage.load_latest_user_profile(&username).await? else {
bail!(
"No cached profile found for @{username}",
username = username.as_str()
);
};
let mnemonic = MnemonicPhrase::parse(mnemonic)?;
let keys = derive_keys_for_user(&user.id, &mnemonic)?;
let adapter = NostrSdkAdapter::new(&keys, relays, data_dir).await?;
let metadata = build_profile_metadata(&user, &username);
let _ = adapter.publish_profile(metadata, &keys).await?;
Ok(())
}update_relay_list function · rust · L2018-L2026 (9 LOC)crates/nostrweet-cli/src/main.rs
async fn update_relay_list(mnemonic: &str, relays: &[String]) -> Result<()> {
let mnemonic = MnemonicPhrase::parse(mnemonic)?;
let user_id = UserId::parse("0")?;
let keys = derive_keys_for_user(&user_id, &mnemonic)?;
let adapter = NostrSdkAdapter::new(&keys, relays, Path::new(".")).await?;
adapter.update_relay_list(relays, &keys).await?;
Ok(())
}show_tweet function · rust · L2027-L2083 (57 LOC)crates/nostrweet-cli/src/main.rs
async fn show_tweet(
data_dir: &Path,
bearer_token: Option<&str>,
mnemonic: Option<&str>,
cmd: ShowTweetCommand,
) -> Result<()> {
let tweet_id = TweetId::parse(&cmd.tweet)
.with_context(|| format!("Failed to parse tweet ID from {}", cmd.tweet))?;
let tweet = load_or_fetch_tweet(data_dir, bearer_token, &tweet_id).await?;
let media_urls = extract_media_urls(&tweet);
let mnemonic = mnemonic.map(MnemonicPhrase::parse).transpose()?;
let mut resolver = NostrLinkResolver::new(Some(data_dir.to_path_buf()), mnemonic);
let (content, mentioned_pubkeys) =
format_tweet_as_nostr_content_with_mentions(&tweet, &media_urls, &mut resolver).await?;
let tags = build_nostr_event_tags(&tweet.id, &media_urls, &[], &mentioned_pubkeys)?;
let created_at = tweet.created_at.unix_timestamp().ok();
let keys = Keys::generate();
let draft = NostrEventDraft {
content: content.clone(),
tags,
created_at: created_at.map(|tnew function · rust · L2108-L2119 (12 LOC)crates/nostrweet-cli/src/main.rs
fn new() -> Self {
Self {
last_poll_time: None,
last_success_time: None,
last_profile_post_time: None,
profile_posted: false,
consecutive_failures: 0,
total_tweets_downloaded: 0,
total_tweets_posted: 0,
is_processing: false,
}
}Repobility · code-quality intelligence platform · https://repobility.com
next_poll_delay function · rust · L2120-L2128 (9 LOC)crates/nostrweet-cli/src/main.rs
fn next_poll_delay(&self, base_interval: u64) -> Duration {
if self.consecutive_failures > 0 {
let backoff_seconds = base_interval * 2_u64.pow(self.consecutive_failures.min(5));
return Duration::from_secs(backoff_seconds.min(3600));
}
Duration::from_secs(base_interval)
}new function · rust · L2158-L2164 (7 LOC)crates/nostrweet-cli/src/main.rs
fn new(requests_per_window: u32, window_seconds: u64) -> Self {
Self {
requests_per_window,
window_duration: Duration::from_secs(window_seconds),
request_times: std::collections::VecDeque::new(),
}
}wait_if_needed function · rust · L2165-L2188 (24 LOC)crates/nostrweet-cli/src/main.rs
async fn wait_if_needed(&mut self) {
let cutoff = Instant::now() - self.window_duration;
while let Some(&front) = self.request_times.front() {
if front < cutoff {
self.request_times.pop_front();
} else {
break;
}
}
if self.request_times.len() >= self.requests_per_window as usize
&& let Some(&oldest) = self.request_times.front()
{
let wait_until = oldest + self.window_duration;
let wait_duration = wait_until.saturating_duration_since(Instant::now());
if wait_duration > Duration::ZERO {
info!("Rate limit reached, waiting {wait_duration:?}");
time::sleep(wait_duration).await;
}
}
self.request_times.push_back(Instant::now());
}