Initial version of things: just reads the stream of messages and prints it out and that's that
This commit is contained in:
		
						commit
						13cef8786c
					
				| 
						 | 
					@ -0,0 +1 @@
 | 
				
			||||||
 | 
					/target
 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "nederlandskie"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					anyhow = "1.0.75"
 | 
				
			||||||
 | 
					atrium-api = "0.4.0"
 | 
				
			||||||
 | 
					atrium-xrpc = "0.3.0"
 | 
				
			||||||
 | 
					chrono = "0.4.26"
 | 
				
			||||||
 | 
					ciborium = "0.2.1"
 | 
				
			||||||
 | 
					futures = "0.3.28"
 | 
				
			||||||
 | 
					libipld-core = { version = "0.16.0", features = ["serde-codec"] }
 | 
				
			||||||
 | 
					rs-car = "0.4.1"
 | 
				
			||||||
 | 
					scooby = "0.4.0"
 | 
				
			||||||
 | 
					serde_ipld_dagcbor = "0.4.0"
 | 
				
			||||||
 | 
					sqlx = { version = "0.7.1", features = ["chrono"] }
 | 
				
			||||||
 | 
					tokio = { version = "1.32.0", features = ["full"] }
 | 
				
			||||||
 | 
					tokio-tungstenite = { version = "0.20.0", features = ["native-tls"] }
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,20 @@
 | 
				
			||||||
 | 
					use chrono::{DateTime, Utc};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Post {
 | 
				
			||||||
 | 
					    indexed_at: DateTime<Utc>,
 | 
				
			||||||
 | 
					    author_did: String,
 | 
				
			||||||
 | 
					    cid: String,
 | 
				
			||||||
 | 
					    uri: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct Profile {
 | 
				
			||||||
 | 
					    first_seen_at: DateTime<Utc>,
 | 
				
			||||||
 | 
					    did: String,
 | 
				
			||||||
 | 
					    handle: String,
 | 
				
			||||||
 | 
					    likely_country_of_living: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct SubscriptionState {
 | 
				
			||||||
 | 
					    service: String,
 | 
				
			||||||
 | 
					    cursor: i64,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,184 @@
 | 
				
			||||||
 | 
					use atrium_api::com::atproto::sync::subscribe_repos::{
 | 
				
			||||||
 | 
					    Commit, Handle, Info, Message, Migrate, Tombstone,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use libipld_core::ipld::Ipld;
 | 
				
			||||||
 | 
					use std::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// original definition:
 | 
				
			||||||
 | 
					//```
 | 
				
			||||||
 | 
					// export enum FrameType {
 | 
				
			||||||
 | 
					//   Message = 1,
 | 
				
			||||||
 | 
					//   Error = -1,
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					// export const messageFrameHeader = z.object({
 | 
				
			||||||
 | 
					//   op: z.literal(FrameType.Message), // Frame op
 | 
				
			||||||
 | 
					//   t: z.string().optional(), // Message body type discriminator
 | 
				
			||||||
 | 
					// })
 | 
				
			||||||
 | 
					// export type MessageFrameHeader = z.infer<typeof messageFrameHeader>
 | 
				
			||||||
 | 
					// export const errorFrameHeader = z.object({
 | 
				
			||||||
 | 
					//   op: z.literal(FrameType.Error),
 | 
				
			||||||
 | 
					// })
 | 
				
			||||||
 | 
					// export type ErrorFrameHeader = z.infer<typeof errorFrameHeader>
 | 
				
			||||||
 | 
					// ```
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq, Eq)]
 | 
				
			||||||
 | 
					enum FrameHeader {
 | 
				
			||||||
 | 
					    Message(Option<String>),
 | 
				
			||||||
 | 
					    Error,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TryFrom<Ipld> for FrameHeader {
 | 
				
			||||||
 | 
					    type Error = anyhow::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn try_from(value: Ipld) -> Result<Self, <FrameHeader as TryFrom<Ipld>>::Error> {
 | 
				
			||||||
 | 
					        if let Ipld::Map(map) = value {
 | 
				
			||||||
 | 
					            if let Some(Ipld::Integer(i)) = map.get("op") {
 | 
				
			||||||
 | 
					                match i {
 | 
				
			||||||
 | 
					                    1 => {
 | 
				
			||||||
 | 
					                        let t = if let Some(Ipld::String(s)) = map.get("t") {
 | 
				
			||||||
 | 
					                            Some(s.clone())
 | 
				
			||||||
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            None
 | 
				
			||||||
 | 
					                        };
 | 
				
			||||||
 | 
					                        return Ok(FrameHeader::Message(t));
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    -1 => return Ok(FrameHeader::Error),
 | 
				
			||||||
 | 
					                    _ => {}
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(anyhow::anyhow!("invalid frame type"))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub enum Frame {
 | 
				
			||||||
 | 
					    Message(Box<MessageFrame>),
 | 
				
			||||||
 | 
					    Error(ErrorFrame),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub struct MessageFrame {
 | 
				
			||||||
 | 
					    pub body: Message,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub struct ErrorFrame {
 | 
				
			||||||
 | 
					    // TODO
 | 
				
			||||||
 | 
					    // body: Value,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl TryFrom<&[u8]> for Frame {
 | 
				
			||||||
 | 
					    type Error = anyhow::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn try_from(value: &[u8]) -> Result<Self, <Frame as TryFrom<&[u8]>>::Error> {
 | 
				
			||||||
 | 
					        let mut cursor = Cursor::new(value);
 | 
				
			||||||
 | 
					        let (left, right) = match serde_ipld_dagcbor::from_reader::<Ipld, _>(&mut cursor) {
 | 
				
			||||||
 | 
					            Err(serde_ipld_dagcbor::DecodeError::TrailingData) => {
 | 
				
			||||||
 | 
					                value.split_at(cursor.position() as usize)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => {
 | 
				
			||||||
 | 
					                // TODO
 | 
				
			||||||
 | 
					                return Err(anyhow::anyhow!("invalid frame type"));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let ipld = serde_ipld_dagcbor::from_slice::<Ipld>(left)?;
 | 
				
			||||||
 | 
					        let header = FrameHeader::try_from(ipld)?;
 | 
				
			||||||
 | 
					        match header {
 | 
				
			||||||
 | 
					            FrameHeader::Message(t) => match t.as_deref() {
 | 
				
			||||||
 | 
					                Some("#commit") => Ok(Frame::Message(Box::new(MessageFrame {
 | 
				
			||||||
 | 
					                    body: Message::Commit(Box::new(serde_ipld_dagcbor::from_slice::<Commit>(
 | 
				
			||||||
 | 
					                        right,
 | 
				
			||||||
 | 
					                    )?)),
 | 
				
			||||||
 | 
					                }))),
 | 
				
			||||||
 | 
					                Some("#handle") => Ok(Frame::Message(Box::new(MessageFrame {
 | 
				
			||||||
 | 
					                    body: Message::Handle(Box::new(serde_ipld_dagcbor::from_slice::<Handle>(
 | 
				
			||||||
 | 
					                        right,
 | 
				
			||||||
 | 
					                    )?)),
 | 
				
			||||||
 | 
					                }))),
 | 
				
			||||||
 | 
					                Some("#info") => Ok(Frame::Message(Box::new(MessageFrame {
 | 
				
			||||||
 | 
					                    body: Message::Info(Box::new(serde_ipld_dagcbor::from_slice::<Info>(right)?)),
 | 
				
			||||||
 | 
					                }))),
 | 
				
			||||||
 | 
					                Some("#migrate") => Ok(Frame::Message(Box::new(MessageFrame {
 | 
				
			||||||
 | 
					                    body: Message::Migrate(Box::new(serde_ipld_dagcbor::from_slice::<Migrate>(
 | 
				
			||||||
 | 
					                        right,
 | 
				
			||||||
 | 
					                    )?)),
 | 
				
			||||||
 | 
					                }))),
 | 
				
			||||||
 | 
					                Some("#tombstone") => Ok(Frame::Message(Box::new(MessageFrame {
 | 
				
			||||||
 | 
					                    body: Message::Tombstone(Box::new(
 | 
				
			||||||
 | 
					                        serde_ipld_dagcbor::from_slice::<Tombstone>(right)?,
 | 
				
			||||||
 | 
					                    )),
 | 
				
			||||||
 | 
					                }))),
 | 
				
			||||||
 | 
					                _ => {
 | 
				
			||||||
 | 
					                    let tag = t.as_deref();
 | 
				
			||||||
 | 
					                    Err(anyhow::anyhow!("frame not implemented: tag={tag:?}"))
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            FrameHeader::Error => Ok(Frame::Error(ErrorFrame {})),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod tests {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn serialized_data(s: &str) -> Vec<u8> {
 | 
				
			||||||
 | 
					        assert!(s.len() % 2 == 0);
 | 
				
			||||||
 | 
					        let b2u = |b: u8| match b {
 | 
				
			||||||
 | 
					            b'0'..=b'9' => b - b'0',
 | 
				
			||||||
 | 
					            b'a'..=b'f' => b - b'a' + 10,
 | 
				
			||||||
 | 
					            _ => unreachable!(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        s.as_bytes()
 | 
				
			||||||
 | 
					            .chunks(2)
 | 
				
			||||||
 | 
					            .map(|b| (b2u(b[0]) << 4) + b2u(b[1]))
 | 
				
			||||||
 | 
					            .collect()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn deserialize_message_frame_header() {
 | 
				
			||||||
 | 
					        // {"op": 1, "t": "#commit"}
 | 
				
			||||||
 | 
					        let data = serialized_data("a2626f700161746723636f6d6d6974");
 | 
				
			||||||
 | 
					        let ipld = serde_ipld_dagcbor::from_slice::<Ipld>(&data).expect("failed to deserialize");
 | 
				
			||||||
 | 
					        let result = FrameHeader::try_from(ipld);
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            result.expect("failed to deserialize"),
 | 
				
			||||||
 | 
					            FrameHeader::Message(Some(String::from("#commit")))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn deserialize_error_frame_header() {
 | 
				
			||||||
 | 
					        // {"op": -1}
 | 
				
			||||||
 | 
					        let data = serialized_data("a1626f7020");
 | 
				
			||||||
 | 
					        let ipld = serde_ipld_dagcbor::from_slice::<Ipld>(&data).expect("failed to deserialize");
 | 
				
			||||||
 | 
					        let result = FrameHeader::try_from(ipld);
 | 
				
			||||||
 | 
					        assert_eq!(result.expect("failed to deserialize"), FrameHeader::Error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn deserialize_invalid_frame_header() {
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // {"op": 2, "t": "#commit"}
 | 
				
			||||||
 | 
					            let data = serialized_data("a2626f700261746723636f6d6d6974");
 | 
				
			||||||
 | 
					            let ipld =
 | 
				
			||||||
 | 
					                serde_ipld_dagcbor::from_slice::<Ipld>(&data).expect("failed to deserialize");
 | 
				
			||||||
 | 
					            let result = FrameHeader::try_from(ipld);
 | 
				
			||||||
 | 
					            assert_eq!(
 | 
				
			||||||
 | 
					                result.expect_err("must be failed").to_string(),
 | 
				
			||||||
 | 
					                "invalid frame type"
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            // {"op": -2}
 | 
				
			||||||
 | 
					            let data = serialized_data("a1626f7021");
 | 
				
			||||||
 | 
					            let ipld =
 | 
				
			||||||
 | 
					                serde_ipld_dagcbor::from_slice::<Ipld>(&data).expect("failed to deserialize");
 | 
				
			||||||
 | 
					            let result = FrameHeader::try_from(ipld);
 | 
				
			||||||
 | 
					            assert_eq!(
 | 
				
			||||||
 | 
					                result.expect_err("must be failed").to_string(),
 | 
				
			||||||
 | 
					                "invalid frame type"
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,12 @@
 | 
				
			||||||
 | 
					mod database;
 | 
				
			||||||
 | 
					mod frames;
 | 
				
			||||||
 | 
					mod streaming;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::streaming::start_stream;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::main]
 | 
				
			||||||
 | 
					async fn main() -> Result<(), Box<dyn std::error::Error>> {
 | 
				
			||||||
 | 
					    start_stream().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,96 @@
 | 
				
			||||||
 | 
					use anyhow::Result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::frames::Frame;
 | 
				
			||||||
 | 
					use anyhow::anyhow;
 | 
				
			||||||
 | 
					use atrium_api::app::bsky::feed::post::Record;
 | 
				
			||||||
 | 
					use atrium_api::com::atproto::sync::subscribe_repos::Commit;
 | 
				
			||||||
 | 
					use atrium_api::com::atproto::sync::subscribe_repos::Message;
 | 
				
			||||||
 | 
					use futures::StreamExt;
 | 
				
			||||||
 | 
					use tokio_tungstenite::{connect_async, tungstenite};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn start_stream() -> Result<()> {
 | 
				
			||||||
 | 
					    let (mut stream, _) =
 | 
				
			||||||
 | 
					        connect_async("wss://bsky.social/xrpc/com.atproto.sync.subscribeRepos").await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while let Some(Ok(tungstenite::Message::Binary(message))) = stream.next().await {
 | 
				
			||||||
 | 
					        let commit = match parse_commit_message(&message) {
 | 
				
			||||||
 | 
					            Ok(Some(commit)) => commit,
 | 
				
			||||||
 | 
					            Ok(None) => continue,
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                println!("Couldn't parse commit: {:?}", e);
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let post_messages = extract_post_messages(&commit).await;
 | 
				
			||||||
 | 
					        match post_messages {
 | 
				
			||||||
 | 
					            Ok(post_messages) => {
 | 
				
			||||||
 | 
					                if !post_messages.is_empty() {
 | 
				
			||||||
 | 
					                    println!("{:?}", post_messages);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Err(e) => {
 | 
				
			||||||
 | 
					                println!("Coudln't extract post messages: {:?}", e);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn parse_commit_message(message: &[u8]) -> Result<Option<Commit>> {
 | 
				
			||||||
 | 
					    match Frame::try_from(message)? {
 | 
				
			||||||
 | 
					        Frame::Message(message) => match message.body {
 | 
				
			||||||
 | 
					            Message::Commit(commit) => Ok(Some(*commit)),
 | 
				
			||||||
 | 
					            _ => Ok(None),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        Frame::Error(err) => panic!("Frame error: {err:?}"),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					enum Action {
 | 
				
			||||||
 | 
					    Create,
 | 
				
			||||||
 | 
					    Delete,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					struct PostMessage {
 | 
				
			||||||
 | 
					    action: Action,
 | 
				
			||||||
 | 
					    author_did: String,
 | 
				
			||||||
 | 
					    cid: String,
 | 
				
			||||||
 | 
					    uri: String,
 | 
				
			||||||
 | 
					    languages: Vec<String>,
 | 
				
			||||||
 | 
					    text: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn extract_post_messages(commit: &Commit) -> Result<Vec<PostMessage>> {
 | 
				
			||||||
 | 
					    let mut posts = Vec::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let (items, _) = rs_car::car_read_all(&mut commit.blocks.as_slice(), true).await?;
 | 
				
			||||||
 | 
					    for op in &commit.ops {
 | 
				
			||||||
 | 
					        let collection = op.path.split('/').next().expect("op.path is empty");
 | 
				
			||||||
 | 
					        if (op.action != "create" && op.action != "delete") || collection != "app.bsky.feed.post" {
 | 
				
			||||||
 | 
					            continue;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some((_, item)) = items.iter().find(|(cid, _)| Some(*cid) == op.cid) {
 | 
				
			||||||
 | 
					            let record: Record = ciborium::from_reader(&mut item.as_slice())?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            posts.push(PostMessage {
 | 
				
			||||||
 | 
					                action: if op.action == "create" {
 | 
				
			||||||
 | 
					                    Action::Create
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    Action::Delete
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                languages: record.langs.unwrap_or_else(Vec::new),
 | 
				
			||||||
 | 
					                text: record.text,
 | 
				
			||||||
 | 
					                author_did: commit.repo.clone(),
 | 
				
			||||||
 | 
					                cid: op.cid.expect("cid is not there, what").to_string(),
 | 
				
			||||||
 | 
					                uri: format!("at://{}/{}", commit.repo, op.path),
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(posts)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue