···11+# Bingus Bot
22+33+A very clever kitty
44+55+Current features:
66+77+- [Markov chain](https://en.wikipedia.org/wiki/Markov_chain) for a "smart" chatbot
88+99+## Pre-reqs
1010+1111+- Python 3.12
1212+- Pipenv
1313+- Just (optional, QoL)
1414+1515+## Environment Setup
1616+1717+With `just`: `just setup`
1818+1919+Otherwise, `pipenv install`
2020+2121+### .env
2222+2323+Minimal needed is an env var called `TOKEN` that contains the bot's token.
2424+2525+Cogs may have additional options, these variables **should** be prefix with the cog's name, ex:
2626+2727+Cog `Math` may have `Math.TEMP_UNIT=fahrenheit`.
2828+2929+## Cog Docs
3030+3131+### [Markov](src/cogs/markov.py)
3232+3333+A markov chain is an incredibly simple model where it decides the next token based on previous
3434+knowledge of what tokens the current token has been proceeded by.
3535+3636+#### Markov Commands
3737+3838+- `/markov`: Make bingus try and reply to a prompt passed, use this to bypass the 80% change that bingus
3939+ usually has to reply
4040+- `/scan_history`: Scan the history of the current channel and add it to the chain. Since Bingus only learns
4141+ from *new* messages while he's active, you may need to do this when restarting him. This command can take a while depending on the number of messages.
4242+4343+#### Markov Config
4444+4545+- `Markov.REPLY_CHANNELS`: A *comma-delimited* list of channel IDs that the bot should have
4646+ have a chance to reply to messages in. The bot still learns from all channels in realtime, but
4747+ these channels it'll have an 80% of replying to any message
4848+4949+## Adding Cogs
5050+5151+To start, you can run `just add-cog` (or `python src/dev/new_cog.py`).
5252+5353+Follow the steps and you'll get a new cog in `src/cogs`. Here is where you can add
5454+commands, event listeners, etc.
5555+5656+See the file generated for some simple examples, review the [PyCord docs](https://guide.pycord.dev/introduction) for more help and information.
5757+5858+### Best Practices
5959+6060+Generally you'll want to make all state, config, etc. locallized to your cog file. That
6161+way we can easily disable it if needed and nothing will break.
6262+6363+#### Config
6464+6565+For simplicity we'll just use env vars for config. Prefix any env vars your cog will
6666+read with the cog's name and a `.` (example: `Markov` cog can have a var called `Markov.REPLY_CHANNELS`).
6767+6868+Try to documents these options within this README file under the [The individual cogs docs](#cog-docs).
6969+Create a third-level heading with your cog's name, and a link to its source code.
···10101111 def __init__(self, bot):
1212 self.bot = bot
1313- self.reply_channels = [int(x) for x in os.getenv("REPLY_CHANNELS", "0").split(",")]
1313+ self.reply_channels = [int(x) for x in os.getenv("Markov.REPLY_CHANNELS", "0").split(",")]
1414 self.markov = MarkovChain({})
15151616 @commands.slash_command()
1717- async def scan_history(self, ctx):
1717+ async def scan_history(self, ctx: discord.ApplicationContext):
1818 buf = []
1919 await ctx.defer()
2020 async for msg in ctx.history(limit=None):
2121- print("Learning...")
2221 if msg.author.id != self.bot.application_id:
2322 buf.append(msg.content)
2423 self.markov.learn(" ".join(buf))
2524 await ctx.respond("Bingus Learned!")
26252726 @commands.slash_command()
2828- async def markov(self, ctx, prompt: discord.Option(str)):
2727+ async def markov(self, ctx: discord.ApplicationContext, prompt: discord.Option(str)):
2928 print("Bingus is responding!")
3029 response = self.markov.respond(prompt)
3130 if response is not None and len(response) != 0:
3231 await ctx.respond(response)
3332 else:
3434- await ctx.respond(":: Bingus couldn't think of what to say!")
3333+ await ctx.respond("> Bingus couldn't think of what to say!")
3534 print("Bingus Responded!")
36353736
+31
src/dev/cog.template.py
···11+22+import discord
33+from discord.ext import commands
44+from discord.message import Message
55+66+class __CName__(commands.Cog):
77+88+ # Setup any state for the cog here, this will
99+ # exist for the run of the program
1010+ # If you need to change the parameters for __init__
1111+ # make sure the [setup] function below this class.
1212+ def __init__(self, bot) -> None:
1313+ self.bot = bot
1414+1515+ # Example of a slash command that will be loaded
1616+ # with this cog
1717+ # @commands.slash_command()
1818+ # async def ping(self, ctx: discord.ApplicationContext):
1919+ # await ctx.respond("pong!")
2020+2121+ # Example of listening to all messages
2222+ # ever sent in any server the bot is in
2323+ # while active
2424+ # @commands.Cog.listener()
2525+ # async def on_message(self, msg: Message):
2626+ # pass
2727+2828+ # See the PyCord docs for more info and guides
2929+3030+def setup(bot):
3131+ bot.add_cog(__CName__(bot))
+27
src/dev/new_cog.py
···11+22+import sys
33+import json
44+from pathlib import Path
55+66+77+def main():
88+ name = input("Enter new cog's name (PascalCase!): ")
99+ cogs_folder = Path(__file__).parent.parent.joinpath("cogs")
1010+ path = cogs_folder.joinpath(f"{name.lower()}.py")
1111+ if path.exists():
1212+ print("This cog seems to already exist! Refusing to overwrite.")
1313+ sys.exit(1)
1414+ else:
1515+ print("Creating new Python file...")
1616+ template_content = Path(__file__).parent.joinpath("cog.template.py").read_text()
1717+ new_cog_file = template_content.replace("__CName__", name)
1818+ path.write_text(new_cog_file)
1919+ print("Adding to \"cogs.json\"...")
2020+ cogs_json_path = cogs_folder.parent.joinpath("cogs.json")
2121+ current: list[str] = json.loads(cogs_json_path.read_text())
2222+ current.append(f"cogs.{name.lower()}")
2323+ cogs_json_path.write_text(json.dumps(current))
2424+ print(f"Cog Created at {path}!")
2525+2626+if __name__ == "__main__":
2727+ main()