Rewrite bot into Python
This commit is contained in:
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
DISCORD_TOKEN=
|
||||||
|
OPENAI_TOKEN=
|
22
.github/workflows/docker-publish.yml
vendored
22
.github/workflows/docker-publish.yml
vendored
@ -1,4 +1,4 @@
|
|||||||
name: Test and Build Docker Image
|
name: Build Docker Image
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@ -7,30 +7,14 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "0 0 * * *"
|
- cron: "0 0 * * *"
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
env:
|
|
||||||
DISCORD_TOKEN: 0
|
|
||||||
OPENAI_TOKEN: 0
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "stable"
|
|
||||||
- run: go test ./... -v
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: test
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
env:
|
env:
|
||||||
DISCORD_TOKEN: 0
|
DISCORD_TOKEN: '0'
|
||||||
OPENAI_TOKEN: 0
|
OPENAI_TOKEN: '0'
|
||||||
if: github.event_name != 'pull_request' && github.event_name != 'schedule'
|
if: github.event_name != 'pull_request' && github.event_name != 'schedule'
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
172
.gitignore
vendored
172
.gitignore
vendored
@ -1,26 +1,162 @@
|
|||||||
# If you prefer the allow list template instead of the deny list, see community template:
|
# Byte-compiled / optimized / DLL files
|
||||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
__pycache__/
|
||||||
#
|
*.py[cod]
|
||||||
# Binaries for programs and plugins
|
*$py.class
|
||||||
*.exe
|
|
||||||
*.exe~
|
# C extensions
|
||||||
*.dll
|
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Distribution / packaging
|
||||||
*.test
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
# PyInstaller
|
||||||
*.out
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
# Dependency directories
|
# Installer logs
|
||||||
vendor/
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
# Go workspace file
|
# Unit test / coverage reports
|
||||||
go.work
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
# Discord bot token
|
# Translations
|
||||||
settings.json
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
.env
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
.idea/
|
||||||
|
12
.vscode/launch.json
vendored
12
.vscode/launch.json
vendored
@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Launch Package",
|
|
||||||
"type": "go",
|
|
||||||
"request": "launch",
|
|
||||||
"mode": "auto",
|
|
||||||
"program": "${fileDirname}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
29
.vscode/settings.json
vendored
29
.vscode/settings.json
vendored
@ -1,25 +1,8 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"Andreas",
|
"forgefilip",
|
||||||
"anewdawn",
|
"forgor",
|
||||||
"bwmarrin",
|
"lovibot",
|
||||||
"discordgo",
|
"plubplub"
|
||||||
"Filip",
|
]
|
||||||
"forgefilip",
|
|
||||||
"Fredrik",
|
|
||||||
"godotenv",
|
|
||||||
"GoodAnimemes",
|
|
||||||
"joho",
|
|
||||||
"lovibot",
|
|
||||||
"openai",
|
|
||||||
"Piplup",
|
|
||||||
"plubplub",
|
|
||||||
"Ryouiki",
|
|
||||||
"sashabaranov",
|
|
||||||
"startswith",
|
|
||||||
"vartanbeno",
|
|
||||||
"waifu",
|
|
||||||
"waifus",
|
|
||||||
"Zettai"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
46
Dockerfile
46
Dockerfile
@ -1,44 +1,10 @@
|
|||||||
FROM golang:alpine
|
FROM python:3.12-slim
|
||||||
|
|
||||||
# Git is required for go mod download
|
ENV PYTHONUNBUFFERED=1
|
||||||
RUN apk update && apk add --no-cache git ca-certificates
|
ENV PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
ENV USER=anewdawn
|
WORKDIR /app
|
||||||
ENV UID=10001
|
|
||||||
|
|
||||||
# Create anewdawn user
|
|
||||||
RUN adduser \
|
|
||||||
--disabled-password \
|
|
||||||
--gecos "" \
|
|
||||||
--home "/nonexistent" \
|
|
||||||
--shell "/sbin/nologin" \
|
|
||||||
--no-create-home \
|
|
||||||
--uid "${UID}" \
|
|
||||||
"${USER}"
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
# Copy the current directory contents into the container at /usr/src/app
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN pip install .
|
||||||
|
|
||||||
# Download dependencies
|
CMD ["python", "main.py"]
|
||||||
RUN go get -d -v
|
|
||||||
|
|
||||||
# Build the binary
|
|
||||||
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /usr/local/bin/anewdawn
|
|
||||||
|
|
||||||
FROM scratch
|
|
||||||
|
|
||||||
COPY --from=0 /etc/passwd /etc/passwd
|
|
||||||
COPY --from=0 /etc/group /etc/group
|
|
||||||
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
|
|
||||||
# Copy the binary from the first stage
|
|
||||||
COPY --from=0 /usr/local/bin/anewdawn /usr/local/bin/anewdawn
|
|
||||||
|
|
||||||
# Use an unprivileged user.
|
|
||||||
USER anewdawn:anewdawn
|
|
||||||
|
|
||||||
# Command to run the executable
|
|
||||||
ENTRYPOINT ["/usr/local/bin/anewdawn"]
|
|
||||||
|
18
go.mod
18
go.mod
@ -1,18 +0,0 @@
|
|||||||
module github.com/TheLovinator1/ANewDawn
|
|
||||||
|
|
||||||
go 1.22.2
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/bwmarrin/discordgo v0.28.1
|
|
||||||
github.com/sashabaranov/go-openai v1.26.3
|
|
||||||
github.com/vartanbeno/go-reddit/v2 v2.0.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
|
||||||
github.com/gorilla/websocket v1.5.1 // indirect
|
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
|
||||||
golang.org/x/net v0.25.0 // indirect
|
|
||||||
golang.org/x/oauth2 v0.20.0 // indirect
|
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
|
||||||
)
|
|
62
go.sum
62
go.sum
@ -1,62 +0,0 @@
|
|||||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
|
||||||
github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4=
|
|
||||||
github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
|
||||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
|
||||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
|
||||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
|
||||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/sashabaranov/go-openai v1.24.0 h1:4H4Pg8Bl2RH/YSnU8DYumZbuHnnkfioor/dtNlB20D4=
|
|
||||||
github.com/sashabaranov/go-openai v1.24.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
|
||||||
github.com/sashabaranov/go-openai v1.24.1 h1:DWK95XViNb+agQtuzsn+FyHhn3HQJ7Va8z04DQDJ1MI=
|
|
||||||
github.com/sashabaranov/go-openai v1.24.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
|
||||||
github.com/sashabaranov/go-openai v1.24.2 h1:DZxL5CGahIeRcseuJhvMSMT5SVs1urfVZG9c6/Lyn7M=
|
|
||||||
github.com/sashabaranov/go-openai v1.24.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
|
||||||
github.com/sashabaranov/go-openai v1.25.0 h1:3h3DtJ55zQJqc+BR4y/iTcPhLk4pewJpyO+MXW2RdW0=
|
|
||||||
github.com/sashabaranov/go-openai v1.25.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
|
||||||
github.com/sashabaranov/go-openai v1.26.0 h1:upM565hxdqvCxNzuAcEBZ1XsfGehH0/9kgk9rFVpDxQ=
|
|
||||||
github.com/sashabaranov/go-openai v1.26.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
|
||||||
github.com/sashabaranov/go-openai v1.26.1 h1:B5plrmc/r7hKgYX69oT2VSt5w0O6u9BJYTjB8lNCesI=
|
|
||||||
github.com/sashabaranov/go-openai v1.26.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
|
||||||
github.com/sashabaranov/go-openai v1.26.2 h1:cVlQa3gn3eYqNXRW03pPlpy6zLG52EU4g0FrWXc0EFI=
|
|
||||||
github.com/sashabaranov/go-openai v1.26.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
|
||||||
github.com/sashabaranov/go-openai v1.26.3 h1:Tjnh4rcvsSU68f66r05mys+Zou4vo4qyvkne6AIRJPI=
|
|
||||||
github.com/sashabaranov/go-openai v1.26.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
|
||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
|
||||||
github.com/vartanbeno/go-reddit/v2 v2.0.1 h1:P6ITpf5YHjdy7DHZIbUIDn/iNAoGcEoDQnMa+L4vutw=
|
|
||||||
github.com/vartanbeno/go-reddit/v2 v2.0.1/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI=
|
|
||||||
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
|
||||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
390
main.go
390
main.go
@ -1,390 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/bwmarrin/discordgo"
|
|
||||||
|
|
||||||
"github.com/vartanbeno/go-reddit/v2/reddit"
|
|
||||||
)
|
|
||||||
|
|
||||||
var config Config
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
loadedConfig, err := Load()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
config = *loadedConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPostsFromReddit(subreddit string) (string, error) {
|
|
||||||
if subreddit == "" {
|
|
||||||
return "", fmt.Errorf("subreddit cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
client, err := reddit.NewReadonlyClient()
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to create Reddit client:", err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
posts, _, err := client.Subreddit.TopPosts(context.Background(), subreddit, &reddit.ListPostOptions{
|
|
||||||
ListOptions: reddit.ListOptions{
|
|
||||||
Limit: 100,
|
|
||||||
},
|
|
||||||
Time: "all",
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get posts from Reddit: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the subreddit exists
|
|
||||||
if len(posts) == 0 {
|
|
||||||
return "", fmt.Errorf("subreddit '%v' does not exist", subreddit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// [Title](<https://old.reddit.com{Permalink}>)\n{URL}
|
|
||||||
randInt := rand.Intn(len(posts))
|
|
||||||
discordMessage := fmt.Sprintf("[%v](<https://old.reddit.com%v>)\n%v", posts[randInt].Title, posts[randInt].Permalink, posts[randInt].URL)
|
|
||||||
|
|
||||||
return discordMessage, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleRedditCommand(s *discordgo.Session, i *discordgo.InteractionCreate, subreddit string) {
|
|
||||||
post, err := GetPostsFromReddit(subreddit)
|
|
||||||
if err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
||||||
Data: &discordgo.InteractionResponseData{
|
|
||||||
Content: fmt.Sprintf("Cannot get a random post: %v", err),
|
|
||||||
Flags: discordgo.MessageFlagsEphemeral,
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
log.Println("Failed to respond to interaction:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
||||||
Data: &discordgo.InteractionResponseData{
|
|
||||||
Content: post,
|
|
||||||
},
|
|
||||||
}); err != nil {
|
|
||||||
log.Println("Failed to respond to interaction:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
commands = []*discordgo.ApplicationCommand{
|
|
||||||
{
|
|
||||||
Name: "dank_memes",
|
|
||||||
Description: "Sends dank meme from /r/GoodAnimemes",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "waifus",
|
|
||||||
Description: "Sends waifu from /r/WatchItForThePlot",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "milkers",
|
|
||||||
Description: "Sends milkers from /r/RetrousseTits",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "thighs",
|
|
||||||
Description: "Sends thighs from /r/ZettaiRyouiki",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
|
|
||||||
// Dank memes command
|
|
||||||
"dank_memes": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
||||||
handleRedditCommand(s, i, "GoodAnimemes")
|
|
||||||
},
|
|
||||||
|
|
||||||
// Waifus command
|
|
||||||
"waifus": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
||||||
handleRedditCommand(s, i, "WatchItForThePlot")
|
|
||||||
},
|
|
||||||
|
|
||||||
// Milkers command
|
|
||||||
"milkers": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
||||||
handleRedditCommand(s, i, "RetrousseTits")
|
|
||||||
},
|
|
||||||
|
|
||||||
// Thighs command
|
|
||||||
"thighs": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
||||||
handleRedditCommand(s, i, "ZettaiRyouiki")
|
|
||||||
},
|
|
||||||
// Echo command
|
|
||||||
"echo": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
||||||
// Check if the user provided a message
|
|
||||||
if len(i.ApplicationCommandData().Options) == 0 {
|
|
||||||
// If not, send an ephemeral message to the user
|
|
||||||
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
||||||
Data: &discordgo.InteractionResponseData{
|
|
||||||
Content: "You need to provide a message!",
|
|
||||||
Flags: discordgo.MessageFlagsEphemeral,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to respond to interaction:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the option is not empty
|
|
||||||
if i.ApplicationCommandData().Options[0].StringValue() == "" {
|
|
||||||
// If not, send an ephemeral message to the user
|
|
||||||
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
|
|
||||||
Type: discordgo.InteractionResponseChannelMessageWithSource,
|
|
||||||
Data: &discordgo.InteractionResponseData{
|
|
||||||
Content: "The message cannot be empty!",
|
|
||||||
Flags: discordgo.MessageFlagsEphemeral,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to respond to interaction:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Respond to the original message so we don't get "This interaction failed" error
|
|
||||||
if _, err := s.ChannelMessageSend(i.ChannelID, i.ApplicationCommandData().Options[0].StringValue()); err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func onMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
|
||||||
if m.Author.ID == s.State.User.ID {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow DMs
|
|
||||||
if m.GuildID == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
allowedUsers := []string{
|
|
||||||
"thelovinator",
|
|
||||||
"killyoy",
|
|
||||||
"forgefilip",
|
|
||||||
"plubplub",
|
|
||||||
"nobot",
|
|
||||||
"kao172",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Have a 1/100 chance of replying to a message if written by a user in allowedUsers
|
|
||||||
randInt := rand.Intn(100)
|
|
||||||
log.Println("Random number:", randInt)
|
|
||||||
log.Println("Mentions:", m.Mentions)
|
|
||||||
if len(m.Mentions) == 0 && randInt == 4 {
|
|
||||||
for _, user := range allowedUsers {
|
|
||||||
log.Println("User:", user)
|
|
||||||
if m.Author.Username == user {
|
|
||||||
log.Println("User is in allowedUsers")
|
|
||||||
r, err := GenerateGPT4Response(m.Content, m.Author.Username)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to get OpenAI response:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("OpenAI response:", r)
|
|
||||||
log.Println("Channel ID:", m.ChannelID)
|
|
||||||
_, err = s.ChannelMessageSend(m.ChannelID, r)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Mentions != nil {
|
|
||||||
for _, mention := range m.Mentions {
|
|
||||||
if mention.ID == s.State.User.ID {
|
|
||||||
r, err := GenerateGPT4Response(m.Content, m.Author.Username)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "prompt is too long") {
|
|
||||||
if _, err := s.ChannelMessageSend(m.ChannelID, "Message is too long!"); err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(err.Error(), "prompt is too short") {
|
|
||||||
if _, err := s.ChannelMessageSend(m.ChannelID, "Message is too short!"); err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
message, err := s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Brain broke :flushed: %v", err))
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = s.ChannelMessageSend(m.ChannelID, r)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.HasPrefix(strings.ToLower(m.Content), "lovibot") {
|
|
||||||
r, err := GenerateGPT4Response(m.Content, m.Author.Username)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "prompt is too long") {
|
|
||||||
_, err := s.ChannelMessageSend(m.ChannelID, "Message is too long!")
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(err.Error(), "prompt is too short") {
|
|
||||||
_, err := s.ChannelMessageSend(m.ChannelID, "Message is too short!")
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
message, err := s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Brain broke :flushed: %v", err))
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_ = message
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err = s.ChannelMessageSend(m.ChannelID, r)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Failed to send message to channel:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
discordToken := config.DiscordToken
|
|
||||||
|
|
||||||
// Create a new Discord session using the provided bot token.
|
|
||||||
session, err := discordgo.New("Bot " + discordToken)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Cannot create a new Discord session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a handler function to the discordgo.Session that is triggered when a slash command is received.
|
|
||||||
session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
|
|
||||||
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
|
|
||||||
log.Printf("Handling '%v' command. %+v", i.ApplicationCommandData().Name, i.ApplicationCommandData())
|
|
||||||
h(s, i)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add a handler function to the discordgo.Session that is triggered when a message is received.
|
|
||||||
session.AddHandler(onMessageCreate)
|
|
||||||
|
|
||||||
// Print the user we are logging in as.
|
|
||||||
session.AddHandler(func(s *discordgo.Session, _ *discordgo.Ready) {
|
|
||||||
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Open a websocket connection to Discord and begin listening.
|
|
||||||
err = session.Open()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Cannot open the session: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove all existing commands.
|
|
||||||
appID := session.State.User.ID
|
|
||||||
log.Println("Removing existing commands from all servers for the bot", appID)
|
|
||||||
|
|
||||||
// Remove the commands for guild 98905546077241344
|
|
||||||
log.Println("Removing commands for Killyoy's server...")
|
|
||||||
old_commands, err := session.ApplicationCommands(appID, "98905546077241344")
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("Cannot get commands for guild 98905546077241344: %v", err)
|
|
||||||
}
|
|
||||||
for _, v := range old_commands {
|
|
||||||
err := session.ApplicationCommandDelete(session.State.User.ID, "98905546077241344", v.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("Cannot delete '%v' command: %v", v.Name, err)
|
|
||||||
}
|
|
||||||
log.Printf("Deleted '%v' command.", v.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the commands for guild 341001473661992962
|
|
||||||
log.Println("Removing commands for TheLovinator's server...")
|
|
||||||
old_commands2, err := session.ApplicationCommands(appID, "341001473661992962")
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("Cannot get commands for guild 341001473661992962: %v", err)
|
|
||||||
}
|
|
||||||
for _, v := range old_commands2 {
|
|
||||||
err := session.ApplicationCommandDelete(session.State.User.ID, "341001473661992962", v.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("Cannot delete '%v' command: %v", v.Name, err)
|
|
||||||
}
|
|
||||||
log.Printf("Deleted '%v' command.", v.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the commands for guild 98905546077241344
|
|
||||||
log.Println("Registering commands for Killyoy's server...")
|
|
||||||
registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
|
|
||||||
for i, v := range commands {
|
|
||||||
cmd, err := session.ApplicationCommandCreate(session.State.User.ID, "98905546077241344", v)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("Cannot create '%v' command: %v", v.Name, err)
|
|
||||||
}
|
|
||||||
registeredCommands[i] = cmd
|
|
||||||
log.Printf("Registered '%v' command.", cmd.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the commands for guild 341001473661992962
|
|
||||||
log.Println("Registering commands for TheLovinator's server...")
|
|
||||||
registeredCommands2 := make([]*discordgo.ApplicationCommand, len(commands))
|
|
||||||
for i, v := range commands {
|
|
||||||
cmd, err := session.ApplicationCommandCreate(session.State.User.ID, "341001473661992962", v)
|
|
||||||
if err != nil {
|
|
||||||
log.Panicf("Cannot create '%v' command: %v", v.Name, err)
|
|
||||||
}
|
|
||||||
registeredCommands2[i] = cmd
|
|
||||||
log.Printf("Registered '%v' command.", cmd.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run s.Close() when the program exits.
|
|
||||||
defer session.Close()
|
|
||||||
|
|
||||||
// Wait here until CTRL-C or other term signal is received.
|
|
||||||
stop := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(stop, os.Interrupt)
|
|
||||||
log.Println("Press Ctrl+C to exit")
|
|
||||||
<-stop
|
|
||||||
|
|
||||||
// Bye bye!
|
|
||||||
log.Println("Gracefully shutting down.")
|
|
||||||
}
|
|
174
main.py
Normal file
174
main.py
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ext import commands
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from openai.types.chat.chat_completion import ChatCompletion
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
logger: logging.Logger = logging.getLogger(__name__)
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Load the environment variables from the .env file
|
||||||
|
load_dotenv(verbose=True)
|
||||||
|
|
||||||
|
# Get the Discord token and OpenAI API key from the environment variables
|
||||||
|
discord_token: str | None = os.getenv("DISCORD_TOKEN")
|
||||||
|
openai_api_key: str | None = os.getenv("OPENAI_TOKEN")
|
||||||
|
if not discord_token or not openai_api_key:
|
||||||
|
logger.error("You haven't configured the bot correctly. Please set the environment variables.")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Use OpenAI for chatting with the bot
|
||||||
|
openai_client = OpenAI(api_key=openai_api_key)
|
||||||
|
|
||||||
|
# Create a bot with the necessary intents
|
||||||
|
# TODO(TheLovinator): We should only enable the intents we need # noqa: TD003
|
||||||
|
intents: discord.Intents = discord.Intents.default()
|
||||||
|
intents.message_content = True
|
||||||
|
bot = commands.Bot(command_prefix="!", intents=intents)
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_ready() -> None: # noqa: RUF029
|
||||||
|
"""Print a message when the bot is ready."""
|
||||||
|
logger.info("Logged on as %s", bot.user)
|
||||||
|
|
||||||
|
|
||||||
|
def chat(msg: str) -> str | None:
|
||||||
|
"""Chat with the bot using the OpenAI API.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: The message to send to the bot.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The response from the bot.
|
||||||
|
"""
|
||||||
|
completion: ChatCompletion = openai_client.chat.completions.create(
|
||||||
|
model="gpt-4o-mini",
|
||||||
|
messages=[
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "You are a chatbot. Use Markdown to format your messages if you want.",
|
||||||
|
},
|
||||||
|
{"role": "user", "content": msg},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
response: str | None = completion.choices[0].message.content
|
||||||
|
logger.info("AI response: %s from message: %s", response, msg)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
def get_allowed_users() -> list[str]:
|
||||||
|
"""Get the list of allowed users to interact with the bot.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The list of allowed users.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
"thelovinator",
|
||||||
|
"killyoy",
|
||||||
|
"forgefilip",
|
||||||
|
"plubplub",
|
||||||
|
"nobot",
|
||||||
|
"kao172",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def remove_mentions(message_content: str) -> str:
|
||||||
|
"""Remove mentions of the bot from the message content.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message_content: The message content to process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The message content without the mentions of the bot.
|
||||||
|
"""
|
||||||
|
message_content = message_content.removeprefix("lovibot").strip()
|
||||||
|
message_content = message_content.removeprefix(",").strip()
|
||||||
|
if bot.user:
|
||||||
|
message_content = message_content.replace(f"<@!{bot.user.id}>", "").strip()
|
||||||
|
message_content = message_content.replace(f"<@{bot.user.id}>", "").strip()
|
||||||
|
|
||||||
|
return message_content
|
||||||
|
|
||||||
|
|
||||||
|
@bot.event
|
||||||
|
async def on_message(message: discord.Message) -> None:
|
||||||
|
"""Respond to a message."""
|
||||||
|
logger.info("Message received: %s", message.content)
|
||||||
|
|
||||||
|
message_content: str = message.content.lower()
|
||||||
|
|
||||||
|
# Ignore messages from the bot itself to prevent an infinite loop
|
||||||
|
if message.author == bot.user:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Only allow certain users to interact with the bot
|
||||||
|
allowed_users: list[str] = get_allowed_users()
|
||||||
|
if message.author.name not in allowed_users:
|
||||||
|
logger.info("Ignoring message from: %s", message.author.name)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the message mentions the bot or starts with the bot's name
|
||||||
|
things_to_notify_on: list[str] = ["lovibot"]
|
||||||
|
if bot.user:
|
||||||
|
things_to_notify_on.extend((f"<@!{bot.user.id}>", f"<@{bot.user.id}>"))
|
||||||
|
|
||||||
|
# Only respond to messages that mention the bot or are a reply to a bot message
|
||||||
|
if any(thing.lower() in message_content for thing in things_to_notify_on) or message.reference:
|
||||||
|
if message.reference:
|
||||||
|
# Get the message that the current message is replying to
|
||||||
|
message_id: int | None = message.reference.message_id
|
||||||
|
if message_id is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
reply_message: discord.Message | None = await message.channel.fetch_message(message_id)
|
||||||
|
except discord.errors.NotFound:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get the message content and author
|
||||||
|
reply_content: str = reply_message.content
|
||||||
|
reply_author: str = reply_message.author.name
|
||||||
|
|
||||||
|
# Add the reply message to the current message
|
||||||
|
message.content = f"{reply_author}: {reply_content}\n{message.author.name}: {message.content}"
|
||||||
|
|
||||||
|
# Remove the mention of the bot from the message
|
||||||
|
message_content = remove_mentions(message_content)
|
||||||
|
|
||||||
|
# Grab 10 messages before the current one to provide context
|
||||||
|
old_messages: list[str] = [
|
||||||
|
f"{old_message.author.name}: {old_message.content}"
|
||||||
|
async for old_message in message.channel.history(limit=10)
|
||||||
|
]
|
||||||
|
old_messages.reverse()
|
||||||
|
|
||||||
|
# Get the response from OpenAI
|
||||||
|
response: str | None = chat("\n".join(old_messages) + "\n" + f"{message.author.name}: {message.content}")
|
||||||
|
|
||||||
|
# Remove LoviBot: from the response
|
||||||
|
if response:
|
||||||
|
response = response.removeprefix("LoviBot:").strip()
|
||||||
|
|
||||||
|
if response:
|
||||||
|
logger.info("Responding to message: %s with: %s", message.content, response)
|
||||||
|
await message.channel.send(response)
|
||||||
|
else:
|
||||||
|
logger.warning("No response from the AI model. Message: %s", message.content)
|
||||||
|
await message.channel.send("I forgor how to think 💀")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logger.info("Starting the bot.")
|
||||||
|
bot.run(token=discord_token, root_logger=True)
|
40
main_test.go
40
main_test.go
@ -1,40 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Returns a string with a length between 1 and 2000 characters
|
|
||||||
func TestGetPostsFromReddit_ReturnsPostWithValidLength(t *testing.T) {
|
|
||||||
if os.Getenv("CI") == "" {
|
|
||||||
subreddit := "celebs"
|
|
||||||
post, err := GetPostsFromReddit(subreddit)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if len(post) < 1 || len(post) > 2000 {
|
|
||||||
t.Errorf("Post length is not within the valid range")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
t.Skip("Skipping test in CI environment as the IP is probably blocked by Reddit")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an error when the subreddit does not exist
|
|
||||||
func TestGetPostsFromReddit_ReturnsErrorWhenSubredditDoesNotExist(t *testing.T) {
|
|
||||||
subreddit := "nonexistent"
|
|
||||||
_, err := GetPostsFromReddit(subreddit)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("Expected error, but got nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an error when the subreddit is empty
|
|
||||||
func TestGetPostsFromReddit_ReturnsErrorWhenSubredditIsEmpty(t *testing.T) {
|
|
||||||
subreddit := ""
|
|
||||||
_, err := GetPostsFromReddit(subreddit)
|
|
||||||
if err.Error() != "subreddit cannot be empty" {
|
|
||||||
t.Errorf("Expected error 'subreddit cannot be empty', but got '%v'", err)
|
|
||||||
}
|
|
||||||
}
|
|
89
openai.go
89
openai.go
@ -1,89 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
openai "github.com/sashabaranov/go-openai"
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
Prompt is the Discord message that the user sent to the bot.
|
|
||||||
*/
|
|
||||||
func GenerateGPT4Response(prompt string, author string) (string, error) {
|
|
||||||
openAIToken := config.OpenAIToken
|
|
||||||
if openAIToken == "" {
|
|
||||||
return "", fmt.Errorf("OPENAI_API_KEY is not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the prompt
|
|
||||||
fmt.Println("Prompt:", author, ":", prompt)
|
|
||||||
|
|
||||||
// Check if the prompt is too long
|
|
||||||
if len(prompt) > 2048 {
|
|
||||||
return "", fmt.Errorf("prompt is too long")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add additional information to the system message
|
|
||||||
var additionalInfo string
|
|
||||||
switch author {
|
|
||||||
case "thelovinator":
|
|
||||||
additionalInfo = "User (TheLovinator) is a programmer. Wants to live in the woods. Real name is Joakim. He made the bot."
|
|
||||||
case "killyoy":
|
|
||||||
additionalInfo = "User (KillYoy) likes to play video games. Real name is Andreas. Good at CSS."
|
|
||||||
case "forgefilip":
|
|
||||||
additionalInfo = "User (ForgeFilip) likes watches. Real name is Filip."
|
|
||||||
case "plubplub":
|
|
||||||
additionalInfo = "User (Piplup) likes to play WoW and Path of Exile. Real name is Axel. Is also called Bambi."
|
|
||||||
case "nobot":
|
|
||||||
additionalInfo = "User (Nobot) likes to play WoW. Real name is Gustav. Really good at programming."
|
|
||||||
case "kao172":
|
|
||||||
additionalInfo = "User (kao172) likes cars. Real name is Fredrik."
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new client
|
|
||||||
client := openai.NewClient(openAIToken)
|
|
||||||
|
|
||||||
// System message
|
|
||||||
var systemMessage string
|
|
||||||
systemMessage = `You are in a Discord server.
|
|
||||||
You are Swedish.
|
|
||||||
Use Markdown for formatting.
|
|
||||||
Please respond with a short message.
|
|
||||||
`
|
|
||||||
|
|
||||||
// Add additional information to the system message
|
|
||||||
if additionalInfo != "" {
|
|
||||||
systemMessage = fmt.Sprintf("%s\n%s", systemMessage, additionalInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the system message
|
|
||||||
fmt.Println("System message:", systemMessage)
|
|
||||||
|
|
||||||
// Create a completion
|
|
||||||
resp, err := client.CreateChatCompletion(
|
|
||||||
context.Background(),
|
|
||||||
openai.ChatCompletionRequest{
|
|
||||||
Model: openai.GPT4o,
|
|
||||||
Messages: []openai.ChatCompletionMessage{
|
|
||||||
{
|
|
||||||
Role: openai.ChatMessageRoleSystem,
|
|
||||||
Content: systemMessage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Role: openai.ChatMessageRoleUser,
|
|
||||||
Content: prompt,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to get response from OpenAI: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ourResponse := resp.Choices[0].Message.Content
|
|
||||||
|
|
||||||
fmt.Println("Response:", ourResponse)
|
|
||||||
return ourResponse, nil
|
|
||||||
}
|
|
51
pyproject.toml
Normal file
51
pyproject.toml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
[project]
|
||||||
|
name = "anewdawn"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "My shit bot"
|
||||||
|
dependencies = ["discord.py", "openai", "python-dotenv"]
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
# https://docs.astral.sh/ruff/
|
||||||
|
line-length = 120
|
||||||
|
fix = true
|
||||||
|
unsafe-fixes = true
|
||||||
|
extend-exclude = [".venv"]
|
||||||
|
show-fixes = true
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
# https://docs.astral.sh/ruff/linter/
|
||||||
|
preview = true
|
||||||
|
select = ["ALL"]
|
||||||
|
ignore = [
|
||||||
|
"CPY001", # Checks for the absence of copyright notices within Python files.
|
||||||
|
"D100", # Checks for undocumented public module definitions.
|
||||||
|
"FIX002", # Checks for "TODO" comments.
|
||||||
|
"D104", # Checks for undocumented public package definitions.
|
||||||
|
# https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
|
||||||
|
"W191",
|
||||||
|
"E111",
|
||||||
|
"E114",
|
||||||
|
"E117",
|
||||||
|
"D206",
|
||||||
|
"D300",
|
||||||
|
"Q000",
|
||||||
|
"Q001",
|
||||||
|
"Q002",
|
||||||
|
"Q003",
|
||||||
|
"COM812",
|
||||||
|
"COM819",
|
||||||
|
"ISC001",
|
||||||
|
"ISC002",
|
||||||
|
]
|
||||||
|
pydocstyle.convention = "google"
|
||||||
|
isort.required-imports = ["from __future__ import annotations"]
|
||||||
|
pycodestyle.ignore-overlong-task-comments = true
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
# https://docs.astral.sh/ruff/formatter/
|
||||||
|
docstring-code-format = true
|
||||||
|
docstring-code-line-length = 20
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"tests/**/*.py" = ["S101", "ARG", "FBT"]
|
66
settings.go
66
settings.go
@ -1,66 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config holds the configuration parameters
|
|
||||||
type Config struct {
|
|
||||||
DiscordToken string `json:"discord_token"`
|
|
||||||
OpenAIToken string `json:"openai_token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load reads configuration from settings.json or environment variables
|
|
||||||
func Load() (*Config, error) {
|
|
||||||
// Try reading from settings.json file first
|
|
||||||
config, err := loadFromJSONFile("settings.json")
|
|
||||||
if err != nil {
|
|
||||||
// If reading from file fails, try reading from environment variables
|
|
||||||
config, err = loadFromEnvironment()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadFromJSONFile reads configuration from a JSON file
|
|
||||||
func loadFromJSONFile(filename string) (*Config, error) {
|
|
||||||
file, err := os.Open(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open settings file: %v", err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
config := &Config{}
|
|
||||||
decoder := json.NewDecoder(file)
|
|
||||||
err = decoder.Decode(config)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode settings file: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadFromEnvironment reads configuration from environment variables
|
|
||||||
func loadFromEnvironment() (*Config, error) {
|
|
||||||
discordToken := os.Getenv("DISCORD_TOKEN")
|
|
||||||
if discordToken == "" {
|
|
||||||
return nil, fmt.Errorf("DISCORD_TOKEN environment variable not set or empty. Also tried reading from settings.json file")
|
|
||||||
}
|
|
||||||
|
|
||||||
openAIToken := os.Getenv("OPENAI_TOKEN")
|
|
||||||
if openAIToken == "" {
|
|
||||||
return nil, fmt.Errorf("OPENAI_TOKEN environment variable not set or empty. Also tried reading from settings.json file")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := &Config{
|
|
||||||
DiscordToken: discordToken,
|
|
||||||
OpenAIToken: openAIToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"discord_token": "",
|
|
||||||
"openai_token": ""
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Returns a Config object with DiscordToken set when DISCORD_TOKEN environment variable is set
|
|
||||||
func TestLoadFromEnvironment_DiscordTokenSet(t *testing.T) {
|
|
||||||
os.Setenv("DISCORD_TOKEN", "test_token")
|
|
||||||
os.Setenv("OPENAI_TOKEN", "test_token2")
|
|
||||||
defer os.Unsetenv("DISCORD_TOKEN")
|
|
||||||
defer os.Unsetenv("OPENAI_TOKEN")
|
|
||||||
|
|
||||||
config, err := loadFromEnvironment()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Expected no error, but got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := &Config{
|
|
||||||
DiscordToken: "test_token",
|
|
||||||
OpenAIToken: "test_token2",
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(config, expected) {
|
|
||||||
t.Errorf("Expected config to be %v, but got %v", expected, config)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an error when DISCORD_TOKEN environment variable is empty
|
|
||||||
func TestLoadFromEnvironment_EmptyDiscordToken(t *testing.T) {
|
|
||||||
os.Setenv("DISCORD_TOKEN", "")
|
|
||||||
defer os.Unsetenv("DISCORD_TOKEN")
|
|
||||||
|
|
||||||
_, err := loadFromEnvironment()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected an error, but got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "DISCORD_TOKEN environment variable not set or empty. Also tried reading from settings.json file"
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("Expected error message to be '%s', but got '%s'", expected, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an error when DISCORD_TOKEN environment variable is not set and settings.json file is not present
|
|
||||||
func TestLoadFromEnvironment_NoDiscordTokenNoSettingsFile(t *testing.T) {
|
|
||||||
os.Unsetenv("DISCORD_TOKEN")
|
|
||||||
|
|
||||||
_, err := loadFromEnvironment()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected an error, but got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "DISCORD_TOKEN environment variable not set or empty. Also tried reading from settings.json file"
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("Expected error message to be '%s', but got '%s'", expected, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns an error when settings.json file is present but DiscordToken is not set
|
|
||||||
func TestLoadFromEnvironment_SettingsFileNoDiscordToken(t *testing.T) {
|
|
||||||
os.Setenv("DISCORD_TOKEN", "")
|
|
||||||
defer os.Unsetenv("DISCORD_TOKEN")
|
|
||||||
|
|
||||||
_, err := loadFromEnvironment()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Expected an error, but got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
expected := "DISCORD_TOKEN environment variable not set or empty. Also tried reading from settings.json file"
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("Expected error message to be '%s', but got '%s'", expected, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user