55 Commits

Author SHA1 Message Date
Michał Czyż
9ce77fdb66 chore: release v0.10.8 2024-12-15 20:02:38 +01:00
089bfa18cf Merge branch 'senpai' of github.com:eRgo35/lyra into senpai 2024-12-15 20:02:03 +01:00
2fb262b804 fix: nix minor overlay changes 2024-12-15 20:01:39 +01:00
c317b53c2e Merge pull request #16 from eRgo35/release-plz-2024-08-15T18-15-55Z
chore: release v0.10.7
2024-12-15 19:18:28 +01:00
b09fc82611 fix: lock rebuild sync 2024-12-15 19:15:28 +01:00
c627352057 fix: upgraded deps 2024-12-15 19:08:24 +01:00
45236f248e feat: nix flake 2024-12-15 18:55:28 +01:00
Michał Czyż
e20a98f378 chore: release 2024-08-16 14:35:51 +02:00
66052e4f74 chore: version bump to 0.10.6
Some checks failed
Rust / build (push) Has been cancelled
Release Plz / Release-plz (push) Has been cancelled
2024-08-16 14:33:37 +02:00
09b0d6fd9d fix: yt-dlp errors for playlist parsing 2024-08-16 14:33:22 +02:00
c27d1f70da chore: version bump to 0.10.5
Some checks are pending
Rust / build (push) Waiting to run
Release Plz / Release-plz (push) Waiting to run
2024-08-15 20:13:42 +02:00
73b885e5d6 fix: version requirements for crates.io 2024-08-15 20:11:35 +02:00
8c0eb62d41 fix: yt-dlp errors 2024-08-15 20:05:52 +02:00
5bc86aca2b Merge pull request #15 from eRgo35/release-plz-2024-08-13T13-12-37Z
Some checks failed
Rust / build (push) Has been cancelled
Release Plz / Release-plz (push) Has been cancelled
chore: release v0.10.4
2024-08-13 15:25:20 +02:00
Michał Czyż
57a281a1d3 chore: release 2024-08-13 15:21:37 +02:00
2f4045d4a9 fix: added wip info for some commands 2024-08-13 15:20:54 +02:00
aa215daf28 chore: cargo-dist action setup 2024-08-13 15:11:07 +02:00
7fde11f615 chore: repo added to Cargo.toml 2024-08-13 15:07:50 +02:00
c8f86a2f9c chore: using cargo-dist for package building 2024-08-13 15:06:32 +02:00
d9b6c8fc4b Merge pull request #14 from eRgo35/release-plz-2024-08-13T12-51-00Z
chore: release v0.10.3
2024-08-13 14:56:39 +02:00
Michał Czyż
051ae857a7 chore: release 2024-08-13 14:51:01 +02:00
08fbdc828c ci: release-plz 2024-08-13 14:50:20 +02:00
aeb10d9baf ops: default branch yeet 2024-08-12 20:58:01 +02:00
af247aa6d5 ci: release please setup 2024-08-12 20:01:06 +02:00
eae957c6d7 Create ci.yml 2024-08-12 19:33:07 +02:00
5399f3ba6d oop 2024-08-12 19:31:51 +02:00
076dfd0a93 fixed release to build arm64 2024-08-12 19:08:59 +02:00
212abd9bdd chore: release 0.10.2 2024-08-12 18:46:53 +02:00
8d787f2a11 fix: crates bumped 2024-08-12 18:46:21 +02:00
486f403d10 chore: release 0.10.1 2024-08-12 18:20:24 +02:00
bfd301c9c2 fix: deps broken 2024-08-12 18:14:09 +02:00
670b8dd30e gh: actions 2024-08-12 18:13:59 +02:00
c607aaf403 updated image in README.md 2024-08-12 13:37:10 +02:00
f41fa7e09b updated README.md 2024-08-12 13:36:36 +02:00
10b7d4d7e3 queue system improved 2024-08-08 20:46:22 +02:00
772d5d2a97 spotify-parser is now a submodule 2024-08-05 16:03:52 +02:00
f1b52d2be2 unchecked submodules for cross compile 2024-08-05 15:12:16 +02:00
e77818e09b 0.9.1 heartbeat no longer dropping 2024-08-05 15:01:45 +02:00
5a026afc52 songbird and serenity submodule 2024-08-05 14:39:13 +02:00
8a0d08de4e added songbird as a submodule 2024-08-05 11:32:23 +02:00
f224ccfbdf reduced ai timeout, impl uptime 2024-08-04 21:26:45 +02:00
d0af34833b lyra nightly avatar 2024-08-04 20:11:31 +02:00
c2e653b3ea [0.9.0] new parser 2024-08-04 18:03:21 +02:00
78a1937210 [0.8.2] lyra package update 2024-08-03 00:12:44 +02:00
fc42427437 updated packages and closing #2
Signed-off-by: Michael Czyż <mike@c2yz.com>
2024-05-18 23:46:25 +02:00
9474577233 Lyra is now open source! 2024-02-22 13:55:11 +01:00
b86e8b9daf soundboard and some tools 2024-02-21 12:29:30 +01:00
ef29e5319a server scripts fix 2024-02-20 22:36:40 +01:00
ee3d9a0c45 new commands, music fixes and more 2024-02-20 22:27:36 +01:00
4e92771f8f minor refractors, uptime removed 2024-02-18 19:28:46 +01:00
8a947926f7 0.5.1 refractor 2024-02-16 12:42:38 +01:00
639fd7775f more commands incoming 2024-02-13 23:00:54 +01:00
8fa86b0182 0.5.0 embed, help, refractor 2024-02-13 18:44:01 +01:00
d75c5bc9a2 lyra spotify repos 2024-02-13 13:48:20 +01:00
21822405c0 cleanup part two 2024-02-12 22:29:14 +01:00
60 changed files with 4281 additions and 853 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
DISCORD_TOKEN=<YOUR_DISCORD_TOKEN>
PREFIX=<YOUR_PREFIX>

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: eRgo35

22
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Rust
on:
push:
branches: [ "senpai" ]
pull_request:
branches: [ "senpai" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

27
.github/workflows/release-plz.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Release Plz
permissions:
pull-requests: write
contents: write
on:
push:
branches:
- senpai
jobs:
release-plz:
name: Release-plz
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: MarcoIeni/release-plz-action@v0.5
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

281
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,281 @@
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with cargo-dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't cargo-dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (cargo-dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'cargo dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-20.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.20.0/cargo-dist-installer.sh | sh"
- name: Cache cargo-dist
uses: actions/upload-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/cargo-dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "cargo dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by cargo-dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to cargo dist
# - install-dist: expression to run to install cargo-dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cargo-dist
run: ${{ matrix.install_dist }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "cargo dist ran successfully"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "cargo dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v4
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
# Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-20.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install cached cargo-dist
uses: actions/download-artifact@v4
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/cargo-dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v4
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v4
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
- plan
- host
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-20.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

2
.gitignore vendored
View File

@@ -1,2 +1,4 @@
/target /target
**/target
.env .env
.direnv/

34
CHANGELOG.md Normal file
View File

@@ -0,0 +1,34 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.10.8](https://github.com/eRgo35/lyra/compare/v0.10.7...v0.10.8) - 2024-12-15
### Fixed
- nix minor overlay changes
## [0.10.7](https://github.com/eRgo35/lyra/compare/v0.10.6...v0.10.7) - 2024-08-16
### Other
- update Cargo.lock dependencies
## [0.10.4](https://github.com/eRgo35/lyra/compare/v0.10.3...v0.10.4) - 2024-08-13
### Fixed
- added wip info for some commands
### Other
- cargo-dist action setup
- repo added to Cargo.toml
- using cargo-dist for package building
## [0.10.3](https://github.com/eRgo35/lyra/compare/v0.10.2...v0.10.3) - 2024-08-13
### Other
- release-plz
# Changelog

2550
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,74 @@
[package] [package]
name = "lyra" name = "lyra"
version = "0.4.0" version = "0.10.8"
authors = ["Michał Czyż <mike@c2yz.com>"] authors = ["Michał Czyż <mike@c2yz.com>"]
edition = "2021" edition = "2021"
description = "A featureful Discord bot written in Rust." description = "A featureful Discord bot written in Rust."
documentation = "https://lyra.c2yz.com/docs" documentation = "https://github.com/eRgo35/lyra"
repository = "https://github.com/eRgo35/lyra"
readme = "README.md" readme = "README.md"
homepage = "https://lyra.c2yz.com" homepage = "https://lyra.c2yz.com"
license-file = "LICENSE.md" license-file = "LICENSE.md"
keywords = ["discord", "bot", "rust", "music", "featureful"] keywords = ["discord", "bot", "rust", "music", "featureful"]
[dependencies] [dependencies]
spotify-parser = "1.0.1"
dotenv = "0.15.0" dotenv = "0.15.0"
openssl = { version = "0.10.63", features = ["vendored"] } json = "0.12.4"
poise = "0.6.1" openssl = { version = "0.10.66", features = ["vendored"] }
regex = "1.10.3" owoify = "0.1.5"
reqwest = "0.11.23" poise = { default-features = true, version = "0.6.1" }
serenity = { version = "0.12.0", features = ["cache", "framework", "standard_framework", "voice"] } rand = "0.8.5"
songbird = { version = "0.4.0", features = ["builtin-queue", "serenity"] } regex = "1.10.6"
symphonia = "0.5.3" reqwest = { version = "0.11.27", features = ["json"] }
tokio = { version = "1.35.1", features = ["macros", "full", "signal"] } serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122"
songbird = { version = "0.4.6", default-features = true, features = [
"builtin-queue",
], git = "https://github.com/eRgo35/songbird" }
serenity = { default-features = true, features = [
"cache",
"framework",
"standard_framework",
"voice",
"http",
"rustls_backend",
], version = "0.12" }
symphonia = { version = "0.5.4", features = [
"aac",
"adpcm",
"alac",
"flac",
"mpa",
"isomp4",
] }
tokio = { version = "1.39.2", features = ["macros", "full", "signal"] }
tracing = "0.1.40" tracing = "0.1.40"
tracing-futures = "0.2.5" tracing-futures = "0.2.5"
tracing-subscriber = "0.3.18" tracing-subscriber = "0.3.18"
url = "2.5.2"
once_cell = "1.19.0"
[patch.crates-io.serenity-voice-model]
git = "https://github.com/serenity-rs/serenity"
branch = "current"
# The profile that 'cargo dist' will build with
[profile.dist]
inherits = "release"
lto = "thin"
# Config for 'cargo dist'
[workspace.metadata.dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.20.0"
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = ["shell"]
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["x86_64-unknown-linux-gnu"]
# Path that installers should place binaries in
install-path = "CARGO_HOME"
# Whether to install an updater program
install-updater = false

View File

@@ -1,19 +0,0 @@
FROM rust:1.71-alpine
RUN apk add --update \
alpine-sdk \
ffmpeg \
youtube-dl \
pkgconfig \
cmake \
openssl-dev \
musl-dev \
openssl
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/lyra"]

View File

@@ -1,7 +1,21 @@
# License MIT License
Copyright (C) 2024 Michał Czyż Copyright (c) 2024 Michał Czyż
All Rights Reserved. Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
This build is private and shall not be distributed nor modified without permission. The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

120
README.md
View File

@@ -1,12 +1,120 @@
# Lyra <h2 align="center">
<a href="https://lyra.c2yz.com" target="blank_">
<img height="256" alt="Lyra" src="assets/lyra-256.png" />
</a>
<br />
Lyra: a discord music bot written in rust :crab:
</h2>
![](assets/lyra-256.png) Lyra is an open source, discord music bot written in Rust.
The idea behind this project is to allow a user to self-host one's own instance of the bot.
User no longer has to rely on 3rd-parties to provide them a service.
The bot can be run on a desktop, a vps or a phone as it's just a simple binary.
Lyra is a music bot written in Rust. Currently bot is still heavily in development!
More features coming soon!
## Getting Started ## Getting started
## Building Before you start, you need to create a discord bot and get a token.
You can do it [here](https://discord.com/developers/applications).
After you create a bot, you need to invite it to your server.
Then, head to download section and download the latest release (or compile it yourself).
After you download the binary, you need to create a `.env` file in the same directory as the binary.
Example can be found in `.env.example` file.
```
DISCORD_TOKEN=<YOUR_DISCORD_TOKEN>
PREFIX=<YOUR_PREFIX>
```
DISCORD_TOKEN is the token you got from discord developers page and PREFIX is the prefix you want to use for your bot.
Bot by default reacts only to the prefix. To enable slash commands, while the bot is running type `:register` in the chat (where `:` is your bot prefix).
## Features
- Music playback
- Audio effects (soon)
- Some multipurpose commands
- Slash commands
- Self-hosted
## Compilation
To compile the source code on your own, you need `rust` and `cargo`
To run a dev version, `cd` into the project directory and type
```bash
$ cargo run
```
To build a production version use
```bash
$ cargo build --release
```
If you need a version for a different system or architecture, you can use `cross` crate
```bash
$ cross build -r --target aarch64-unknown-linux-gnu
```
To run a program, just type
```bash
$ ./lyra
```
Remember to provide a `.env` file in the same directory as the binary.
If you want to disown the bot from the shell, I recommend using the script I provided in `scripts` folder
## Commands ## Commands
As of now, the commands are:
```
Music:
/deafen Deafens itself while in a voice channel; aliases: deafen, undeaden, shuush
/join Joins your voice channel
/leave Leaves the voice channel; aliases: leave, qa!
/mute Mutes itself while in a voice channel; aliases: mute, unmute, shhh
/pause Pauses the currently playing song
/play Plays a song; you can search by query or paste an url; aliases: play, p, enqueue
/queue Shows next tracks in queue; aliases: queue, q
/repeat Loops currently playing song provided amount of times; aliases: repeat, loop, while, for
/resume Resumes currently paused song
/seek Seeks a track by provided seconds
/skip Skips the currently playing song; aliases: skip, :skipper:
/stop Stops playback and destroys the queue; aliases: stop, end
/volume Changes output volume
/effect Plays one of available audio effects
/stream Hijacks output and plays audio; search by query or paste an url; aliases: stream, override, hijack
Tools:
/ai Asks AI
/dice Rolls a dice
/dictionary Explains provided query
/ip Shows IP information
/metar Prints metar for provided airport
/owoify Owoifies whatever you want uwu
/ping Pings you backs with a response time
/posix Prints current time in POSIX format
/qr Creates a qr code from text
/taf Returns taf for provided airport
/uptime Checks how long the bot has been running
/verse Reference Bible by verse
/weather Shows weather for provided location
Help:
/help Prints this help message; aliases: help, huh, welp
Use /help command for more info on a command.
You can edit you message to the bot and the bot will edit its response.
```

BIN
assets/lyra-nightly.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

View File

@@ -1,6 +0,0 @@
version: '2'
services:
melvinjs:
container_name: lyra
build: .

46
flake.lock generated Normal file
View File

@@ -0,0 +1,46 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1734119587,
"narHash": "sha256-AKU6qqskl0yf2+JdRdD0cfxX4b9x3KKV5RqA6wijmPM=",
"rev": "3566ab7246670a43abd2ffa913cc62dad9cdf7d5",
"revCount": 721821,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.721821%2Brev-3566ab7246670a43abd2ffa913cc62dad9cdf7d5/0193cb18-1103-723d-8c38-29b3e808b002/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.%2A.tar.gz"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1734230139,
"narHash": "sha256-zsp0Mz8VgyIAnU8UhP/YT1g+zlsl+NIJTBMAbY+RifQ=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "150fbc8aa2bc501041810bbc1dbfe73694a861be",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

87
flake.nix Normal file
View File

@@ -0,0 +1,87 @@
{
description = "A Nix-flake-based Rust development environment";
inputs = {
nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.*.tar.gz";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
rust-overlay,
}:
let
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forEachSupportedSystem =
f:
nixpkgs.lib.genAttrs supportedSystems (
system:
f {
pkgs = import nixpkgs {
inherit system;
overlays = [
rust-overlay.overlays.default
self.overlays.default
];
};
}
);
in
{
overlays.default = final: prev: {
rustToolchain =
let
rust = prev.rust-bin;
in
if builtins.pathExists ./rust-toolchain.toml then
rust.fromRustupToolchainFile ./rust-toolchain.toml
else if builtins.pathExists ./rust-toolchain then
rust.fromRustupToolchainFile ./rust-toolchain
else
rust.stable.latest.default.override {
extensions = [
"rust-src"
"rustfmt"
];
};
};
devShells = forEachSupportedSystem (
{ pkgs }:
{
default = pkgs.mkShell {
packages = with pkgs; [
rustToolchain
openssl
pkg-config
cargo-deny
cargo-edit
cargo-watch
rust-analyzer
cmake
opusTools
opusfile
rustup
cargo-cross
gcc
];
env = {
# Required by rust-analyzer
RUST_SRC_PATH = "${pkgs.rustToolchain}/lib/rustlib/src/rust/library";
};
};
}
);
};
}

View File

@@ -1,3 +1,3 @@
#!/bin/bash #!/bin/bash
nohup ../lyra > lyra.log 2> lyra.err < /dev/null & nohup ./lyra > lyra.log 2> lyra.err < /dev/null &

View File

@@ -1,3 +1,3 @@
pub mod kashi; pub mod embeds;
pub mod music; pub mod music;
pub mod tools; pub mod tools;

56
src/commands/embeds.rs Normal file
View File

@@ -0,0 +1,56 @@
use crate::{Context, Error};
use poise::serenity_prelude::{
Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
};
use poise::CreateReply;
pub async fn fail(ctx: Context<'_>, err: String) -> Result<(), Error> {
ctx.send(
CreateReply::default().embed(
error_embed(ctx, &format!("Failed: {:?}", err))
.await
.unwrap(),
),
)
.await?;
Ok(())
}
pub async fn error_embed(ctx: Context<'_>, msg: &str) -> Result<CreateEmbed, Error> {
let embed = CreateEmbed::default()
.author(
CreateEmbedAuthor::new("Something went wrong!").icon_url(ctx.author().clone().face()),
)
.colour(Color::from_rgb(255, 58, 97))
.title("Oopsie, Doopsie!")
.description(msg)
.timestamp(Timestamp::now())
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}
pub async fn embed(
ctx: Context<'_>,
author: &str,
description: &str,
title: &str,
) -> Result<CreateEmbed, Error> {
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new(author).icon_url(ctx.author().clone().face()))
.colour(Color::from_rgb(255, 58, 97))
.title(title)
.description(description)
.timestamp(Timestamp::now())
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -1,3 +0,0 @@
pub mod kashi;
pub use kashi::kashi;

View File

@@ -1,10 +0,0 @@
use crate::{Context, Error};
#[poise::command(prefix_command, slash_command)]
pub async fn kashi(ctx: Context<'_>) -> Result<(), Error> {
let response = format!("Kashi platform is currently under construction!");
ctx.say(response).await?;
Ok(())
}

View File

@@ -1,3 +0,0 @@
pub mod kashi;
pub mod music;
pub mod tools;

View File

@@ -1,15 +1,19 @@
pub mod deafen; pub mod deafen;
pub mod join; pub mod join;
pub mod leave; pub mod leave;
pub mod misc; pub mod metadata;
pub mod mute; pub mod mute;
pub mod notifier;
pub mod pause; pub mod pause;
pub mod play; pub mod play;
pub mod queue; pub mod queue;
pub mod repeat; pub mod repeat;
pub mod resume; pub mod resume;
pub mod seek;
pub mod skip; pub mod skip;
pub mod soundboard;
pub mod stop; pub mod stop;
pub mod volume;
pub use deafen::deafen; pub use deafen::deafen;
pub use join::join; pub use join::join;
@@ -20,5 +24,7 @@ pub use play::play;
pub use queue::queue; pub use queue::queue;
pub use repeat::repeat; pub use repeat::repeat;
pub use resume::resume; pub use resume::resume;
pub use seek::seek;
pub use skip::skip; pub use skip::skip;
pub use stop::stop; pub use stop::stop;
pub use volume::volume;

View File

@@ -1,10 +1,21 @@
use crate::{Context, Error}; use crate::{
commands::embeds::{embed, error_embed, fail},
Context, Error,
};
use poise::CreateReply;
#[poise::command(prefix_command, slash_command)] /// Deafens itself while in a voice channel; \
/// aliases: deafen, undeaden, shuush
#[poise::command(
prefix_command,
slash_command,
aliases("shuush", "undeafen"),
category = "Music"
)]
pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> { pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -12,7 +23,9 @@ pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> {
let handler_lock = match manager.get(guild_id) { let handler_lock = match manager.get(guild_id) {
Some(handler) => handler, Some(handler) => handler,
None => { None => {
ctx.say("Not in a voice channel").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(()); return Ok(());
} }
@@ -22,16 +35,18 @@ pub async fn deafen(ctx: Context<'_>) -> Result<(), Error> {
if handler.is_deaf() { if handler.is_deaf() {
if let Err(err) = handler.deafen(false).await { if let Err(err) = handler.deafen(false).await {
ctx.say(format!("Failed: {:?}", err)).await?; fail(ctx, err.to_string()).await.unwrap();
} }
ctx.say("Undeafened").await?; ctx.send(CreateReply::default().embed(embed(ctx, "Undeafened!", "", "").await.unwrap()))
.await?;
} else { } else {
if let Err(err) = handler.deafen(true).await { if let Err(err) = handler.deafen(true).await {
ctx.say(format!("Failed: {:?}", err)).await?; fail(ctx, err.to_string()).await.unwrap();
} }
ctx.say("Deafened").await?; ctx.send(CreateReply::default().embed(embed(ctx, "Deafened!", "", "").await.unwrap()))
.await?;
} }
Ok(()) Ok(())

View File

@@ -1,21 +1,33 @@
use crate::{Context, Error}; use crate::commands::music::notifier::TrackErrorNotifier;
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use songbird::TrackEvent; use songbird::TrackEvent;
use crate::commands::music::misc::TrackErrorNotifier;
#[poise::command(prefix_command, slash_command)] /// Joins your voice channel
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn join(ctx: Context<'_>) -> Result<(), Error> { pub async fn join(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let channel_id = ctx.guild().unwrap().voice_states.get(&ctx.author().id).and_then(|voice_state| voice_state.channel_id); let channel_id = ctx
.guild()
.unwrap()
.voice_states
.get(&ctx.author().id)
.and_then(|voice_state| voice_state.channel_id);
let connect_to = match channel_id { let connect_to = match channel_id {
Some(channel) => channel, Some(channel) => channel,
None => { None => {
ctx.say("Not in a voice channel").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(()); return Ok(());
} }
}; };
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -25,7 +37,8 @@ pub async fn join(ctx: Context<'_>) -> Result<(), Error> {
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
} }
ctx.say("Joined the voice channel").await?; ctx.send(CreateReply::default().embed(embed(ctx, "Joined!", "Hi there!", "").await.unwrap()))
.await?;
Ok(()) Ok(())
} }

View File

@@ -1,24 +1,45 @@
use crate::{Context, Error}; use crate::{
commands::embeds::{embed, error_embed, fail},
Context, Error,
};
use poise::CreateReply;
#[poise::command(prefix_command, slash_command)] /// Leaves the voice channel; \
/// aliases: leave, qa!
#[poise::command(
prefix_command,
slash_command,
aliases("leave", "qa!"),
category = "Music"
)]
pub async fn leave(ctx: Context<'_>) -> Result<(), Error> { pub async fn leave(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
if !manager.get(guild_id).is_some() { if manager.get(guild_id).is_none() {
ctx.say("Not in a voice channel").await?; let msg = "I am not in a voice channel!";
return Ok(()) ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(());
} }
if let Err(err) = manager.remove(guild_id).await { if let Err(err) = manager.remove(guild_id).await {
ctx.say(format!("Failed: {:?}", err)).await?; fail(ctx, err.to_string()).await.unwrap();
} }
ctx.say("Left voice channel").await?; ctx.send(
CreateReply::default().embed(
embed(ctx, "Left!", "I left the voice channel", "")
.await
.unwrap(),
),
)
.await?;
Ok(()) Ok(())
} }

View File

@@ -0,0 +1,7 @@
use songbird::{input::AuxMetadata, typemap::TypeMapKey};
pub struct Metadata;
impl TypeMapKey for Metadata {
type Value = AuxMetadata;
}

View File

@@ -1,10 +1,21 @@
use crate::{Context, Error}; use crate::{
commands::embeds::{embed, error_embed, fail},
Context, Error,
};
use poise::CreateReply;
#[poise::command(prefix_command, slash_command)] /// Mutes itself while in a voice channel; \
/// aliases: mute, unmute, shhh
#[poise::command(
prefix_command,
slash_command,
aliases("shhh", "unmute"),
category = "Music"
)]
pub async fn mute(ctx: Context<'_>) -> Result<(), Error> { pub async fn mute(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -12,8 +23,9 @@ pub async fn mute(ctx: Context<'_>) -> Result<(), Error> {
let handler_lock = match manager.get(guild_id) { let handler_lock = match manager.get(guild_id) {
Some(handler) => handler, Some(handler) => handler,
None => { None => {
ctx.say("Not in a voice channel").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(()); return Ok(());
} }
}; };
@@ -21,17 +33,19 @@ pub async fn mute(ctx: Context<'_>) -> Result<(), Error> {
let mut handler = handler_lock.lock().await; let mut handler = handler_lock.lock().await;
if handler.is_mute() { if handler.is_mute() {
if let Err(e) = handler.mute(false).await { if let Err(err) = handler.mute(false).await {
ctx.say(format!("failed: {:?}", e)).await?; fail(ctx, err.to_string()).await.unwrap();
} }
ctx.say("Unmuted").await?; ctx.send(CreateReply::default().embed(embed(ctx, "Unmuted!", "", "").await.unwrap()))
.await?;
} else { } else {
if let Err(err) = handler.mute(true).await { if let Err(err) = handler.mute(true).await {
ctx.say(format!("Failed: {:?}", err)).await?; fail(ctx, err.to_string()).await.unwrap();
} }
ctx.say("Muted").await?; ctx.send(CreateReply::default().embed(embed(ctx, "Muted!", "", "").await.unwrap()))
.await?;
} }
Ok(()) Ok(())

View File

@@ -1,10 +1,15 @@
use crate::{Context, Error}; use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
#[poise::command(prefix_command, slash_command)] /// Pauses the currently playing song
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn pause(ctx: Context<'_>) -> Result<(), Error> { pub async fn pause(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -14,9 +19,18 @@ pub async fn pause(ctx: Context<'_>) -> Result<(), Error> {
let queue = handler.queue(); let queue = handler.queue();
let _ = queue.pause(); let _ = queue.pause();
ctx.say(format!("Song paused.")).await?; ctx.send(
CreateReply::default().embed(
embed(ctx, "Paused!", "Currently playing song is now paused!", "")
.await
.unwrap(),
),
)
.await?;
} else { } else {
ctx.say("Not in a voice channel to play in").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
} }
Ok(()) Ok(())

View File

@@ -1,160 +1,239 @@
use crate::{Context, Error}; use crate::commands::music::metadata::Metadata;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::{commands::embeds::error_embed, Context, Error};
use fancy_regex::Regex; use poise::serenity_prelude::{
use regex::Regex as Regex_Classic; Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
use std::process::Command; };
use std::time::Duration;
use poise::CreateReply; use poise::CreateReply;
use poise::serenity_prelude::CreateEmbed; use regex::Regex as Regex_Classic;
use poise::serenity_prelude::Colour; use reqwest::Client;
use poise::serenity_prelude::model::Timestamp; use serenity::all::GuildId;
use serenity::builder::CreateEmbedAuthor; use songbird::events::TrackEvent;
use serenity::builder::CreateEmbedFooter;
use songbird::input::AuxMetadata; use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl}; use songbird::input::{Compose, YoutubeDl};
use songbird::events::TrackEvent; use songbird::tracks::TrackQueue;
use songbird::Call;
use crate::commands::music::misc::TrackErrorNotifier; use spotify_parser;
use crate::http::HttpKey; use std::collections::VecDeque;
use std::process::Command;
use std::time::Duration;
/// Plays a song; \
/// you can search by query or paste an url; \
/// aliases: play, p, enqueue
#[poise::command( #[poise::command(
prefix_command, prefix_command,
slash_command, slash_command,
aliases("p", "enqueue") aliases("p", "enqueue"),
category = "Music"
)] )]
pub async fn play( pub async fn play(
ctx: Context<'_>, ctx: Context<'_>,
#[description = "Provide a query or an url"] #[rest] mut song: String, #[description = "Provide a query or an url"]
#[rest]
song: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let regex_spotify = Regex::new(r"https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-)+)(?:(?=\?)(?:[?&]foo=(\d*)(?=[&#]|$)|(?![?&]foo=)[^#])+)?(?=#|$)").unwrap();
let regex_youtube = Regex_Classic::new(r#""url": "(https://www.youtube.com/watch\?v=[A-Za-z0-9]{11})""#).unwrap();
let regex_youtube_playlist = Regex::new(r"^((?:https?:)\/\/)?((?:www|m)\.)?((?:youtube\.com)).*(youtu.be\/|list=)([^#&?]*).*").unwrap();
let is_playlist = regex_youtube_playlist.is_match(&song).unwrap();
let is_spotify = regex_spotify.is_match(&song).unwrap();
let is_query = !song.starts_with("http");
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let channel_id = ctx.guild().unwrap().voice_states.get(&ctx.author().id).and_then(|voice_state| voice_state.channel_id); let http_client = ctx.data().http_client.clone();
let manager = songbird::get(ctx.serenity_context()).await.unwrap().clone();
let mut rest_playlist: VecDeque<String> = VecDeque::new();
if manager.get(guild_id).is_none() {
let channel_id = ctx
.guild()
.unwrap()
.voice_states
.get(&ctx.author().id)
.and_then(|voice_state| voice_state.channel_id);
let connect_to = match channel_id { let connect_to = match channel_id {
Some(channel) => channel, Some(channel) => channel,
None => { None => {
ctx.say("Not in a voice channel").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
return Ok(()); return Ok(());
} }
}; };
let http_client = {
let data = ctx.serenity_context().data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
let manager = songbird::get(&ctx.serenity_context())
.await
.expect("Songbird Voice placed at init")
.clone();
if let Ok(handler_lock) = manager.join(guild_id, connect_to).await { if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
let mut handler = handler_lock.lock().await; let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier); handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
if is_playlist { rest_playlist = handle_play(ctx, song, handler, http_client.clone())
let raw_list = Command::new("yt-dlp") .await
.args(["-j", "--flat-playlist", &song]) .unwrap();
.output()
.expect("failed to execute process")
.stdout;
let list = String::from_utf8(raw_list.clone()).expect("Invalid UTF-8");
let urls: Vec<String> = regex_youtube.captures_iter(&list).map(|capture| capture[1].to_string()).collect();
let mut sources: Vec<YoutubeDl> = vec![];
for url in urls {
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client.clone(), url);
let _ = handler.enqueue_input(src.clone().into()).await;
sources.push(src);
}
let embed = generate_playlist_embed(ctx, sources).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
} else { } else {
if is_spotify { let msg = "Failed to join the voice channel!";
let exec = format!("node ./src/spotify --url {}", song); ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
let query = Command::new("sh").arg("-c").arg(exec).output().expect("failed to execute process").stdout; .await?;
let query_str = String::from_utf8(query.clone()).expect("Invalid UTF-8"); }
song = format!("ytsearch:{}", query_str.to_string()); } else {
let handler = manager.get(guild_id).unwrap();
let mut handler = handler.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
rest_playlist = handle_play(ctx, song, handler, http_client.clone())
.await
.unwrap();
} }
if is_query { handle_playlist(rest_playlist, manager, guild_id, http_client)
song = format!("ytsearch:{}", song); .await
.unwrap();
Ok(())
} }
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client, song); async fn handle_play<'a>(
let _ = handler.enqueue_input(src.clone().into()).await; ctx: Context<'a>,
song: String,
mut handler: tokio::sync::MutexGuard<'a, Call>,
http_client: Client,
) -> Result<VecDeque<String>, Error> {
let mut results = parse_data(song).await;
let embed = generate_embed(ctx, src).await; let src: YoutubeDl =
let response = CreateReply::default().embed(embed.unwrap()); YoutubeDl::new_ytdl_like("yt-dlp", http_client, results.pop_front().unwrap());
ctx.send(response).await?;
ctx.send(
CreateReply::default().embed(
generate_embed(ctx, src.clone(), handler.queue(), results.clone())
.await
.unwrap(),
),
)
.await?;
let aux_metadata = src.clone().aux_metadata().await.unwrap();
handler
.enqueue_input(src.clone().into())
.await
.typemap()
.write()
.await
.insert::<Metadata>(aux_metadata);
Ok(results)
}
async fn parse_data(data: String) -> VecDeque<String> {
let tracks = spotify_parser::retrieve_async_url(&data)
.await
.unwrap_or(vec![data])
.iter()
.flat_map(|track| {
if track.contains("?list=") {
let regex_youtube = Regex_Classic::new(
r#""url": "(https://www.youtube.com/watch\?v=[A-Za-z0-9]{11})""#,
)
.unwrap();
let list = Command::new("yt-dlp")
.args(["--username", "oauth2", "--password", "''"])
.args(["-j", "--flat-playlist", track])
.output()
.expect("Failed to execute process")
.stdout;
let list = String::from_utf8(list).unwrap();
regex_youtube
.captures_iter(&list)
.map(|capture| capture.get(1).unwrap().as_str().to_string())
.collect::<Vec<String>>()
} else if track.starts_with("http") {
vec![track.clone()]
} else {
vec![format!("ytsearch:{}", track)]
}
})
.collect();
tracks
}
async fn handle_playlist(
playlist: VecDeque<String>,
manager: std::sync::Arc<songbird::Songbird>,
guild_id: GuildId,
http_client: Client,
) -> Result<(), Error> {
for song in playlist {
if manager.get(guild_id).is_some() {
let handler = manager.get(guild_id).unwrap();
let mut handler = handler.lock().await;
let src: YoutubeDl =
YoutubeDl::new_ytdl_like("yt-dlp", http_client.clone(), song.clone());
let aux_metadata = src.clone().aux_metadata().await.unwrap();
handler
.enqueue_input(src.clone().into())
.await
.typemap()
.write()
.await
.insert::<Metadata>(aux_metadata);
} }
} }
Ok(()) Ok(())
} }
async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed, Error> { async fn generate_embed(
ctx: Context<'_>,
src: YoutubeDl,
queue: &TrackQueue,
results: VecDeque<String>,
) -> Result<CreateEmbed, Error> {
let metadata = src.clone().aux_metadata().await.unwrap(); let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata {title, thumbnail, source_url, artist, duration, ..} = metadata; let AuxMetadata {
let timestamp = Timestamp::now(); title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60; let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60; let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let mut description = format!("Enqueued @ {}", queue.len() + 1);
let mut tracks = "Tracks enqueued";
let embed = CreateEmbed::default() if results.len() == 1 {
.author(CreateEmbedAuthor::new("Track enqueued").icon_url(ctx.author().clone().face())) tracks = "Track enqueued";
.colour(Colour::from_rgb(255, 58, 97))
.title(title.unwrap())
.url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))
.field("Artist", artist.unwrap_or("Unknown Artist".to_string()), true)
.field("Duration", format!("{:02}:{:02}", duration_minutes, duration_seconds), true)
.field("DJ", ctx.author().name.clone(), true)
.timestamp(timestamp)
.footer(CreateEmbedFooter::new(ctx.cache().current_user().name.to_string()).icon_url(ctx.cache().current_user().face()));
Ok(embed)
} }
async fn generate_playlist_embed(ctx: Context<'_>, sources: Vec<YoutubeDl>) -> Result<CreateEmbed, Error> { if queue.is_empty() {
let src = sources.get(0).unwrap(); description = "Playing now!".to_string();
}
let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata {title, thumbnail, source_url, artist, duration, ..} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Enqueued tracks: {}", sources.len() - 1);
let embed = CreateEmbed::default() let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Playlist enqueued").icon_url(ctx.author().clone().face())) .author(CreateEmbedAuthor::new(tracks).icon_url(ctx.author().clone().face()))
.colour(Colour::from_rgb(255, 58, 97)) .colour(Color::from_rgb(255, 58, 97))
.title(title.unwrap()) .title(title.unwrap())
.url(source_url.unwrap()) .url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face())) .thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))
.field("Artist", artist.unwrap_or("Unknown Artist".to_string()), true) .field(
.field("Duration", format!("{:02}:{:02}", duration_minutes, duration_seconds), true) "Artist",
artist.unwrap_or("Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true) .field("DJ", ctx.author().name.clone(), true)
.description(description) .description(description)
.timestamp(timestamp) .timestamp(Timestamp::now())
.footer(CreateEmbedFooter::new(ctx.cache().current_user().name.to_string()).icon_url(ctx.cache().current_user().face())); .footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed) Ok(embed)
} }

View File

@@ -1,10 +1,22 @@
use crate::{Context, Error}; use std::time::Duration;
#[poise::command(prefix_command, slash_command)] use crate::commands::music::metadata::Metadata;
use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::{
Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
};
use poise::CreateReply;
use songbird::input::AuxMetadata;
const QUEUE_DISPLAY_LENGTH: usize = 10;
/// Shows next tracks in queue; \
/// aliases: queue, q
#[poise::command(prefix_command, slash_command, aliases("q"), category = "Music")]
pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { pub async fn queue(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -12,23 +24,68 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> {
if let Some(handler_lock) = manager.get(guild_id) { if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await; let handler = handler_lock.lock().await;
let queue = handler.queue(); let queue = handler.queue();
let mut queue_res = String::from("Queue: \n"); let mut queue_res = String::from("");
let mut too_long = false;
for (index, song) in queue.clone().current_queue().iter().enumerate() {
let meta_typemap = song.typemap().read().await;
let metadata = meta_typemap.get::<Metadata>().unwrap();
let AuxMetadata {
title,
artist,
duration,
..
} = metadata;
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
for (i, song) in queue.current_queue().iter().enumerate() {
queue_res.push_str(&format!( queue_res.push_str(&format!(
"{}. {} - {}\n", "{}. {} - {} [{:02}:{:02}] \n",
i + 1, index,
song.uuid(), title.as_ref().unwrap(),
"Artist" artist.as_ref().unwrap(),
// song.metadata().artist.clone().unwrap_or_else(|| String::from("Unknown")) duration_minutes,
duration_seconds
));
if index + 1 == QUEUE_DISPLAY_LENGTH {
too_long = true;
break;
}
}
if too_long {
queue_res.push_str(&format!(
"and {} more...",
queue.len() - QUEUE_DISPLAY_LENGTH
)); ));
} }
ctx.say(queue_res).await?; ctx.send(CreateReply::default().embed(embed(ctx, queue_res).await.unwrap()))
.await?;
} else { } else {
ctx.say("Not in a voice channel!").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
} }
Ok(()) Ok(())
} }
async fn embed(ctx: Context<'_>, queue: String) -> Result<CreateEmbed, Error> {
let title = "Now playing";
let timestamp = Timestamp::now();
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Queue").icon_url(ctx.author().clone().face()))
.colour(Color::from_rgb(255, 58, 97))
.title(title)
.description(queue)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -1,16 +1,31 @@
use crate::{Context, Error}; use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use songbird::tracks::LoopState; use songbird::tracks::LoopState;
#[poise::command(prefix_command, slash_command)] /// Loops currently playing song provided amount of times; \
pub async fn repeat(ctx: Context<'_>, times: usize) -> Result<(), Error> { /// aliases: repeat, loop, while, for
#[poise::command(
prefix_command,
slash_command,
aliases("loop", "while", "for"),
category = "Music"
)]
pub async fn repeat(
ctx: Context<'_>,
#[description = "How many times"]
#[rest]
times: usize,
) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
if let Some(handler_lock) = manager.get(guild_id) { if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await; let handler = handler_lock.lock().await;
let queue = handler.queue(); let queue = handler.queue();
@@ -22,21 +37,53 @@ pub async fn repeat(ctx: Context<'_>, times: usize) -> Result<(), Error> {
LoopState::Infinite => { LoopState::Infinite => {
let _ = queue.current().unwrap().disable_loop(); let _ = queue.current().unwrap().disable_loop();
ctx.say("Song unlooped.").await?; ctx.send(
CreateReply::default()
.embed(embed(ctx, "Song Unlooped!", "", "").await.unwrap()),
)
.await?;
} }
LoopState::Finite(_) => { LoopState::Finite(_) => {
if times < 100 { if times == 0 {
let _ = queue.current().unwrap().disable_loop();
ctx.send(
CreateReply::default()
.embed(embed(ctx, "Song Unlooped!", "", "").await.unwrap()),
)
.await?;
} else if times < 100 {
let _ = queue.current().unwrap().loop_for(times); let _ = queue.current().unwrap().loop_for(times);
ctx.say("Song looped forever (a very long time)").await?; ctx.send(
} CreateReply::default().embed(
else { embed(
ctx,
&format!("Song looped {} times!", times),
"You definitelly love this song!",
"",
)
.await
.unwrap(),
),
)
.await?;
} else {
let _ = queue.current().unwrap().enable_loop(); let _ = queue.current().unwrap().enable_loop();
ctx.say(format!("Song looped {} times.", times)).await?; ctx.send(
CreateReply::default().embed(
embed(ctx, "Song looped forever!", "A very long time!", "")
.await
.unwrap(),
),
)
.await?;
} }
} }
} }
} else { } else {
ctx.say("Not in a voice channel to play in").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
} }
Ok(()) Ok(())

View File

@@ -1,10 +1,15 @@
use crate::{Context, Error}; use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
#[poise::command(prefix_command, slash_command)] /// Resumes currently paused song
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn resume(ctx: Context<'_>) -> Result<(), Error> { pub async fn resume(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -14,9 +19,18 @@ pub async fn resume(ctx: Context<'_>) -> Result<(), Error> {
let queue = handler.queue(); let queue = handler.queue();
let _ = queue.resume(); let _ = queue.resume();
ctx.say(format!("Song resumed.")).await?; ctx.send(
CreateReply::default().embed(
embed(ctx, "Resumed!", "Currently paused song is now resumed!", "")
.await
.unwrap(),
),
)
.await?;
} else { } else {
ctx.say("Not in a voice channel to play in").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
} }
Ok(()) Ok(())

View File

@@ -0,0 +1,51 @@
use std::time::Duration;
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
/// Seeks a track by provided seconds
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn seek(
ctx: Context<'_>,
#[description = "How many seconds shall I seek"] seek: u64,
) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(ctx.serenity_context())
.await
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let seek_duration = Duration::from_secs(seek);
let track = queue.current().unwrap();
let _ = track.seek(seek_duration);
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"Track seeked!",
&format!("Track seeked by: {} seconds", seek),
"",
)
.await
.unwrap(),
),
)
.await?;
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}

View File

@@ -1,10 +1,23 @@
use crate::{Context, Error}; use crate::commands::music::metadata::Metadata;
use std::time::Duration;
#[poise::command(prefix_command, slash_command)] use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::serenity_prelude::{
Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
};
use poise::CreateReply;
use songbird::{input::AuxMetadata, tracks::TrackHandle};
/// Skips the currently playing song; \
/// aliases: skip, :skipper:
#[poise::command(prefix_command, slash_command, aliases("skipper:"), category = "Music")]
pub async fn skip(ctx: Context<'_>) -> Result<(), Error> { pub async fn skip(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -12,12 +25,83 @@ pub async fn skip(ctx: Context<'_>) -> Result<(), Error> {
if let Some(handler_lock) = manager.get(guild_id) { if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await; let handler = handler_lock.lock().await;
let queue = handler.queue(); let queue = handler.queue();
let _ = queue.skip(); let _ = queue.clone().skip();
let track_raw = queue.clone().current_queue();
let track = track_raw.get(1);
let queue_length = queue.len() - 1;
ctx.say(format!("Song skipped: {} in queue.", queue.len())).await?; let mut response = CreateReply::default().embed(
embed(ctx, "Skipped!", "The queue is empty!", "")
.await
.unwrap(),
);
if let Some(track) = track {
response = CreateReply::default().embed(
generate_embed(ctx, track.clone(), queue_length)
.await
.unwrap(),
);
};
ctx.send(response).await?;
} else { } else {
ctx.say("Not in a voice channel to play in").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
} }
Ok(()) Ok(())
} }
async fn generate_embed(
ctx: Context<'_>,
track: TrackHandle,
queue_length: usize,
) -> Result<CreateEmbed, Error> {
let meta_typemap = track.typemap().read().await;
let metadata = meta_typemap.get::<Metadata>().unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = format!("Song skipped! Queue length is {}", queue_length);
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Skipped!").icon_url(ctx.author().clone().face()))
.colour(Color::from_rgb(255, 58, 97))
.title(title.as_ref().unwrap())
.url(source_url.as_ref().unwrap())
.thumbnail(
thumbnail
.as_ref()
.unwrap_or(&ctx.cache().current_user().face()),
)
.field(
"Artist",
artist.as_ref().unwrap_or(&"Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -0,0 +1,5 @@
pub mod effect;
pub mod stream;
pub use effect::effect;
pub use stream::stream;

View File

@@ -0,0 +1,115 @@
use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::{
Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
};
use poise::CreateReply;
use songbird::events::TrackEvent;
use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl};
use std::time::Duration;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::http::HttpKey;
/// Plays one of available audio effects
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn effect(
ctx: Context<'_>,
#[description = "Shall output pause?"]
#[flag]
pause: bool,
#[description = "Provide a query or an url"]
#[rest]
mut song: String,
) -> Result<(), Error> {
let is_query = !song.starts_with("http");
let guild_id = ctx.guild_id().unwrap();
let http_client = {
let data = ctx.serenity_context().data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
let manager = songbird::get(ctx.serenity_context())
.await
.expect("Songbird Voice placed at init")
.clone();
if pause {
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let _ = queue.pause();
}
}
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
if is_query {
song = format!("ytsearch:{}", song);
}
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client, song);
let embed = generate_embed(ctx, src.clone()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
let _ = handler.play_input(src.clone().into());
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}
async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed, Error> {
let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = "Playing now!";
let embed = CreateEmbed::default()
.author(CreateEmbedAuthor::new("Playing an effect!").icon_url(ctx.author().clone().face()))
.colour(Color::from_rgb(255, 58, 97))
.title(title.unwrap())
.url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))
.field(
"Artist",
artist.unwrap_or("Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -0,0 +1,124 @@
use crate::{commands::embeds::error_embed, Context, Error};
use poise::serenity_prelude::{
Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
};
use poise::CreateReply;
use songbird::events::TrackEvent;
use songbird::input::AuxMetadata;
use songbird::input::{Compose, YoutubeDl};
use std::time::Duration;
use crate::commands::music::notifier::TrackErrorNotifier;
use crate::http::HttpKey;
/// Hijacks output and plays audio; \
/// search by query or paste an url; \
/// aliases: stream, override, hijack
#[poise::command(
prefix_command,
slash_command,
aliases("override", "hijack"),
category = "Music"
)]
pub async fn stream(
ctx: Context<'_>,
#[description = "Shall output pause?"]
#[flag]
pause: bool,
#[description = "Provide a query or an url"]
#[rest]
mut song: String,
) -> Result<(), Error> {
let is_query = !song.starts_with("http");
let guild_id = ctx.guild_id().unwrap();
let http_client = {
let data = ctx.serenity_context().data.read().await;
data.get::<HttpKey>()
.cloned()
.expect("Guaranteed to exist in the typemap.")
};
let manager = songbird::get(ctx.serenity_context())
.await
.expect("Songbird Voice placed at init")
.clone();
if pause {
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let _ = queue.pause();
}
}
if let Some(handler_lock) = manager.get(guild_id) {
let mut handler = handler_lock.lock().await;
handler.add_global_event(TrackEvent::Error.into(), TrackErrorNotifier);
if is_query {
song = format!("ytsearch:{}", song);
}
let src = YoutubeDl::new_ytdl_like("yt-dlp", http_client, song);
let embed = generate_embed(ctx, src.clone()).await;
let response = CreateReply::default().embed(embed.unwrap());
ctx.send(response).await?;
let _ = handler.play_input(src.clone().into());
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}
async fn generate_embed(ctx: Context<'_>, src: YoutubeDl) -> Result<CreateEmbed, Error> {
let metadata = src.clone().aux_metadata().await.unwrap();
let AuxMetadata {
title,
thumbnail,
source_url,
artist,
duration,
..
} = metadata;
let timestamp = Timestamp::now();
let duration_minutes = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() / 60;
let duration_seconds = duration.unwrap_or(Duration::new(0, 0)).clone().as_secs() % 60;
let description = "Playing now!";
let embed = CreateEmbed::default()
.author(
CreateEmbedAuthor::new("Audio output hijacked!").icon_url(ctx.author().clone().face()),
)
.colour(Color::from_rgb(255, 58, 97))
.title(title.unwrap())
.url(source_url.unwrap())
.thumbnail(thumbnail.unwrap_or(ctx.cache().current_user().face()))
.field(
"Artist",
artist.unwrap_or("Unknown Artist".to_string()),
true,
)
.field(
"Duration",
format!("{:02}:{:02}", duration_minutes, duration_seconds),
true,
)
.field("DJ", ctx.author().name.clone(), true)
.description(description)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -1,10 +1,16 @@
use crate::{Context, Error}; use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
#[poise::command(prefix_command, slash_command)] /// Stops playback and destroys the queue; \
/// aliases: stop, end
#[poise::command(prefix_command, slash_command, aliases("end"), category = "Music")]
pub async fn stop(ctx: Context<'_>) -> Result<(), Error> { pub async fn stop(ctx: Context<'_>) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap(); let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(&ctx.serenity_context()) let manager = songbird::get(ctx.serenity_context())
.await .await
.expect("Songbird client placed at init") .expect("Songbird client placed at init")
.clone(); .clone();
@@ -14,9 +20,23 @@ pub async fn stop(ctx: Context<'_>) -> Result<(), Error> {
let queue = handler.queue(); let queue = handler.queue();
queue.stop(); queue.stop();
ctx.say("Playback stopped!").await?; ctx.send(
CreateReply::default().embed(
embed(
ctx,
"Stopped!",
"Playback stopped!",
"Queue destroyed! Bot will stay and chill with you in a vc",
)
.await
.unwrap(),
),
)
.await?;
} else { } else {
ctx.say("Not in a voice channel!").await?; let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
} }
Ok(()) Ok(())

View File

@@ -0,0 +1,44 @@
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
/// Changes output volume
#[poise::command(prefix_command, slash_command, category = "Music")]
pub async fn volume(ctx: Context<'_>, #[description = "Volume"] volume: f32) -> Result<(), Error> {
let guild_id = ctx.guild_id().unwrap();
let manager = songbird::get(ctx.serenity_context())
.await
.expect("Songbird client placed at init")
.clone();
if let Some(handler_lock) = manager.get(guild_id) {
let handler = handler_lock.lock().await;
let queue = handler.queue();
let track = queue.current().unwrap();
let _ = track.set_volume(volume / 100.0);
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"Volume changed",
"",
&format!("Set volume to {}%", volume),
)
.await
.unwrap(),
),
)
.await?;
} else {
let msg = "I am not in a voice channel!";
ctx.send(CreateReply::default().embed(error_embed(ctx, msg).await.unwrap()))
.await?;
}
Ok(())
}

View File

@@ -1,5 +1,31 @@
pub mod ai;
pub mod dice;
pub mod dictionary;
pub mod help;
pub mod ip;
pub mod metar;
pub mod owoify;
pub mod ping; pub mod ping;
pub mod posix;
pub mod qr;
pub mod register; pub mod register;
pub mod taf;
pub mod uptime;
pub mod verse;
pub mod weather;
pub use ai::ai;
pub use dice::dice;
pub use dictionary::dictionary;
pub use help::help;
pub use ip::ip;
pub use metar::metar;
pub use owoify::owoify;
pub use ping::ping; pub use ping::ping;
pub use posix::posix;
pub use qr::qr;
pub use register::register; pub use register::register;
pub use taf::taf;
pub use uptime::uptime;
pub use verse::verse;
pub use weather::weather;

48
src/commands/tools/ai.rs Normal file
View File

@@ -0,0 +1,48 @@
use rand::Rng;
use std::thread::sleep;
use std::time::Duration;
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Asks AI
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn ai(
ctx: Context<'_>,
#[description = "prompt to ask"]
#[rest]
prompt: String,
) -> Result<(), Error> {
let iamsorry = &[
"I'm sorry, but as an AI language model, I must follow ethical guidelines, and I cannot engage in harmful, malicious, or offensive behavior.",
"I'm sorry, but as an AI language model, I may not always be perfect and can make mistakes or provide inaccurate information. Please verify important details from reliable sources.",
"I'm sorry, but as an AI language model, I can't engage in real-time conversations or remember previous interactions with users.",
"I'm sorry, but as an AI language model, I don't have personal opinions or feelings; I can only provide information based on patterns in the data I was trained on.",
"I'm sorry, but as an AI language model, I don't have access to real-time information or updates beyond my last training data in September 2021.",
"I'm sorry, but as an AI language model, I don't have the ability to recall specific personal data or information about individuals.",
"I'm sorry, but as an AI language model, I don't have consciousness or self-awareness. I'm simply a program designed to process and generate human-like text."
];
println!("Funny prompts: {}", prompt);
let response = {
let mut rng = rand::thread_rng();
rng.gen_range(0..iamsorry.len())
};
sleep(Duration::from_secs(1));
ctx.send(
CreateReply::default().embed(
embed(ctx, "AI Response:", "", iamsorry[response])
.await
.unwrap(),
),
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,30 @@
use rand::Rng;
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Rolls a dice
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn dice(ctx: Context<'_>) -> Result<(), Error> {
let dice = {
let mut rng = rand::thread_rng();
rng.gen_range(1..=6)
};
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"Let's roll the dice!",
"",
&format!("Your number is: {}", dice),
)
.await
.unwrap(),
),
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,72 @@
use poise::CreateReply;
use serde::{Deserialize, Serialize};
use url::form_urlencoded;
use crate::{commands::embeds::embed, Context, Error};
/// Explains provided query
#[poise::command(prefix_command, slash_command, aliases("dict"), category = "Tools")]
pub async fn dictionary(
ctx: Context<'_>,
#[description = "Word you're looking for"]
#[rest]
word: String,
) -> Result<(), Error> {
let data: String = form_urlencoded::byte_serialize(word.as_bytes()).collect();
let client = reqwest::Client::new();
let response = client
.get(format!(
"https://api.dictionaryapi.dev/api/v2/entries/en/{}",
data
))
.send()
.await
.unwrap();
match response.status() {
reqwest::StatusCode::OK => match response.json::<Vec<Word>>().await {
Ok(parsed) => {
println!("{:?}", parsed);
ctx.send(CreateReply::default().embed(embed(ctx, "", "", "").await.unwrap()))
.await?;
}
Err(err) => println!("Something is messed up! {:?}", err),
},
reqwest::StatusCode::UNAUTHORIZED => {
println!("Unauthorized.. Uoops!!");
}
error => {
println!("Something went wrong: {:?}", error);
}
}
Ok(())
}
#[derive(Serialize, Deserialize, Debug)]
struct Definition {
definition: String,
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug)]
struct Meaning {
partOfSpeech: String,
definitions: Vec<Definition>,
}
#[allow(non_snake_case)]
#[derive(Serialize, Deserialize, Debug)]
struct Phonetic {
text: Option<String>,
audio: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
struct Word {
word: String,
phonetics: Vec<Phonetic>,
meanings: Vec<Meaning>,
}

View File

@@ -0,0 +1,23 @@
use crate::{Context, Error};
/// Prints this help message; aliases: help, huh, welp
#[poise::command(
prefix_command,
slash_command,
track_edits,
aliases("huh", "welp"),
category = "Help"
)]
pub async fn help(
ctx: Context<'_>,
#[description = "Specific command to show help about"] command: Option<String>,
) -> Result<(), Error> {
let config = poise::builtins::HelpConfiguration {
extra_text_at_bottom: "\
Use /help command for more info on a command.
You can edit you message to the bot and the bot will edit its response.",
..Default::default()
};
poise::builtins::help(ctx, command.as_deref(), config).await?;
Ok(())
}

14
src/commands/tools/ip.rs Normal file
View File

@@ -0,0 +1,14 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Shows IP information
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn ip(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(
CreateReply::default().embed(embed(ctx, "IP Info", "Work in progress", "").await.unwrap()),
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,14 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Prints metar for provided airport
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn metar(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(
CreateReply::default().embed(embed(ctx, "Metar", "Work in progress", "").await.unwrap()),
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,18 @@
use owoify::OwOifiable;
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Owoifies whatever you want uwu
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn owoify(
ctx: Context<'_>,
#[description = "Text to owoify w-woify OwO"]
#[rest]
text: String,
) -> Result<(), Error> {
ctx.send(CreateReply::default().embed(embed(ctx, "OwO", &text.owoify(), "").await.unwrap()))
.await?;
Ok(())
}

View File

@@ -1,11 +1,13 @@
use crate::{Context, Error}; use crate::{Context, Error};
use std::time::SystemTime; use std::time::SystemTime;
#[poise::command(prefix_command, slash_command)] /// Pings you backs with a response time
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
let system_now = SystemTime::now() let system_now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH) .duration_since(SystemTime::UNIX_EPOCH)
.unwrap().as_millis() as i64; .unwrap()
.as_millis() as i64;
let message_now = ctx.created_at().timestamp_millis(); let message_now = ctx.created_at().timestamp_millis();

View File

@@ -0,0 +1,30 @@
use std::time::SystemTime;
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Prints current time in POSIX format
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn posix(ctx: Context<'_>) -> Result<(), Error> {
let time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis();
ctx.send(
CreateReply::default().embed(
embed(
ctx,
"The time is",
"since Jan 1st 1970",
&format!("{} ms", time),
)
.await
.unwrap(),
),
)
.await?;
Ok(())
}

47
src/commands/tools/qr.rs Normal file
View File

@@ -0,0 +1,47 @@
use poise::serenity_prelude::{
Color, CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, Timestamp,
};
use poise::CreateReply;
use crate::{Context, Error};
use url::form_urlencoded;
/// Creates a qr code from text
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn qr(
ctx: Context<'_>,
#[description = "Message to encode"]
#[rest]
message: String,
) -> Result<(), Error> {
let response = CreateReply::default().embed(generate_embed(ctx, message).await.unwrap());
ctx.send(response).await?;
Ok(())
}
async fn generate_embed(ctx: Context<'_>, message: String) -> Result<CreateEmbed, Error> {
let timestamp = Timestamp::now();
let data: String = form_urlencoded::byte_serialize(message.as_bytes()).collect();
let url = format!(
"http://api.qrserver.com/v1/create-qr-code/?data={}&size=1000x1000&ecc=Q&margin=8",
data
);
let embed = CreateEmbed::default()
.author(
CreateEmbedAuthor::new("Your message as a QR Code!")
.icon_url(ctx.author().clone().face()),
)
.colour(Color::from_rgb(255, 58, 97))
.title("Your QR Code:")
.url(url.clone())
.image(url)
.timestamp(timestamp)
.footer(
CreateEmbedFooter::new(ctx.cache().current_user().name.to_string())
.icon_url(ctx.cache().current_user().face()),
);
Ok(embed)
}

View File

@@ -1,12 +1,7 @@
use crate::{Context, Error}; use crate::{Context, Error};
#[poise::command(prefix_command, check = "check")] #[poise::command(prefix_command, hide_in_help, owners_only)]
pub async fn register(ctx: Context<'_>) -> Result<(), Error> { pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
poise::builtins::register_application_commands_buttons(ctx).await?; poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(()) Ok(())
} }
async fn check(ctx: Context<'_>) -> Result<bool, Error> {
let owner = std::env::var("OWNER_ID").expect("Environment variable `OWNER_ID` not found");
Ok(ctx.author().id.to_string() == owner)
}

14
src/commands/tools/taf.rs Normal file
View File

@@ -0,0 +1,14 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Returns taf for provided airport
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn taf(ctx: Context<'_>) -> Result<(), Error> {
ctx.send(
CreateReply::default().embed(embed(ctx, "Taf", "Work in progress", "").await.unwrap()),
)
.await?;
Ok(())
}

View File

@@ -0,0 +1,48 @@
use once_cell::sync::Lazy;
use poise::CreateReply;
use std::sync::Mutex;
use crate::{commands::embeds::embed, Context, Error};
pub static PROCESS_UPTIME: Lazy<Mutex<std::time::SystemTime>> =
Lazy::new(|| Mutex::new(std::time::SystemTime::now()));
/// Checks how long the bot has been running
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn uptime(ctx: Context<'_>) -> Result<(), Error> {
let start = *PROCESS_UPTIME.lock().unwrap();
let uptime = std::time::SystemTime::now().duration_since(start).unwrap();
let (days, hours, minutes, seconds) = (
uptime.as_secs() / 86400,
(uptime.as_secs() / 3600) % 24,
(uptime.as_secs() / 60) % 60,
uptime.as_secs() % 60,
);
let mut message = format!(
"I have been awake for {} days, {} hours, {} minutes and {} seconds!",
days, hours, minutes, seconds
);
if days != 0 {
message = format!("I have been awake for {} days!", days);
}
if days == 0 && hours != 0 {
message = format!("I have been awake for {} hours!", hours);
}
if days == 0 && hours == 0 && minutes != 0 {
message = format!("I have been awake for {} minutes!", minutes);
}
if days == 0 && hours == 0 && minutes == 0 && seconds != 0 {
message = format!("I have been awake for {} seconds!", seconds);
}
ctx.send(CreateReply::default().embed(embed(ctx, "Uptime", "", &message).await.unwrap()))
.await?;
Ok(())
}

View File

@@ -0,0 +1,78 @@
use crate::{
commands::embeds::{embed, error_embed},
Context, Error,
};
use poise::CreateReply;
use serde::{Deserialize, Serialize};
use url::form_urlencoded;
/// Reference Bible by verse
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn verse(
ctx: Context<'_>,
#[description = "Latin?"]
#[flag]
latin: bool,
#[description = "BOOK+CHAPTER:VERSE"]
#[rest]
verse: String,
) -> Result<(), Error> {
let data: String = form_urlencoded::byte_serialize(verse.as_bytes()).collect();
let translation = if latin { "clementine" } else { "web" };
let client = reqwest::Client::new();
let response = client
.get(format!(
"https://bible-api.com/{}?translation={}",
data, translation
))
.send()
.await
.unwrap();
match response.status() {
reqwest::StatusCode::OK => {
match response.json::<APIResponse>().await {
Ok(parsed) => {
if parsed.text.len() > 4000 {
ctx.send(
CreateReply::default()
.embed(error_embed(ctx, "Quoted text is too long!").await.unwrap()),
)
.await?;
return Ok(());
}
ctx.send(
CreateReply::default().embed(
embed(
ctx,
&parsed.translation_name,
&parsed.text,
&parsed.reference,
)
.await
.unwrap(),
),
)
.await?;
}
Err(err) => println!("Something is messed up! {:?}", err),
};
}
reqwest::StatusCode::UNAUTHORIZED => {
println!("Unauthorized.. Uoops!!");
}
error => {
println!("Something went wrong: {:?}", error);
}
}
Ok(())
}
#[derive(Serialize, Deserialize, Debug)]
struct APIResponse {
reference: String,
text: String,
translation_name: String,
translation_note: String,
}

View File

@@ -0,0 +1,19 @@
use poise::CreateReply;
use crate::{commands::embeds::embed, Context, Error};
/// Shows weather for provided location
#[poise::command(prefix_command, slash_command, category = "Tools")]
pub async fn weather(
ctx: Context<'_>,
#[description = "Provide a city name"]
#[rest]
_location: String,
) -> Result<(), Error> {
ctx.send(
CreateReply::default().embed(embed(ctx, "Weather", "Work in progress", "").await.unwrap()),
)
.await?;
Ok(())
}

View File

@@ -1,5 +1,5 @@
use reqwest::Client as HttpClient;
use poise::serenity_prelude::prelude::TypeMapKey; use poise::serenity_prelude::prelude::TypeMapKey;
use reqwest::Client as HttpClient;
pub struct HttpKey; pub struct HttpKey;

View File

@@ -1,40 +1,24 @@
use songbird::SerenityInit; use commands::tools::uptime::PROCESS_UPTIME;
use poise::serenity_prelude::{self as serenity, ActivityData}; use poise::serenity_prelude::{self as serenity, ActivityData};
use reqwest::Client as HttpClient;
use songbird::SerenityInit;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tracing::{info, warn, error}; use tracing::{error, info, warn};
use reqwest::Client as HttpClient;
mod http;
mod commands; mod commands;
mod http;
// http typemap for handling requests use crate::commands::music;
use crate::commands::tools;
use crate::http::HttpKey; use crate::http::HttpKey;
// commands: music
use crate::commands::music::deafen::*;
use crate::commands::music::join::*;
use crate::commands::music::leave::*;
use crate::commands::music::mute::*;
use crate::commands::music::play::*;
use crate::commands::music::queue::*;
use crate::commands::music::skip::*;
use crate::commands::music::stop::*;
use crate::commands::music::repeat::*;
use crate::commands::music::pause::*;
use crate::commands::music::resume::*;
// commands: tools
use crate::commands::tools::ping::*;
// commands: kashi
use crate::commands::kashi::kashi::*;
type Error = Box<dyn std::error::Error + Send + Sync>; type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>; type Context<'a> = poise::Context<'a, Data, Error>;
pub struct Data; pub struct Data {
pub http_client: HttpClient,
}
async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
match error { match error {
@@ -50,47 +34,54 @@ async fn on_error(error: poise::FrameworkError<'_, Data, Error>) {
} }
} }
// this is for debug only
#[poise::command(prefix_command)]
pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(())
}
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
// logger and dotenv initialization
tracing_subscriber::fmt::init(); tracing_subscriber::fmt::init();
dotenv::dotenv().expect("Failed to load .env file."); dotenv::dotenv().expect("Failed to load .env file.");
let token = std::env::var("DISCORD_TOKEN").expect("Environment variable `DISCORD_TOKEN` not found!"); let _ = *PROCESS_UPTIME.lock().unwrap();
let token =
std::env::var("DISCORD_TOKEN").expect("Environment variable `DISCORD_TOKEN` not found!");
let prefix = std::env::var("PREFIX").expect("Environment variable `PREFIX` not found!"); let prefix = std::env::var("PREFIX").expect("Environment variable `PREFIX` not found!");
let commands = vec![ let commands = vec![
// commands: music music::deafen(),
deafen(), music::join(),
join(), music::leave(),
leave(), music::mute(),
repeat(), music::pause(),
mute(), music::play(),
pause(), music::queue(),
play(), music::repeat(),
queue(), music::resume(),
resume(), music::seek(),
skip(), music::skip(),
stop(), music::stop(),
// commands: tools music::volume(),
ping(), music::soundboard::effect(),
// commands: kashi music::soundboard::stream(),
kashi(), tools::ai(),
// commands: debug tools::dice(),
register(), tools::dictionary(),
tools::help(),
tools::ip(),
tools::metar(),
tools::owoify(),
tools::ping(),
tools::posix(),
tools::qr(),
tools::register(),
tools::taf(),
tools::uptime(),
tools::verse(),
tools::weather(),
]; ];
let options = poise::FrameworkOptions { let options = poise::FrameworkOptions {
commands, commands,
prefix_options: poise::PrefixFrameworkOptions { prefix_options: poise::PrefixFrameworkOptions {
prefix: Some(prefix.to_string().into()), prefix: Some(prefix.to_string()),
edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan( edit_tracker: Some(Arc::new(poise::EditTracker::for_timespan(
Duration::from_secs(3600), Duration::from_secs(3600),
))), ))),
@@ -124,7 +115,10 @@ async fn main() {
skip_checks_for_owners: false, skip_checks_for_owners: false,
event_handler: |_ctx, event, _framework, _data| { event_handler: |_ctx, event, _framework, _data| {
Box::pin(async move { Box::pin(async move {
info!("Got an event in event handler: {:?}", event.snake_case_name()); info!(
"Got an event in event handler: {:?}",
event.snake_case_name()
);
Ok(()) Ok(())
}) })
}, },
@@ -132,19 +126,31 @@ async fn main() {
}; };
let framework = poise::Framework::builder() let framework = poise::Framework::builder()
.setup(move |ctx, ready, _framework| { .setup(move |ctx, ready, framework| {
Box::pin(async move { Box::pin(async move {
info!("{} [{}] connected successfully!", ready.user.name, ready.user.id); info!(
"{} [{}] connected successfully!",
ready.user.name, ready.user.id
);
ctx.set_activity(Some(ActivityData::listening(prefix + "help"))); ctx.set_activity(Some(ActivityData::listening(prefix + "help")));
// poise::builtins::register_globally(ctx, &framework.options().commands).await?; // poise::builtins::register_globally(ctx, &framework.options().commands).await?;
poise::builtins::register_in_guild(
ctx,
&framework.options().commands,
512680330495524873.into(),
)
.await?;
Ok(Data {}) Ok(Data {
http_client: HttpClient::new(),
})
}) })
}) })
.options(options) .options(options)
.build(); .build();
let intents = serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; let intents =
serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT;
let mut client = serenity::ClientBuilder::new(token, intents) let mut client = serenity::ClientBuilder::new(token, intents)
.framework(framework) .framework(framework)