Initial commit

This commit is contained in:
Charlotte Som 2022-04-08 17:52:59 +01:00
commit fbc6270a0a
23 changed files with 293 additions and 0 deletions

12
.editorconfig Normal file
View file

@ -0,0 +1,12 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true
[*.rs]
indent_size = 4

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/Cargo.lock

7
Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[workspace]
members = [
"phoebe",
"phoebe-main",
"mid-chat",
"services/*"
]

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# phoebe
bridgers [primarily, discord ↔ matrix]
A [Charlotte Som](https://som.codes/) project.
## Architecture
- `mid-chat` - An intermediate representation for chat messages. Best-effort common denomination
- `services/*` - Handling for individual chat services & conversion to and from the common-denominator chat message IR
- `phoebe` - Main: Database, message dispatch, service orchestration, etc

2
data/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!/.gitignore

6
mid-chat/Cargo.toml Normal file
View file

@ -0,0 +1,6 @@
[package]
name = "mid-chat"
version = "0.1.0"
edition = "2021"
[dependencies]

30
mid-chat/src/content.rs Normal file
View file

@ -0,0 +1,30 @@
pub type ChatMessageContent = Vec<ChatContentComponent>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChatContentComponent {
Plain(String),
Link {
target: String,
text: ChatMessageContent,
},
Italic(ChatMessageContent),
Bold(ChatMessageContent),
Strikethrough(ChatMessageContent),
Underline(ChatMessageContent),
Code(String),
CodeBlock {
lang: Option<String>,
source: String,
},
Spoiler {
reason: Option<String>,
content: ChatMessageContent,
},
HardBreak,
BlockQuote(ChatMessageContent),
}

6
mid-chat/src/event.rs Normal file
View file

@ -0,0 +1,6 @@
use crate::ChatMessage;
#[derive(Debug, Clone)]
pub enum ChatEvent {
NewMessage(ChatMessage),
}

22
mid-chat/src/lib.rs Normal file
View file

@ -0,0 +1,22 @@
pub mod reference;
pub use reference::*;
#[derive(Debug, Clone)]
pub struct ChatAuthor {
pub reference: ChatReference,
pub display_name: String,
pub display_color: Option<[u8; 3]>,
}
mod content;
pub use content::*;
#[derive(Debug, Clone)]
pub struct ChatMessage {
pub origin: ChatReference,
pub author: ChatAuthor,
pub content: ChatMessageContent,
// TODO: Attachments
}
pub mod event;

View file

@ -0,0 +1,5 @@
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct ChatReference {
pub service: &'static str,
pub id: String,
}

11
phoebe-main/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "phoebe-main"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
tracing-subscriber = { version = "0.3.10", features = ["env-filter"] }
color-eyre = "0.6.1"
phoebe = { path = "../phoebe" }
phoebe-discord = { path = "../services/phoebe-discord" }

23
phoebe-main/src/main.rs Normal file
View file

@ -0,0 +1,23 @@
use color_eyre::Result;
use tracing_subscriber::EnvFilter;
use phoebe::service::Service;
#[tokio::main]
async fn main() -> Result<()> {
color_eyre::install()?;
tracing_subscriber::fmt()
.with_target(true)
.with_env_filter(EnvFilter::from_default_env())
.init();
let (tx, rx) = tokio::sync::broadcast::channel(512);
let db = phoebe::open_core_db().await?;
let services: Vec<Box<dyn Service>> = vec![Box::new(
phoebe_discord::setup(db.clone(), tx.clone()).await?,
)];
Ok(())
}

1
phoebe/.env Normal file
View file

@ -0,0 +1 @@
DATABASE_URL="sqlite://PHOEBE_DB_ROOT/main.db"

13
phoebe/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "phoebe"
version = "0.1.0"
edition = "2021"
authors = ["videogame hacker <half-kh-hacker@hackery.site>"]
[dependencies]
mid-chat = { path = "../mid-chat" }
tokio = { version = "1", features = ["full"] }
sqlx = { version = "0.5", features = ["runtime-tokio-native-tls", "sqlite"] }
tracing = "0.1"
async-trait = "0.1.53"
eyre = "0.6.8"

26
phoebe/src/db.rs Normal file
View file

@ -0,0 +1,26 @@
use sqlx::{sqlite::SqliteConnectOptions, SqlitePool};
use tokio::sync::OnceCell;
use tracing::debug;
static PHOEBE_DB_ROOT: OnceCell<String> = OnceCell::const_new();
async fn get_db_root() -> &'static str {
PHOEBE_DB_ROOT
.get_or_init(|| async {
std::env::var("PHOEBE_DB_ROOT")
.expect("PHOEBE_DB_ROOT environment variable was not set!")
})
.await
}
async fn create_or_open_sqlite(url: &str) -> sqlx::Result<SqlitePool> {
let options = url.parse::<SqliteConnectOptions>()?.create_if_missing(true);
SqlitePool::connect_with(options).await
}
pub async fn open(name: &str) -> sqlx::Result<SqlitePool> {
debug!("Opening {name} database…");
let db_root = get_db_root().await;
let db_url = format!("sqlite://{}/{}.db", db_root, name);
create_or_open_sqlite(&db_url).await
}

14
phoebe/src/lib.rs Normal file
View file

@ -0,0 +1,14 @@
use mid_chat::event::ChatEvent;
pub mod db;
pub mod prelude;
pub mod service;
pub type ChatEventSender = tokio::sync::broadcast::Sender<ChatEvent>;
pub type ChatEventReceiver = tokio::sync::broadcast::Receiver<ChatEvent>;
pub async fn open_core_db() -> sqlx::Result<sqlx::SqlitePool> {
let db = db::open("main").await?;
sqlx::migrate!().run(&db).await?;
Ok(db)
}

6
phoebe/src/prelude.rs Normal file
View file

@ -0,0 +1,6 @@
pub use crate::{service::Service, ChatEventReceiver, ChatEventSender};
pub use async_trait::async_trait;
pub use eyre::Result;
pub use mid_chat::event::ChatEvent;
pub use sqlx::SqlitePool;

7
phoebe/src/service.rs Normal file
View file

@ -0,0 +1,7 @@
use mid_chat::event::ChatEvent;
#[async_trait::async_trait]
pub trait Service {
fn get_service_tag(&self) -> &'static str;
async fn handle_chat_event(&mut self, event: &ChatEvent);
}

View file

@ -0,0 +1 @@
DATABASE_URL="sqlite://PHOEBE_DB_ROOT/discord_media.db"

View file

@ -0,0 +1,11 @@
[package]
name = "phoebe-discord"
version = "0.1.0"
edition = "2021"
[dependencies]
phoebe = { path = "../../phoebe" }
serenity = "0.10.10"
sqlx = { version = "0.5", features = ["runtime-tokio-native-tls", "sqlite"] }
tracing = "0.1"
tokio = { version = "1", features = ["full"] }

View file

@ -0,0 +1,70 @@
use phoebe::prelude::*;
use serenity::{
client::{Context, EventHandler},
model::prelude::*,
prelude::*,
Client,
};
use tracing::{debug, info};
pub struct DiscordService {
discord_client: Client,
discord_ctx: Context,
}
struct DiscordHandler {
core_db: SqlitePool,
discord_media_db: SqlitePool,
chat_event_tx: ChatEventSender,
ctx_tx: tokio::sync::mpsc::Sender<Context>,
}
#[async_trait]
impl EventHandler for DiscordHandler {
async fn ready(&self, ctx: Context, _data_about_bot: Ready) {
let _ = self.ctx_tx.send(ctx).await;
}
}
pub async fn setup(core_db: SqlitePool, tx: ChatEventSender) -> Result<DiscordService> {
info!("Setting up Discord service…");
let discord_media_db = phoebe::db::open("discord_media").await?;
sqlx::migrate!().run(&discord_media_db).await?;
let (ctx_tx, mut ctx_rx) = tokio::sync::mpsc::channel::<Context>(1);
let discord_handler = DiscordHandler {
core_db,
discord_media_db,
chat_event_tx: tx,
ctx_tx,
};
// TODO: Create a discord client
debug!("Logging in…");
let discord_token = std::env::var("PHOEBE_DISCORD_TOKEN")
.expect("PHOEBE_DISCORD_TOKEN environment variable was not set!");
let client = Client::builder(&discord_token)
.event_handler(discord_handler)
.await?;
let discord_ctx = ctx_rx.recv().await.expect("Couldn't get Discord context");
debug!("Logged in!");
Ok(DiscordService {
discord_client: client,
discord_ctx,
})
}
#[async_trait]
impl Service for DiscordService {
async fn handle_chat_event(&mut self, event: &ChatEvent) {
dbg!(event);
}
fn get_service_tag(&self) -> &'static str {
"discord"
}
}

View file

@ -0,0 +1,7 @@
[package]
name = "phoebe-matrix"
version = "0.1.0"
edition = "2021"
[dependencies]
phoebe = { path = "../../phoebe" }

View file