Wire up enough to connect to Discord
parent
fbc6270a0a
commit
e806786a1c
|
@ -16,7 +16,8 @@ pub struct ChatMessage {
|
|||
pub origin: ChatReference,
|
||||
pub author: ChatAuthor,
|
||||
pub content: ChatMessageContent,
|
||||
// TODO: Attachments
|
||||
pub attachments: Vec<()>,
|
||||
pub replying: Option<ChatReference>,
|
||||
}
|
||||
|
||||
pub mod event;
|
||||
|
|
|
@ -9,3 +9,5 @@ tracing-subscriber = { version = "0.3.10", features = ["env-filter"] }
|
|||
color-eyre = "0.6.1"
|
||||
phoebe = { path = "../phoebe" }
|
||||
phoebe-discord = { path = "../services/phoebe-discord" }
|
||||
tracing = "0.1.33"
|
||||
futures = "0.3.21"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use color_eyre::Result;
|
||||
use tracing::info;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use phoebe::service::Service;
|
||||
|
@ -12,12 +13,24 @@ async fn main() -> Result<()> {
|
|||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let (tx, rx) = tokio::sync::broadcast::channel(512);
|
||||
let (tx, _) = tokio::sync::broadcast::channel(512);
|
||||
|
||||
let db = phoebe::open_core_db().await?;
|
||||
let services: Vec<Box<dyn Service>> = vec![Box::new(
|
||||
let services: Vec<Box<dyn Service + Send + Sync>> = vec![Box::new(
|
||||
phoebe_discord::setup(db.clone(), tx.clone()).await?,
|
||||
)];
|
||||
|
||||
let handles = services.into_iter().map(|mut srv| {
|
||||
let mut rx = tx.subscribe();
|
||||
tokio::spawn(async move {
|
||||
info!("Handling events for {}…", srv.get_service_tag());
|
||||
while let Ok(event) = rx.recv().await {
|
||||
srv.handle_chat_event(&event).await;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let _ = futures::future::join_all(handles).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use mid_chat::event::ChatEvent;
|
||||
pub use mid_chat;
|
||||
|
||||
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 type ChatEventSender = tokio::sync::broadcast::Sender<mid_chat::event::ChatEvent>;
|
||||
pub type ChatEventReceiver = tokio::sync::broadcast::Receiver<mid_chat::event::ChatEvent>;
|
||||
|
||||
pub async fn open_core_db() -> sqlx::Result<sqlx::SqlitePool> {
|
||||
let db = db::open("main").await?;
|
||||
|
|
|
@ -5,7 +5,8 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
phoebe = { path = "../../phoebe" }
|
||||
serenity = "0.10.10"
|
||||
serenity = { version = "0.10.10", default-features = false, features = ["builder", "cache", "client", "gateway", "model", "http", "utils", "rustls_backend"] }
|
||||
sqlx = { version = "0.5", features = ["runtime-tokio-native-tls", "sqlite"] }
|
||||
tracing = "0.1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
discord_message_format = { git = "https://git.lavender.software/charlotte/discord-message-format.git" }
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
use discord_message_format::DiscordComponent;
|
||||
use phoebe::mid_chat::{ChatContentComponent, ChatContentComponent::*, ChatMessageContent};
|
||||
|
||||
pub fn convert(discord_message: &[DiscordComponent<'_>]) -> ChatMessageContent {
|
||||
discord_message
|
||||
.iter()
|
||||
.map(discord_to_mid)
|
||||
.collect::<ChatMessageContent>()
|
||||
}
|
||||
|
||||
pub fn format(message_content: &[ChatContentComponent]) -> String {
|
||||
message_content.iter().map(mid_to_discord).collect()
|
||||
}
|
||||
|
||||
fn discord_to_mid(discord: &DiscordComponent<'_>) -> ChatContentComponent {
|
||||
match discord {
|
||||
DiscordComponent::Plain(text) => Plain(text.to_string()),
|
||||
DiscordComponent::Literal(char) => Plain(char.to_string()),
|
||||
DiscordComponent::Link(link) => Link {
|
||||
target: link.to_string(),
|
||||
text: vec![Plain(link.to_string())],
|
||||
},
|
||||
|
||||
DiscordComponent::Bold(content) => Bold(convert(content)),
|
||||
DiscordComponent::Italic(content) => Italic(convert(content)),
|
||||
DiscordComponent::Strikethrough(content) => Strikethrough(convert(content)),
|
||||
DiscordComponent::Underline(content) => Underline(convert(content)),
|
||||
|
||||
DiscordComponent::Code(code) => Code(code.to_string()),
|
||||
DiscordComponent::CodeBlock { lang, source } => CodeBlock {
|
||||
lang: lang.map(|s| s.to_string()),
|
||||
source: source.to_string(),
|
||||
},
|
||||
|
||||
DiscordComponent::Spoiler(content) => Spoiler {
|
||||
reason: None,
|
||||
content: convert(content),
|
||||
},
|
||||
|
||||
DiscordComponent::LineBreak => HardBreak,
|
||||
DiscordComponent::Quote(content) => BlockQuote(convert(content)),
|
||||
}
|
||||
}
|
||||
|
||||
fn mid_to_discord(component: &ChatContentComponent) -> String {
|
||||
match component {
|
||||
Plain(text) => text.to_string(), // TODO: Escape
|
||||
|
||||
Link { target, text } => {
|
||||
let formatted_text = format(text);
|
||||
|
||||
// TODO: Maybe tolerate a missing http(s) URL scheme?
|
||||
if &formatted_text == target {
|
||||
formatted_text
|
||||
} else {
|
||||
format!("{} ({})", formatted_text, target)
|
||||
}
|
||||
}
|
||||
|
||||
Italic(inner) => format!("*{}*", format(inner)),
|
||||
Bold(inner) => format!("**{}**", format(inner)),
|
||||
Strikethrough(inner) => format!("~~{}~~", format(inner)),
|
||||
Underline(inner) => format!("__{}__", format(inner)),
|
||||
|
||||
Code(code) => format!("`{}`", code), // TODO: Double-grave delimiting when code contains '`'
|
||||
CodeBlock { lang, source } => {
|
||||
format!(
|
||||
"```{}\n{}\n```",
|
||||
lang.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
source.to_string()
|
||||
)
|
||||
}
|
||||
|
||||
Spoiler { content, .. } => {
|
||||
format!("||{}||", format(content))
|
||||
} // TODO: Spoiler reason
|
||||
|
||||
HardBreak => "\n".to_string(),
|
||||
BlockQuote(inner) => format(inner)
|
||||
.lines()
|
||||
.map(|l| format!("> {}\n", l))
|
||||
.collect(),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
use phoebe::{mid_chat::*, prelude::*};
|
||||
use serenity::{
|
||||
client::{Context, EventHandler},
|
||||
model::prelude::*,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::discord_reference;
|
||||
|
||||
pub struct DiscordHandler {
|
||||
pub core_db: SqlitePool,
|
||||
pub discord_media_db: SqlitePool,
|
||||
pub chat_event_tx: ChatEventSender,
|
||||
pub ctx_tx: tokio::sync::mpsc::UnboundedSender<Context>,
|
||||
}
|
||||
|
||||
impl DiscordHandler {
|
||||
async fn get_author(&self, ctx: &Context, message: &Message) -> ChatAuthor {
|
||||
let display_name = message
|
||||
.author_nick(ctx)
|
||||
.await
|
||||
.unwrap_or_else(|| message.author.name.clone());
|
||||
|
||||
async fn tag_color(ctx: &Context, message: &Message) -> Option<[u8; 3]> {
|
||||
let color = message.member(ctx).await.ok()?.colour(ctx).await?;
|
||||
Some([color.r(), color.b(), color.g()])
|
||||
}
|
||||
|
||||
let display_color = tag_color(ctx, message).await;
|
||||
|
||||
ChatAuthor {
|
||||
reference: discord_reference(message.author.id),
|
||||
display_name,
|
||||
display_color,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for DiscordHandler {
|
||||
async fn ready(&self, ctx: Context, _ready: Ready) {
|
||||
debug!("Discord signalled ready!");
|
||||
let _ = self.ctx_tx.send(ctx);
|
||||
}
|
||||
|
||||
async fn message(&self, ctx: Context, message: Message) {
|
||||
let origin = ChatReference {
|
||||
service: "discord",
|
||||
id: message.id.to_string(),
|
||||
};
|
||||
|
||||
let author = self.get_author(&ctx, &message).await;
|
||||
|
||||
let content = discord_message_format::parse(&message.content);
|
||||
let content = super::chat_conv::convert(&content);
|
||||
|
||||
let replies_to = message.referenced_message.as_ref().map(|m| ChatReference {
|
||||
service: "discord",
|
||||
id: m.id.to_string(),
|
||||
});
|
||||
|
||||
let chat_message = ChatMessage {
|
||||
origin,
|
||||
author,
|
||||
content,
|
||||
attachments: vec![],
|
||||
replying: replies_to,
|
||||
};
|
||||
|
||||
let _ = self
|
||||
.chat_event_tx
|
||||
.send(ChatEvent::NewMessage(chat_message))
|
||||
.expect("Failed to dispatch incoming Discord chat message");
|
||||
}
|
||||
}
|
|
@ -1,40 +1,30 @@
|
|||
use phoebe::prelude::*;
|
||||
use serenity::{
|
||||
client::{Context, EventHandler},
|
||||
model::prelude::*,
|
||||
prelude::*,
|
||||
Client,
|
||||
};
|
||||
use phoebe::{mid_chat, prelude::*};
|
||||
use serenity::{client::Context, Client};
|
||||
use tracing::{debug, info};
|
||||
|
||||
pub struct DiscordService {
|
||||
discord_client: Client,
|
||||
discord_ctx: Context,
|
||||
}
|
||||
mod chat_conv;
|
||||
mod handler;
|
||||
|
||||
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 fn discord_reference(id: impl ToString) -> mid_chat::ChatReference {
|
||||
mid_chat::ChatReference {
|
||||
service: "discord",
|
||||
id: id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiscordService {
|
||||
pub discord_ctx: Context,
|
||||
}
|
||||
|
||||
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 (ctx_tx, mut ctx_rx) = tokio::sync::mpsc::unbounded_channel::<Context>();
|
||||
|
||||
let discord_handler = DiscordHandler {
|
||||
let discord_handler = handler::DiscordHandler {
|
||||
core_db,
|
||||
discord_media_db,
|
||||
chat_event_tx: tx,
|
||||
|
@ -45,17 +35,22 @@ pub async fn setup(core_db: SqlitePool, tx: ChatEventSender) -> Result<DiscordSe
|
|||
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)
|
||||
|
||||
let mut client = Client::builder(&discord_token)
|
||||
.event_handler(discord_handler)
|
||||
.await?;
|
||||
|
||||
tokio::spawn(async move {
|
||||
client
|
||||
.start()
|
||||
.await
|
||||
.expect("Failed to start Discord client")
|
||||
});
|
||||
|
||||
let discord_ctx = ctx_rx.recv().await.expect("Couldn't get Discord context");
|
||||
debug!("Logged in!");
|
||||
|
||||
Ok(DiscordService {
|
||||
discord_client: client,
|
||||
discord_ctx,
|
||||
})
|
||||
Ok(DiscordService { discord_ctx })
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
Loading…
Reference in New Issue