Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F123
main.rs
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
23 KB
Referenced Files
None
Subscribers
None
main.rs
View Options
use
std
::
env
;
use
std
::
sync
::
Arc
;
use
std
::
collections
::
HashMap
;
use
std
::
time
::
Duration
;
use
tokio
::
time
::
sleep
;
use
opensubsonic
::
Auth
;
use
opensubsonic
::
Client
as
SubsonicClient
;
use
reqwest
::
Client
as
HttpClient
;
use
serenity
::
async_trait
;
use
serenity
::
http
::
Http
;
use
serenity
::
Result
as
SerenityResult
;
use
serenity
::
model
::
application
::
ComponentInteractionDataKind
;
use
serenity
::
model
::
channel
::
Message
;
use
serenity
::
model
::
prelude
::
ChannelId
;
use
serenity
::
model
::
prelude
::
GuildId
;
use
serenity
::
model
::
gateway
::
Ready
;
use
serenity
::
prelude
::
*
;
use
songbird
::
SerenityInit
;
use
songbird
::
events
::{
Event
,
EventContext
,
EventHandler
as
VoiceEventHandler
,
TrackEvent
};
use
songbird
::
input
::
HttpRequest
as
SongbirdHttp
;
use
songbird
::
tracks
::{
Track
,
TrackHandle
,
PlayMode
};
use
poise
::
serenity_prelude
as
serenity
;
use
poise
::
reply
::
ReplyHandle
;
// poise crud
struct
CommandData
{
subsonic_client
:
Arc
<
SubsonicClient
>
,
}
type
Error
=
Box
<
dyn
std
::
error
::
Error
+
Send
+
Sync
>
;
type
CommandContext
<'
a
>
=
poise
::
Context
<'
a
,
CommandData
,
Error
>
;
struct
Handler
;
// https://stackoverflow.com/a/38461750
fn
truncate
(
s
:
&
str
,
max_chars
:
usize
)
->
&
str
{
match
s
.
char_indices
().
nth
(
max_chars
)
{
None
=>
s
,
Some
((
idx
,
_
))
=>
&
s
[
..
idx
],
}
}
fn
truncate_in_place
(
s
:
&
mut
String
,
max_chars
:
usize
)
{
let
bytes
=
truncate
(
&
s
,
max_chars
).
len
();
s
.
truncate
(
bytes
);
}
fn
duration_string
(
duration
:
Duration
)
->
String
{
if
!
duration
.
is_zero
()
{
let
duration_minutes
=
duration
.
as_secs
()
/
60
;
let
duration_seconds
=
duration
.
as_secs
()
%
60
;
return
format!
(
"{}:{:02}"
,
duration_minutes
,
duration_seconds
)
}
else
{
return
"0:00"
.
to_string
()
};
}
fn
get_description
(
track
:
&
TrackHandle
)
->
String
{
let
user_data
:
Arc
<
HashMap
<
String
,
String
>>
=
track
.
data
();
return
user_data
.
get
(
"description"
).
unwrap
().
clone
();
}
/// Registers the bot's commands on Discord.
#[poise::command(prefix_command)]
pub
async
fn
register
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
poise
::
builtins
::
register_application_commands_buttons
(
ctx
).
await
?
;
Ok
(())
}
/// Join a voice channel that you're in.
#[poise::command(slash_command)]
async
fn
join
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
(
guild_id
,
channel_id
)
=
{
let
guild
=
ctx
.
guild
().
unwrap
();
let
channel_id
=
guild
.
voice_states
.
get
(
&
ctx
.
author
().
id
)
.
and_then
(
|
voice_state
|
voice_state
.
channel_id
);
(
guild
.
id
,
channel_id
)
};
let
connect_to
=
match
channel_id
{
Some
(
channel
)
=>
channel
,
None
=>
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel"
).
await
);
return
Ok
(());
},
};
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Ok
(
handler_lock
)
=
manager
.
join
(
guild_id
,
connect_to
).
await
{
let
mut
handle
=
handler_lock
.
lock
().
await
;
let
chan_id
=
ctx
.
channel_id
();
let
send_http
=
ctx
.
serenity_context
().
http
.
clone
();
handle
.
add_global_event
(
Event
::
Track
(
TrackEvent
::
End
),
TrackEndNotifier
{
chan_id
,
http
:
send_http
,
},
);
let
send_http
=
ctx
.
serenity_context
().
http
.
clone
();
handle
.
add_global_event
(
Event
::
Track
(
TrackEvent
::
Play
),
TrackPlayNotifier
{
guild_id
,
chan_id
,
ctx
:
ctx
.
serenity_context
().
clone
(),
// ugh
http
:
send_http
,
},
);
poise_check_msg
(
ctx
.
reply
(
"Joined voice channel"
).
await
);
}
return
Ok
(());
}
/// Leave the voice channel that the bot is in.
#[poise::command(slash_command)]
async
fn
leave
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
let
has_handler
=
manager
.
get
(
guild_id
).
is_some
();
if
has_handler
{
if
let
Err
(
e
)
=
manager
.
remove
(
guild_id
).
await
{
poise_check_msg
(
ctx
.
reply
(
format!
(
"Failed: {:?}"
,
e
))
.
await
,
);
}
poise_check_msg
(
ctx
.
reply
(
"Left voice channel"
).
await
);
}
else
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel"
).
await
);
}
return
Ok
(());
}
/// Print the current queue of tracks to the channel.
#[poise::command(slash_command)]
async
fn
print_queue
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Some
(
handler_lock
)
=
manager
.
get
(
guild_id
)
{
let
handler
=
handler_lock
.
lock
().
await
;
let
queue
=
handler
.
queue
().
current_queue
();
if
queue
.
len
()
>
0
{
let
mut
output
=
String
::
new
();
for
track
in
queue
{
let
extra
=
if
let
Some
(
current_track
)
=
handler
.
queue
().
current
()
&&
current_track
.
uuid
()
==
track
.
uuid
()
&&
let
Ok
(
info
)
=
current_track
.
get_info
().
await
{
let
state_string
=
match
info
.
playing
{
PlayMode
::
Play
=>
"playing"
,
PlayMode
::
Pause
=>
"paused"
,
PlayMode
::
Stop
=>
"stopped"
,
PlayMode
::
End
=>
"ended"
,
PlayMode
::
Errored
(
_
)
=>
"error"
,
_
=>
"unknown"
};
let
duration
=
duration_string
(
info
.
position
);
let
user_data
:
Arc
<
HashMap
<
String
,
String
>>
=
current_track
.
data
();
let
length
=
user_data
.
get
(
"duration"
).
unwrap
().
clone
();
format!
(
" ({}, {}/{})"
,
state_string
,
duration
,
length
)
}
else
{
""
.
to_string
()
};
output
=
format!
(
"{}* {}{}
\n
"
,
output
,
get_description
(
&
track
),
extra
);
}
poise_check_msg
(
ctx
.
reply
(
output
)
.
await
,
);
}
else
{
poise_check_msg
(
ctx
.
reply
(
"No tracks queued."
)
.
await
,
);
}
}
else
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel to play in"
)
.
await
,
);
}
return
Ok
(());
}
/// Resumes playback when paused.
#[poise::command(slash_command)]
async
fn
play
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Some
(
handler_lock
)
=
manager
.
get
(
guild_id
)
{
let
handler
=
handler_lock
.
lock
().
await
;
let
queue
=
handler
.
queue
();
if
queue
.
current
().
is_none
()
{
poise_check_msg
(
ctx
.
reply
(
"Nothing to resume. Queue something up!"
)
.
await
,
);
return
Ok
(());
}
match
queue
.
resume
()
{
Ok
(
_
)
=>
poise_check_msg
(
ctx
.
reply
(
"Track resumed."
).
await
),
Err
(
err
)
=>
{
println!
(
"Resume error: {}"
,
err
);
poise_check_msg
(
ctx
.
reply
(
"Error resuming."
).
await
)
},
}
}
else
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel to play in"
)
.
await
,
);
}
return
Ok
(());
}
/// Pauses playback.
#[poise::command(slash_command)]
async
fn
pause
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Some
(
handler_lock
)
=
manager
.
get
(
guild_id
)
{
let
handler
=
handler_lock
.
lock
().
await
;
let
queue
=
handler
.
queue
();
if
queue
.
current
().
is_none
()
{
poise_check_msg
(
ctx
.
reply
(
"Nothing to pause. Queue something up!"
)
.
await
,
);
return
Ok
(());
}
match
queue
.
pause
()
{
Ok
(
_
)
=>
poise_check_msg
(
ctx
.
reply
(
"Track paused."
).
await
),
Err
(
err
)
=>
{
println!
(
"Pause error: {}"
,
err
);
poise_check_msg
(
ctx
.
reply
(
"Error pausing."
).
await
)
},
}
}
else
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel to play in"
)
.
await
,
);
}
return
Ok
(());
}
/// Stops playback.
#[poise::command(slash_command)]
async
fn
stop
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Some
(
handler_lock
)
=
manager
.
get
(
guild_id
)
{
let
handler
=
handler_lock
.
lock
().
await
;
let
queue
=
handler
.
queue
();
queue
.
stop
();
poise_check_msg
(
ctx
.
reply
(
"Queue cleared."
).
await
);
}
else
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel to play in"
)
.
await
,
);
}
return
Ok
(());
}
/// Skips the current track and moves to the next one in the queue.
#[poise::command(slash_command)]
async
fn
skip
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Some
(
handler_lock
)
=
manager
.
get
(
guild_id
)
{
let
handler
=
handler_lock
.
lock
().
await
;
let
queue
=
handler
.
queue
();
let
_
=
queue
.
skip
();
poise_check_msg
(
ctx
.
reply
(
format!
(
"Song skipped: {} in queue."
,
queue
.
len
()),
)
.
await
,
);
}
else
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel to play in"
)
.
await
,
);
}
return
Ok
(());
}
/// Mutes the bot in the voice channel.
#[poise::command(slash_command)]
async
fn
mute
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
let
handler_lock
=
match
manager
.
get
(
guild_id
)
{
Some
(
handler
)
=>
handler
,
None
=>
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel"
).
await
);
return
Ok
(());
},
};
let
mut
handler
=
handler_lock
.
lock
().
await
;
if
handler
.
is_mute
()
{
poise_check_msg
(
ctx
.
reply
(
"Already muted"
).
await
);
}
else
{
if
let
Err
(
e
)
=
handler
.
mute
(
true
).
await
{
poise_check_msg
(
ctx
.
reply
(
format!
(
"Failed: {:?}"
,
e
))
.
await
,
);
}
poise_check_msg
(
ctx
.
reply
(
"Now muted"
).
await
);
}
return
Ok
(());
}
/// Unmutes in the voice channel if muted.
#[poise::command(slash_command)]
async
fn
unmute
(
ctx
:
CommandContext
<'
_
>
)
->
Result
<
(),
Error
>
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Some
(
handler_lock
)
=
manager
.
get
(
guild_id
)
{
let
mut
handler
=
handler_lock
.
lock
().
await
;
if
let
Err
(
e
)
=
handler
.
mute
(
false
).
await
{
poise_check_msg
(
ctx
.
reply
(
format!
(
"Failed: {:?}"
,
e
))
.
await
,
);
}
poise_check_msg
(
ctx
.
reply
(
"Unmuted"
).
await
);
}
else
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel to unmute in"
)
.
await
,
);
}
return
Ok
(());
}
struct
SubsonicSearchTrackResult
{
title
:
String
,
artist
:
String
,
album
:
String
,
duration
:
Duration
,
url
:
String
}
impl
SubsonicSearchTrackResult
{
fn
label
(
&
self
)
->
String
{
return
format!
(
"'{}', by '{}', from '{}'"
,
self
.
title
,
self
.
artist
,
self
.
album
);
}
fn
label_truncated
(
&
self
,
max_chars
:
usize
)
->
String
{
let
mut
label
=
self
.
label
();
if
label
.
len
()
>
max_chars
{
truncate_in_place
(
&
mut
label
,
max_chars
);
}
return
label
;
}
}
async
fn
play_url
(
ctx
:
CommandContext
<'
_
>
,
result
:
&
SubsonicSearchTrackResult
)
{
let
guild_id
=
ctx
.
guild_id
().
unwrap
();
let
manager
=
songbird
::
get
(
ctx
.
serenity_context
())
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Some
(
handler_lock
)
=
manager
.
get
(
guild_id
)
{
let
mut
handler
=
handler_lock
.
lock
().
await
;
let
http_client
=
HttpClient
::
new
();
// XXX: Reusable HTTP client, like in
// https://raw.githubusercontent.com/serenity-rs/songbird/refs/heads/current/examples/serenity/voice_events_queue/src/main.rs
let
src
=
SongbirdHttp
::
new
(
http_client
,
result
.
url
.
clone
());
let
mut
user_data
=
Arc
::
new
(
HashMap
::
new
());
let
description
=
result
.
label
();
let
duration_string
=
duration_string
(
result
.
duration
);
Arc
::
make_mut
(
&
mut
user_data
)
.
insert
(
"description"
.
to_string
(),
description
);
Arc
::
make_mut
(
&
mut
user_data
)
.
insert
(
"duration"
.
to_string
(),
duration_string
);
let
track
=
Track
::
new_with_data
(
src
.
into
(),
user_data
);
handler
.
enqueue
(
track
).
await
;
poise_check_msg
(
ctx
.
reply
(
format!
(
"Added song '{}' to queue: position {}"
,
result
.
title
,
handler
.
queue
().
len
()),
)
.
await
,
);
}
else
{
poise_check_msg
(
ctx
.
reply
(
"Not in a voice channel to play in"
)
.
await
,
);
}
}
fn
parse_track_results
(
client
:
&
Arc
<
opensubsonic
::
Client
>
,
tracks
:
&
Vec
<
opensubsonic
::
data
::
Child
>
)
->
HashMap
<
String
,
SubsonicSearchTrackResult
>
{
let
mut
parsed_results
:
HashMap
<
String
,
SubsonicSearchTrackResult
>
=
HashMap
::
new
();
for
result
in
tracks
{
// implicit discord limit?
if
parsed_results
.
len
()
>=
25
{
break
;
}
let
title
=
result
.
title
.
clone
();
let
artist
=
result
.
artist
.
as_ref
().
unwrap
().
clone
();
let
album
=
result
.
album
.
as_ref
().
unwrap
().
clone
();
let
duration_secs
=
result
.
duration
.
unwrap_or
(
0
).
try_into
().
expect
(
"Hey, where's my duration?"
);
let
duration
=
Duration
::
from_secs
(
duration_secs
);
// set channel bitrate here if we want to transcode
let
url
:
String
=
client
.
stream_url
(
result
.
id
.
as_ref
(),
None
,
None
).
unwrap
().
into
();
parsed_results
.
insert
(
result
.
id
.
clone
(),
SubsonicSearchTrackResult
{
title
,
artist
,
album
,
duration
,
url
});
}
return
parsed_results
;
}
fn
components_from_track_results
(
tracks
:
&
HashMap
<
String
,
SubsonicSearchTrackResult
>
)
->
Vec
<
serenity
::
CreateActionRow
>
{
if
tracks
.
len
()
==
1
{
return
vec!
[
serenity
::
CreateActionRow
::
Buttons
(
tracks
.
iter
().
map
(
|
(
k
,
v
)
|
{
// truncate to 100 chars, discord limit?
let
label
=
v
.
label_truncated
(
100
);
return
serenity
::
CreateButton
::
new
(
format!
(
"{k}"
))
.
label
(
label
)
}).
collect
()
)
]
}
return
vec!
[
serenity
::
CreateActionRow
::
SelectMenu
(
serenity
::
CreateSelectMenu
::
new
(
"tracks"
.
to_string
(),
serenity
::
CreateSelectMenuKind
::
String
{
options
:
tracks
.
iter
().
map
(
|
(
k
,
v
)
|
{
let
label
=
v
.
label_truncated
(
100
);
return
serenity
::
CreateSelectMenuOption
::
new
(
label
,
format!
(
"{k}"
))
}).
collect
()
}
)
)
];
}
async
fn
track_select_interaction
(
ctx
:
CommandContext
<'
_
>
,
tracks
:
&
HashMap
<
String
,
SubsonicSearchTrackResult
>
,
message
:
String
)
->
Result
<
(),
Error
>
{
let
create_reply
=
{
let
components
=
components_from_track_results
(
tracks
);
poise
::
CreateReply
::
default
()
.
content
(
message
)
.
components
(
components
)
};
let
reply
=
ctx
.
send
(
create_reply
).
await
?
;
let
message
=
reply
.
message
().
await
?
;
let
interaction
=
match
message
.
await_component_interaction
(
&
ctx
.
serenity_context
().
shard
)
.
timeout
(
Duration
::
from_secs
(
60
*
3
))
.
await
{
Some
(
x
)
=>
x
,
None
=>
{
message
.
delete
(
&
ctx
).
await
?
;
return
Ok
(());
},
};
if
let
Some
(
track_id
)
=
match
&
interaction
.
data
.
kind
{
ComponentInteractionDataKind
::
StringSelect
{
values
}
=>
Some
(
&
values
[
0
]),
ComponentInteractionDataKind
::
Button
=>
Some
(
&
interaction
.
data
.
custom_id
),
_
=>
None
}
&&
let
Some
(
result
)
=
tracks
.
get
(
track_id
)
{
play_url
(
ctx
,
&
result
).
await
;
}
// XXX: Use an interaction stream if we want multiple responses?
// See: https://github.com/serenity-rs/serenity/blob/current/examples/e17_message_components/src/main.rs
interaction
.
create_response
(
ctx
,
serenity
::
CreateInteractionResponse
::
Acknowledge
).
await
?
;
message
.
delete
(
&
ctx
).
await
?
;
return
Ok
(());
}
/// Search for tracks from Subsonic.
#[poise::command(slash_command)]
async
fn
search
(
ctx
:
CommandContext
<'
_
>
,
#[description =
"Track? Artist? Album? Who knows!"
]
q
:
String
)
->
Result
<
(),
Error
>
{
let
subsonic_client
=
&
ctx
.
data
().
subsonic_client
;
let
results
=
subsonic_client
.
search3
(
q
.
as_str
(),
None
,
None
,
None
,
None
,
Some
(
25
),
None
,
None
)
.
await
.
expect
(
"Failed to search Subsonic server"
);
if
results
.
song
.
len
()
==
0
{
poise_check_msg
(
ctx
.
reply
(
format!
(
"No tracks found for '{}'"
,
q
)
)
.
await
);
sleep
(
Duration
::
from_secs
(
60
*
3
)).
await
;
return
Ok
(());
}
let
parsed_results
=
parse_track_results
(
&
subsonic_client
,
&
results
.
song
);
track_select_interaction
(
ctx
,
&
parsed_results
,
format!
(
"Results for '{}'"
,
q
)).
await
?
;
return
Ok
(());
}
/// Get a random set of tracks to pick from.
#[poise::command(slash_command)]
async
fn
random
(
ctx
:
CommandContext
<'
_
>
,
#[description =
"Optional, genre (from the ID3 soup)"
]
genre
:
Option
<
String
>
,
#[description =
"Starting from the year"
]
from_year
:
Option
<
i32
>
,
#[description =
"Up to the the year"
]
to_year
:
Option
<
i32
>
)
->
Result
<
(),
Error
>
{
let
genre_as_str
=
genre
.
as_deref
();
let
subsonic_client
=
&
ctx
.
data
().
subsonic_client
;
let
results
=
subsonic_client
.
get_random_songs
(
Some
(
25
),
genre_as_str
,
from_year
,
to_year
,
None
)
.
await
.
expect
(
"Failed to get random tracks"
);
let
parsed_results
=
parse_track_results
(
&
subsonic_client
,
&
results
);
track_select_interaction
(
ctx
,
&
parsed_results
,
"Random charcuterie platter"
.
to_string
()).
await
?
;
return
Ok
(());
}
#[async_trait]
impl
EventHandler
for
Handler
{
async
fn
ready
(
&
self
,
_
:
Context
,
ready
:
Ready
)
{
println!
(
"{} is connected!"
,
ready
.
user
.
name
);
}
}
struct
TrackEndNotifier
{
chan_id
:
ChannelId
,
http
:
Arc
<
Http
>
,
}
#[async_trait]
impl
VoiceEventHandler
for
TrackEndNotifier
{
async
fn
act
(
&
self
,
ctx
:
&
EventContext
<'
_
>
)
->
Option
<
Event
>
{
if
let
EventContext
::
Track
(
track_list
)
=
ctx
{
check_msg
(
self
.
chan_id
.
say
(
&
self
.
http
,
&
format!
(
"Tracks ended: {}."
,
track_list
.
len
()))
.
await
,
);
}
None
}
}
struct
TrackPlayNotifier
{
guild_id
:
GuildId
,
chan_id
:
ChannelId
,
ctx
:
Context
,
// clone for now, figure out lifetimes later
http
:
Arc
<
Http
>
,
}
#[async_trait]
impl
VoiceEventHandler
for
TrackPlayNotifier
{
async
fn
act
(
&
self
,
_ctx
:
&
EventContext
<'
_
>
)
->
Option
<
Event
>
{
let
manager
=
songbird
::
get
(
&
self
.
ctx
)
.
await
.
expect
(
"Songbird Voice client placed in at initialisation."
)
.
clone
();
if
let
Some
(
handler_lock
)
=
manager
.
get
(
self
.
guild_id
)
{
let
handler
=
handler_lock
.
lock
().
await
;
if
let
Some
(
next_track
)
=
handler
.
queue
().
current
()
{
let
filename
=
get_description
(
&
next_track
);
check_msg
(
self
.
chan_id
.
say
(
&
self
.
http
,
&
format!
(
"Track now playing: {}"
,
filename
))
.
await
,
);
}
}
None
}
}
#[tokio::main]
async
fn
main
()
{
// XXX: Gross workaround for "Could not automatically determine the process-level CryptoProvider from Rustls crate features"
rustls
::
crypto
::
ring
::
default_provider
()
.
install_default
()
.
expect
(
"Failed to install rustls crypto provider"
);
// Login with a bot token from the environment
let
token
=
env
::
var
(
"DISCORD_TOKEN"
).
expect
(
"Expected a token in the environment"
);
let
subsonic_server
=
env
::
var
(
"SUBSONIC_SERVER"
).
expect
(
"Expected a server in the environment"
);
let
subsonic_user
=
env
::
var
(
"SUBSONIC_USERNAME"
).
expect
(
"Expected a username in the environment"
);
let
subsonic_pass
=
env
::
var
(
"SUBSONIC_PASSWORD"
).
expect
(
"Expected a passord in the environment"
);
// Set gateway intents, which decides what events the bot will be notified about
let
intents
=
GatewayIntents
::
non_privileged
()
|
GatewayIntents
::
GUILD_MESSAGES
|
GatewayIntents
::
MESSAGE_CONTENT
;
let
subsonic_client
=
SubsonicClient
::
new
(
&
subsonic_server
,
Auth
::
token
(
subsonic_user
,
subsonic_pass
),
).
expect
(
"Couldn't create client"
);
let
_
=
subsonic_client
.
ping
().
await
;
let
framework
=
poise
::
Framework
::
builder
()
.
options
(
poise
::
FrameworkOptions
{
commands
:
vec
!
[
register
(),
join
(),
leave
(),
print_queue
(),
play
(),
pause
(),
stop
(),
skip
(),
mute
(),
unmute
(),
search
(),
random
(),
],
prefix_options
:
poise
::
PrefixFrameworkOptions
{
prefix
:
Some
(
"!"
.
into
()),
..
Default
::
default
()
},
..
Default
::
default
()
})
.
setup
(
|
ctx
,
_ready
,
framework
|
{
Box
::
pin
(
async
move
{
poise
::
builtins
::
register_globally
(
ctx
,
&
framework
.
options
().
commands
).
await
?
;
Ok
(
CommandData
{
subsonic_client
:
Arc
::
new
(
subsonic_client
)
})
})
})
.
build
();
// Create a new instance of the Client, logging in as a bot.
let
mut
client
=
Client
::
builder
(
&
token
,
intents
)
.
framework
(
framework
)
.
event_handler
(
Handler
)
.
register_songbird
()
.
await
.
expect
(
"Err creating client"
);
// Start listening for events by starting a single shard
if
let
Err
(
why
)
=
client
.
start
().
await
{
println!
(
"Client error: {why:?}"
);
}
}
/// Checks that a message successfully sent; if not, then logs why to stdout.
fn
check_msg
(
result
:
SerenityResult
<
Message
>
)
{
if
let
Err
(
why
)
=
result
{
println!
(
"Error sending message: {:?}"
,
why
);
}
}
fn
poise_check_msg
(
result
:
Result
<
ReplyHandle
<'
_
>
,
SerenityError
>
)
{
if
let
Err
(
why
)
=
result
{
println!
(
"Error sending message: {:?}"
,
why
);
}
}
File Metadata
Details
Attached
Mime Type
text/x-c
Expires
Mon, Sep 28, 3:48 PM (12 h, 15 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
107
Default Alt Text
main.rs (23 KB)
Attached To
Mode
rRMB Rust Music Bot
Attached
Detach File
Event Timeline