oxipage-ext-activity 0.6.0

GitHub 활동 스트림 확장 — Events API 폴링 및 webhook 수신 (doc/02 §2.8)
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, sqlx::FromRow)]
pub struct ActivityEvent {
    pub id: i64,
    pub repo_full_name: String,
    pub event_type: String,
    pub summary: String,
    pub url: String,
    pub occurred_at: String,
    pub synced_at: String,
    pub created_at: String,
}

#[derive(Debug, Clone)]
pub struct ActivityEventInput {
    pub repo_full_name: String,
    pub event_type: String,
    pub summary: String,
    pub url: String,
    pub occurred_at: String,
}

#[derive(Debug, Default, Deserialize)]
pub struct ListQuery {
    pub repo: Option<String>,
    pub limit: Option<i64>,
}

#[derive(Debug, Deserialize)]
pub struct GithubEvent {
    #[serde(default)]
    pub id: Option<String>,
    #[serde(rename = "type")]
    pub kind: String,
    pub repo: GithubRepo,
    pub created_at: String,
    #[serde(default)]
    pub payload: serde_json::Value,
}

#[derive(Debug, Deserialize)]
pub struct GithubRepo {
    pub name: String,
    #[serde(default)]
    pub url: Option<String>,
}

impl GithubEvent {
    pub fn into_input(self) -> ActivityEventInput {
        let event_type = normalize_event_type(&self.kind);
        let url = event_url(&self.payload)
            .or_else(|| self.repo.url.as_deref().map(api_repo_url_to_html))
            .unwrap_or_else(|| format!("https://github.com/{}", self.repo.name));
        ActivityEventInput {
            summary: summary(&event_type, &self.repo.name),
            repo_full_name: self.repo.name,
            event_type,
            url,
            occurred_at: self.created_at,
        }
    }
}

fn normalize_event_type(kind: &str) -> String {
    kind.strip_suffix("Event")
        .unwrap_or(kind)
        .chars()
        .enumerate()
        .flat_map(|(index, c)| {
            if index > 0 && c.is_ascii_uppercase() {
                vec!['_', c.to_ascii_lowercase()]
            } else {
                vec![c.to_ascii_lowercase()]
            }
        })
        .collect()
}

fn summary(event_type: &str, repo: &str) -> String {
    let action = match event_type {
        "push" => "push to",
        "pull_request" => "pull request on",
        "release" => "release on",
        "issues" => "issue on",
        "watch" | "star" => "starred",
        _ => event_type,
    };
    format!("{action} {repo}")
}

fn event_url(payload: &serde_json::Value) -> Option<String> {
    payload
        .get("html_url")
        .and_then(serde_json::Value::as_str)
        .or_else(|| {
            payload
                .pointer("/pull_request/html_url")
                .and_then(serde_json::Value::as_str)
        })
        .or_else(|| {
            payload
                .pointer("/release/html_url")
                .and_then(serde_json::Value::as_str)
        })
        .or_else(|| {
            payload
                .pointer("/issue/html_url")
                .and_then(serde_json::Value::as_str)
        })
        .map(str::to_owned)
}

fn api_repo_url_to_html(url: &str) -> String {
    url.replacen("https://api.github.com/repos/", "https://github.com/", 1)
}