aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorGustav Sörnäs <gustav@sornas.net>2020-11-16 16:19:21 +0100
committerGitHub <noreply@github.com>2020-11-16 16:19:21 +0100
commita20e0162412ac4af6ef629e4b99654afad5a464a (patch)
tree0841535afce3070ab71591d56cb46a9d3e174aac /src
parentf771b73bcda915ce69db49f40473558ae567223f (diff)
parent96b9a8f66f62e64bc4907e12de0772392f7804c7 (diff)
downloadkodapa-a20e0162412ac4af6ef629e4b99654afad5a464a.tar.gz
Merge pull request #1 from lithekod/rust
Initial minimum viable bot
Diffstat (limited to 'src')
-rw-r--r--src/agenda.rs91
-rw-r--r--src/discord.rs126
-rw-r--r--src/main.rs21
-rw-r--r--src/slack.rs139
4 files changed, 377 insertions, 0 deletions
diff --git a/src/agenda.rs b/src/agenda.rs
new file mode 100644
index 0000000..d1a940d
--- /dev/null
+++ b/src/agenda.rs
@@ -0,0 +1,91 @@
+use serde::{
+ Deserialize,
+ Serialize,
+};
+use std::{
+ fmt,
+ fs,
+};
+use tokio::sync::mpsc;
+
+#[derive(Clone, Debug, Deserialize, Serialize)]
+pub struct AgendaPoint {
+ title: String,
+ adder: String,
+}
+
+impl fmt::Display for AgendaPoint {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{} ({})", self.title, self.adder)
+ }
+}
+
+impl AgendaPoint {
+ pub fn to_add_message(&self) -> String {
+ format!("'{}' added by {}", self.title, self.adder)
+ }
+
+ fn to_add_message_response(&self) -> String {
+ //TODO should add a reaction instead
+ format!("Added '{}'", self.title)
+ }
+}
+
+#[derive(Deserialize, Serialize)]
+pub struct Agenda {
+ points: Vec<AgendaPoint>,
+}
+
+impl Agenda {
+ fn write(&self) {
+ fs::write(std::path::Path::new("agenda.json"),
+ serde_json::to_string_pretty(&self).expect("Can't serialize agenda"))
+ .expect("Can't write agenda.json");
+ }
+}
+
+pub enum ParseError {
+ NoSuchCommand,
+}
+
+pub fn parse_message(
+ message: &str,
+ sender: &str,
+ point_sender: &mpsc::UnboundedSender<AgendaPoint>
+) -> Result<Option<String>, ParseError> {
+ if message.starts_with("!add ") {
+ let mut agenda = read_agenda();
+ let agenda_point = AgendaPoint {
+ title: message[5..].to_string(),
+ adder: sender.to_string(),
+ };
+ point_sender.send(agenda_point.clone()).unwrap();
+ let response = agenda_point.to_add_message_response();
+ agenda.points.push(agenda_point);
+ agenda.write();
+ Ok(Some(response))
+ } else if message.starts_with("!agenda") {
+ Ok(Some(read_agenda()
+ .points
+ .iter()
+ .map(|p| p.to_string())
+ .collect::<Vec<_>>()
+ .join("\n")))
+ } else if message.starts_with("!clear") {
+ Agenda {
+ points: Vec::new(),
+ }.write();
+ Ok(None)
+ } else if message.starts_with("!help") {
+ Ok(Some("Available commands:\n```!add -- Add something\n!agenda -- Print the agenda\n!clear -- Remove all items\n!help```".to_string()))
+ } else {
+ Err(ParseError::NoSuchCommand)
+ }
+}
+
+fn read_agenda() -> Agenda {
+ serde_json::from_str::<Agenda>(
+ &fs::read_to_string("agenda.json")
+ .expect("Can't read agenda.json"))
+ .expect("Error parsing agenda.json")
+}
diff --git a/src/discord.rs b/src/discord.rs
new file mode 100644
index 0000000..0507da8
--- /dev/null
+++ b/src/discord.rs
@@ -0,0 +1,126 @@
+use crate::agenda::{
+ parse_message,
+ AgendaPoint
+};
+
+use discord::{
+ model::{
+ ChannelId,
+ Event,
+ PossibleServer,
+ },
+ Discord,
+ Error,
+};
+use futures::join;
+use std::sync::{
+ Arc,
+ Mutex,
+};
+use tokio::{
+ sync::mpsc,
+ task::{
+ spawn,
+ spawn_blocking,
+ },
+};
+
+const TOKEN: Option<&str> = None;
+const CHANNEL: Option<ChannelId> = None;
+
+pub async fn handle(
+ sender: mpsc::UnboundedSender<AgendaPoint>,
+ receiver: mpsc::UnboundedReceiver<AgendaPoint>,
+) {
+ println!("Setting up Discord");
+
+ let token = std::env::var("DISCORD_API_TOKEN").unwrap_or_else(|_| TOKEN.expect("Missing Discord token").to_string());
+ let client = Discord::from_bot_token(&token);
+
+ if let Ok(client) = client {
+ let (connection, _) = client.connect().expect("Discord connect failed"); //TODO
+ let our_id = client.get_current_user().unwrap().id;
+ let client = Arc::new(Mutex::new(client));
+
+ let channel = match std::env::var("DISCORD_CHANNEL") {
+ Ok(channel) => Some(ChannelId(channel.parse::<u64>().unwrap())),
+ Err(_) => CHANNEL,
+ };
+
+ let (_, _) = join!( //TODO?
+ spawn(receive_from_slack(receiver, Arc::clone(&client), channel)),
+ spawn_blocking(move || receive_events(our_id, connection, sender, client, channel)),
+ );
+ }
+}
+
+fn receive_events(
+ _our_id: discord::model::UserId,
+ mut connection: discord::Connection,
+ sender: mpsc::UnboundedSender<AgendaPoint>,
+ client: Arc<Mutex<discord::Discord>>,
+ channel: Option<ChannelId>,
+) {
+ loop {
+ match connection.recv_event() {
+ Ok(Event::ServerCreate(server)) => {
+ if channel.is_none() {
+ if let PossibleServer::Online(server) = server {
+ println!("Discord channels in {}: {:#?}",
+ server.name,
+ server
+ .channels
+ .iter()
+ .map(|channel| format!("{}: {} ({:?})",
+ channel.name,
+ channel.id,
+ channel.kind))
+ .collect::<Vec<_>>());
+ }
+ }
+ }
+
+ Ok(Event::MessageCreate(message)) => {
+ if let Some(channel) = channel {
+ if channel == message.channel_id {
+ if let Ok(Some(s)) = parse_message(
+ &message.content,
+ &message.author.name,
+ &sender,
+ ) {
+ client.lock().unwrap().send_message(channel,
+ &s,
+ "",
+ false).unwrap();
+ }
+ }
+ }
+ }
+ Ok(_) => {}
+ Err(Error::Closed(code, body)) => {
+ println!("Discord closed with code {:?}: {}", code, body);
+ break;
+ }
+ Err(e) => {
+ println!("Discord error: {:?}", e);
+ }
+ }
+ }
+}
+
+async fn receive_from_slack(
+ mut receiver: mpsc::UnboundedReceiver<AgendaPoint>,
+ client: Arc<Mutex<discord::Discord>>,
+ channel: Option<ChannelId>
+) {
+ if let Some(channel) = channel {
+ while let Some(point) = receiver.recv().await {
+ println!("Discord received '{}'", point);
+ client.lock().unwrap().send_message(channel,
+ &point.to_add_message(),
+ "",
+ false).unwrap();
+ }
+ }
+
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..0bcf53c
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,21 @@
+mod agenda;
+mod discord;
+mod slack;
+
+use crate::agenda::AgendaPoint;
+use futures::join;
+use tokio::sync::mpsc;
+
+#[tokio::main]
+async fn main() {
+ println!("Hello, world!");
+
+ let (from_discord, to_slack) = mpsc::unbounded_channel::<AgendaPoint>();
+ let (from_slack, to_discord) = mpsc::unbounded_channel::<AgendaPoint>();
+
+ join!(
+ discord::handle(from_discord, to_discord),
+ slack::handle(from_slack, to_slack),
+ );
+}
+
diff --git a/src/slack.rs b/src/slack.rs
new file mode 100644
index 0000000..1c272cf
--- /dev/null
+++ b/src/slack.rs
@@ -0,0 +1,139 @@
+use crate::agenda::{
+ parse_message,
+ AgendaPoint
+};
+
+use futures::join;
+use slack::{
+ Event,
+ Message,
+};
+use tokio::{
+ sync::mpsc,
+ task::{
+ spawn,
+ spawn_blocking,
+ },
+};
+
+const TOKEN: Option<&str> = None;
+const CHANNEL: Option<&str> = None;
+
+struct Handler {
+ sender: mpsc::UnboundedSender<AgendaPoint>,
+ slack_sender: slack::Sender,
+ slack_channel: Option<String>,
+ print_channels: bool,
+}
+
+impl Handler {
+ fn new(
+ sender: mpsc::UnboundedSender<AgendaPoint>,
+ slack_sender: slack::Sender,
+ slack_channel: Option<String>,
+ ) -> Self {
+ Self {
+ sender,
+ slack_sender,
+ slack_channel: slack_channel.clone(),
+ print_channels: slack_channel.is_none()
+ }
+ }
+}
+
+impl slack::EventHandler for Handler {
+ fn on_event(&mut self, cli: &slack::RtmClient, event: slack::Event) {
+ match event {
+ Event::Hello => {
+ if self.print_channels {
+ println!("Slack channels found: {:#?}",
+ cli
+ .start_response()
+ .channels
+ .as_ref()
+ .and_then(|channels| {
+ Some(channels
+ .iter()
+ .map(|channel| format!("{}: {}",
+ channel.name.as_ref().unwrap_or(&"??".to_string()), //TODO &"".to_string() ?
+ channel.id.as_ref().unwrap_or(&"??".to_string()))) //TODO
+ .collect::<Vec<_>>())
+ }));
+ }
+ }
+ Event::Message(msg) => {
+ if let Some(channel) = &self.slack_channel {
+ match *msg {
+ Message::Standard(msg) => {
+ if msg.channel.is_some() && *channel == msg.channel.unwrap() { //TODO
+ if let Ok(Some(s)) = parse_message(
+ &msg.text.unwrap_or("".to_string()),
+ &msg.user.unwrap_or("??".to_string()),
+ &self.sender,
+ ) {
+ self.slack_sender.send_message(channel.as_str(), &s).unwrap();
+ }
+ }
+ }
+ _ => {} // message type
+ }
+ }
+ }
+ _ => {} // event type
+ }
+ }
+
+ fn on_close(&mut self, _cli: &slack::RtmClient) {}
+
+ fn on_connect(&mut self, _cli: &slack::RtmClient) {}
+}
+
+pub async fn handle(
+ sender: mpsc::UnboundedSender<AgendaPoint>,
+ receiver: mpsc::UnboundedReceiver<AgendaPoint>,
+) {
+ println!("Setting up Slack");
+
+ let token = std::env::var("SLACK_API_TOKEN").unwrap_or_else(|_| TOKEN.expect("Missing slack token").to_string());
+ let channel = match std::env::var("SLACK_CHANNEL") {
+ Ok(channel) => Some(channel),
+ Err(_) => match CHANNEL {
+ Some(channel) => Some(channel.to_string()),
+ None => None
+ }
+ };
+ let client = spawn_blocking(move || {
+ slack::RtmClient::login(&token).unwrap()
+ }).await.unwrap();
+
+ let mut handler = Handler::new(sender, client.sender().clone(), channel.clone());
+ let slack_sender = client.sender().clone();
+
+ let (_, _) = join!(
+ spawn_blocking(move || {
+ match client.run(&mut handler) {
+ Ok(_) => {}
+ Err(e) => {
+ println!("Error: {}", e)
+ }
+ }
+ }),
+ spawn(receive_from_discord(receiver, slack_sender, channel))
+ );
+}
+
+async fn receive_from_discord(
+ mut receiver: mpsc::UnboundedReceiver<AgendaPoint>,
+ sender: slack::Sender,
+ channel: Option<String>,
+) {
+ if let Some(channel) = channel {
+ while let Some(point) = receiver.recv().await {
+ //TODO Sending messages is very slow sometimes. Have seen delays
+ // from 5 up to 20(!) seconds.
+ sender.send_typing(&channel).unwrap();
+ sender.send_message(&channel, &point.to_add_message()).unwrap();
+ println!("Slack message sent");
+ }
+ }
+}