Initial commit
This commit is contained in:
commit
f29244514f
4 changed files with 2466 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
2150
Cargo.lock
generated
Normal file
2150
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "projbotv3"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = {version="1.21.2", features=["full"]}
|
||||
serenity = {version="0.11.5", features=["builder", "cache", "client", "framework", "gateway", "http", "model", "standard_framework", "utils", "rustls_backend", "voice"]}
|
||||
songbird = {version="0.3.0", features=["driver", "serenity-rustls"]}
|
||||
openssl = "0.10.42"
|
||||
form-data-builder = "1.0.1"
|
302
src/main.rs
Normal file
302
src/main.rs
Normal file
|
@ -0,0 +1,302 @@
|
|||
use std::{
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs::{self, OpenOptions},
|
||||
io::{Cursor, Read, Write},
|
||||
net::{Shutdown, TcpStream},
|
||||
time::{Duration, SystemTime}, sync::Arc,
|
||||
};
|
||||
|
||||
use form_data_builder::FormData;
|
||||
use openssl::ssl::{Ssl, SslContext, SslMethod, SslStream};
|
||||
use serenity::{
|
||||
async_trait,
|
||||
framework::StandardFramework,
|
||||
model::prelude::{ChannelType, Message, ChannelId},
|
||||
prelude::*,
|
||||
Client, futures::StreamExt,
|
||||
};
|
||||
use songbird::SerenityInit;
|
||||
|
||||
struct Frame {
|
||||
bytes: Vec<u8>,
|
||||
channel: u64,
|
||||
cache_stream: Option<SslStream<TcpStream>>,
|
||||
byte_to_write: Option<u8>,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
fn cache_frame(&mut self, message: u64, content: &str, token: &str) {
|
||||
let ssl_context = SslContext::builder(SslMethod::tls_client())
|
||||
.expect("ssl: context init failed")
|
||||
.build();
|
||||
let ssl = Ssl::new(&ssl_context).expect("ssl: init failed");
|
||||
let tcp_stream = TcpStream::connect("discord.com:443").expect("api: connect error");
|
||||
let mut stream = SslStream::new(ssl, tcp_stream).expect("ssl: stream init failed");
|
||||
|
||||
let mut form = FormData::new(Vec::new());
|
||||
|
||||
form.write_file(
|
||||
"payload_json",
|
||||
Cursor::new(
|
||||
stringify!({
|
||||
"content": "{content}",
|
||||
"attachments": [
|
||||
{
|
||||
"id": 0,
|
||||
"filename": "projbot3.gif"
|
||||
}
|
||||
]
|
||||
})
|
||||
.replace("{content}", content),
|
||||
),
|
||||
None,
|
||||
"application/json",
|
||||
)
|
||||
.expect("form: payload_json failed");
|
||||
form.write_file(
|
||||
"files[0]",
|
||||
Cursor::new(self.bytes.as_slice()),
|
||||
Some(OsStr::new("projbot3.gif")),
|
||||
"image/gif",
|
||||
)
|
||||
.expect("form: attachment failed");
|
||||
let mut data = form.finish().expect("form: finish failed");
|
||||
|
||||
stream.connect().expect("api: connection failed");
|
||||
stream
|
||||
.write(
|
||||
format!(
|
||||
"PATCH /api/v10/channels/{}/messages/{message} HTTP/1.1\n",
|
||||
&self.channel
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("api: write failed");
|
||||
stream
|
||||
.write(
|
||||
"Host: discord.com\nUser-Agent: projbot3 image uploader (tudbut@tudbut.de)\n"
|
||||
.as_bytes(),
|
||||
)
|
||||
.expect("api: write failed");
|
||||
stream
|
||||
.write(format!("Content-Length: {}\n", data.len()).as_bytes())
|
||||
.expect("api: write failed");
|
||||
stream
|
||||
.write(format!("Content-Type: {}\n", form.content_type_header()).as_bytes())
|
||||
.expect("api: write failed");
|
||||
stream
|
||||
.write(format!("Authorization: Bot {}\n\n", token).as_bytes())
|
||||
.expect("api: write failed");
|
||||
|
||||
// remove the last byte and cache it in the frame object for later write finish
|
||||
self.byte_to_write = Some(
|
||||
*data
|
||||
.last()
|
||||
.expect("form: empty array returned (finish failed)"),
|
||||
);
|
||||
data.remove(data.len() - 1);
|
||||
|
||||
stream.write(data.as_slice()).expect("api: write failed");
|
||||
|
||||
self.cache_stream = Some(stream);
|
||||
// now the frame is ready to send the next part
|
||||
}
|
||||
|
||||
fn complete_send(&mut self) {
|
||||
let ref mut cache_stream = self.cache_stream;
|
||||
let ref byte_to_write = self.byte_to_write;
|
||||
if let Some(stream) = cache_stream {
|
||||
if let Some(byte) = byte_to_write {
|
||||
stream
|
||||
.write(&[*byte])
|
||||
.expect("api: write failed at complete_send");
|
||||
stream.get_ref().set_read_timeout(Some(Duration::from_millis(500))).expect("tcp: unable to set timeout");
|
||||
let mut buf = Vec::new();
|
||||
let _ = stream.read_to_end(&mut buf); // failure is normal
|
||||
stream.shutdown().expect("ssl: shutdown failed");
|
||||
stream
|
||||
.get_ref()
|
||||
.shutdown(Shutdown::Both)
|
||||
.expect("tcp: shutdown failed");
|
||||
self.cache_stream = None;
|
||||
self.byte_to_write = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("complete_send called on uncached frame!");
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_frames(message: Message, ctx: Context) {
|
||||
let mut v: Vec<Frame> = Vec::new();
|
||||
let dir = fs::read_dir("vid_encoded").expect("unable to read dir");
|
||||
let dir: Vec<_> = dir.collect();
|
||||
for i in 0..dir.len() {
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(false)
|
||||
.create(false)
|
||||
.open(format!("vid_encoded/{i}"))
|
||||
.expect("readvid: invalid vid_encoded");
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)
|
||||
.expect("readvid: unable to read file");
|
||||
v.push(Frame {
|
||||
bytes: buf,
|
||||
channel: message.channel_id.0,
|
||||
cache_stream: None,
|
||||
byte_to_write: None,
|
||||
});
|
||||
}
|
||||
let guild_id = message.guild_id.unwrap();
|
||||
let http = ctx.http.clone();
|
||||
let songbird = songbird::get(&ctx)
|
||||
.await
|
||||
.expect("voice: unable to initialize songbird");
|
||||
let c0: Arc<Mutex<Option<ChannelId>>> = Arc::new(Mutex::new(None));
|
||||
let c1 = c0.clone();
|
||||
//thread::spawn(move || {tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async move {
|
||||
tokio::spawn(async move {
|
||||
let message = message;
|
||||
let ctx = ctx;
|
||||
let args = env::args().collect::<Vec<String>>();
|
||||
let token = args.get(1).unwrap();
|
||||
let mut v = v.into_iter();
|
||||
let n = message
|
||||
.channel_id
|
||||
.say(
|
||||
&ctx.http,
|
||||
"<ProjBotV3 by TudbuT#2624> Image will appear below",
|
||||
)
|
||||
.await
|
||||
.expect("discord: unable to send");
|
||||
println!("starting to send in {}@{}", n.id.0, message.channel_id.0);
|
||||
//thread::spawn(move || {tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async move {
|
||||
tokio::spawn(async move {
|
||||
let sa = unix_millis();
|
||||
println!("voice: init");
|
||||
let channel = guild_id.create_channel(http, |c| c.name("ProjBotV3-Sound").kind(ChannelType::Voice)).await.expect("voice: unable to create channel");
|
||||
*c0.lock().await = Some(channel.id);
|
||||
println!("voice: joining");
|
||||
let (handler, err) = songbird.join(guild_id, channel.id).await;
|
||||
if let Err(e) = err {
|
||||
panic!("voice: error {e}");
|
||||
}
|
||||
println!("voice: loading");
|
||||
let handle = handler.lock().await.play_source(songbird::ffmpeg("aud_encoded").await.expect("voice: unable to load"));
|
||||
handle.make_playable().unwrap();
|
||||
handle.pause().expect("voice: unable to pause");
|
||||
handle.set_volume(1.0).unwrap();
|
||||
println!("voice: waiting for video");
|
||||
tokio::time::sleep(Duration::from_millis(5000 - (unix_millis() - sa))).await;
|
||||
println!("voice: playing");
|
||||
handle.play().expect("voice: unable to play");
|
||||
println!("{:?}", handle.get_info().await);
|
||||
});//});
|
||||
let mut sa = unix_millis();
|
||||
let mut to_compensate_for = 0;
|
||||
while let Some(mut frame) = v.next() {
|
||||
println!("vid: caching");
|
||||
frame.cache_frame(
|
||||
n.id.0,
|
||||
format!("<ProjBotV3 by TudbuT#2624> Image will appear below [to_compensate_for={to_compensate_for}]").as_str(),
|
||||
token,
|
||||
);
|
||||
let msgs = n.channel_id.messages_iter(&ctx.http).take(30).collect::<Vec<_>>().await;
|
||||
println!("vid: waiting");
|
||||
let mut to_sleep = 5000 - ((unix_millis() - sa) as i128);
|
||||
sa = unix_millis();
|
||||
if let Some(Ok(msg)) = msgs.iter().filter(|x| x.as_ref().unwrap().content == "!stop").next() {
|
||||
msg.delete(&ctx.http).await.expect("discord: unable to delete command");
|
||||
break;
|
||||
}
|
||||
if let Some(Ok(msg)) = msgs.iter().filter(|x| x.as_ref().unwrap().content == "!sync vid").next() {
|
||||
msg.delete(&ctx.http).await.expect("discord: unable to delete command");
|
||||
to_compensate_for += 100;
|
||||
msg.channel_id.say(&ctx.http, "<ProjBotV3 by TudbuT#2624> Skipped 100ms of video :+1:").await.expect("discord: unable to send commannd response");
|
||||
}
|
||||
if let Some(Ok(msg)) = msgs.iter().filter(|x| x.as_ref().unwrap().content == "!sync aud").next() {
|
||||
msg.delete(&ctx.http).await.expect("discord: unable to delete command");
|
||||
to_sleep += 100;
|
||||
msg.channel_id.say(&ctx.http, "<ProjBotV3 by TudbuT#2624> Skipped 100ms of video :+1:").await.expect("discord: unable to send commannd response");
|
||||
}
|
||||
to_sleep -= (unix_millis() - sa) as i128;
|
||||
'calc: loop {
|
||||
if to_sleep < 0 {
|
||||
to_compensate_for += -to_sleep;
|
||||
break 'calc;
|
||||
}
|
||||
|
||||
if to_compensate_for > 0 {
|
||||
if to_sleep - to_compensate_for >= 0 {
|
||||
to_sleep -= to_compensate_for;
|
||||
to_compensate_for = 0;
|
||||
} else {
|
||||
to_compensate_for -= to_sleep;
|
||||
to_sleep = 0;
|
||||
}
|
||||
break 'calc;
|
||||
}
|
||||
|
||||
break 'calc;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(to_sleep as u64)).await;
|
||||
sa = unix_millis();
|
||||
println!("vid: completing");
|
||||
frame.complete_send();
|
||||
}
|
||||
n.delete(&ctx.http)
|
||||
.await
|
||||
.expect("discord: unable to delete message");
|
||||
if let Some(c) = *c1.lock().await {
|
||||
c.delete(&ctx.http).await.expect("discord: unable to delete voice channel");
|
||||
}
|
||||
});//});
|
||||
}
|
||||
|
||||
struct Handler;
|
||||
|
||||
#[async_trait]
|
||||
impl EventHandler for Handler {
|
||||
|
||||
async fn message(&self, ctx: Context, message: Message) {
|
||||
if message.guild_id == None {
|
||||
println!("DM");
|
||||
return;
|
||||
}
|
||||
|
||||
if message.content == "!play" {
|
||||
println!("hi");
|
||||
send_frames(message, ctx).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let framework = StandardFramework::new().configure(|c| c.prefix("!"));
|
||||
let mut client = Client::builder(
|
||||
env::args()
|
||||
.collect::<Vec<String>>()
|
||||
.get(1)
|
||||
.expect("discord: no token provided"),
|
||||
GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_VOICE_STATES,
|
||||
)
|
||||
.framework(framework)
|
||||
.event_handler(Handler)
|
||||
.register_songbird()
|
||||
.await
|
||||
.expect("discord: init failed");
|
||||
|
||||
if let Err(why) = client.start().await {
|
||||
println!("An error occurred while running the client: {:?}", why);
|
||||
}
|
||||
}
|
||||
|
||||
fn unix_millis() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
Loading…
Add table
Reference in a new issue