Compare commits

..

32 Commits

Author SHA1 Message Date
56b325ca10 use django whitenoise for static files 2025-11-24 08:35:01 -06:00
b02efb06dd fix non-uniqueness oversight is checking for discord bridge 2025-11-24 08:28:17 -06:00
0d691ff431
tweak the green-ness of the middle !reaction result
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-24 16:48:30 -06:00
e7d63ee963
slight tweak to the !reaction reply string
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-24 16:43:44 -06:00
3100efd4a9
add a !reaction dice roll for a very simple oracle-style vibe check
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-24 16:39:06 -06:00
6d8ba18380
cypher: distinguish between a task roll and an attack roll
attack = A, otherwise the same options as with a generic task (= T)

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
3b7c871a94
cypher: case insensitivity when matching task difficulties
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
fa4815153a
cypher: slightly better display of output with no difficulty or mods
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
cc9b110531
fix a display issue in the karma key score
this would probably only matter if adding a key manually that doesn't
have a score, but a fix is a fix

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
af4746fb90
allow Markov contexts to be hidden from the web
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2025-02-08 23:57:23 -06:00
d0cbc815d1
update post_connect to accept a /command to do things other than "msg"
these are just /msg and /mode for now, and the slash is retained out of
convention

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-12-16 23:47:32 -06:00
166b506843
the weather module depends on requests
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-11-08 08:29:30 -06:00
12adb2a205
plug the cypher system roller into the irc plugin
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-11-07 16:32:06 -06:00
8e22ccb2a3
start a TODO for myself
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-11-07 16:16:12 -06:00
02e5769ab5
irc 20.5.0 breaks recursion checks/bot.reply()
it seems like the Command object (the event used in basically
everything) expects an argument to __new__() that deepcopy can't
resolve, and deepcopy is critical to recursion currently, so until that
can be refactored, just pin our library

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-11-07 16:13:06 -06:00
ac16ec99e5
add a Cypher System dice rolling library method
not available to the bot, yet, but that's next

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-11-07 14:33:00 -06:00
994f9ef50b
Revert "replace dice sanity checks as asserts"
not sure why I would have done this, asserts are not great

This reverts commit b917f78ca53a0cb1744e787ddd764d29cb600975.

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-11-07 12:24:33 -06:00
f43b9e4de4
updates to the packages under test
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-10-31 13:41:25 -05:00
58fd0d0728
remove unused gitlab CI configuration
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-10-31 13:41:08 -05:00
cf8d24187f
requirements bumps, pinning Django to 5.0 for now
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-10-31 00:25:55 -05:00
8f76e54d30
add a couple dice roller tests
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-10-31 00:18:56 -05:00
1674300ec3
convert to pyproject.toml (no versioneer)
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-10-31 00:10:03 -05:00
355d17d171
flake8 cleanups
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-07-17 00:30:27 -05:00
e5b9f1634a
completely remove usages of django-adminplus
it was futzing up some /admin/ login stuff, and I hadn't actually used
the three views using it in years, so probably better to just yeet it
(technical term) into the sun.

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-03 13:24:38 -05:00
e63740ca70
replace discord bridge nick with sender nick inside the bot
when the bot has received a message through the discord bridge that
it'll end up reacting to (by creating a countdown item, for instance),
the nick in the event should, for all intents and purposes, be the
sender's nick on the discord side, not the discord bridge itself

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-03 12:52:58 -05:00
03e1269cf2
updates to bump the whole app to Django 5.0
note that this removes support for python 3.8 and 3.9!

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-03 12:44:57 -05:00
2dfb942f91
some trivial setup.py cleanups until we convert
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-03 12:17:09 -05:00
a92d0041cf
add the REUSE tool, not that this repo is compliant yet
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-03 12:16:43 -05:00
84eddf4b8a
requirements bumps
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-01 22:12:40 -05:00
b69053b279
add python 3.11 to versions under test in tox
Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-05-01 22:06:23 -05:00
a1a256ca3b
adopt the DCO and clean up license, etc. information
NOTE: for the moment this project is GPLv3 ONLY (no "or later" was ever
present). I will hopefully reach out to the couple other authors later
to see about adding the "or later" part

Signed-off-by: Brian S. Stephan <bss@incorporeal.org>
2024-02-23 00:09:44 -06:00
ee6ae7080e
remove the bridge-speaker from the message when relevant
this allows downstream event handlers to react to e.g.:

<discord_user> !weather 12345

as if they were normal leading IRC commands
2023-09-12 09:16:38 -05:00
49 changed files with 1212 additions and 2968 deletions

2
.gitignore vendored
View File

@ -5,6 +5,7 @@ tags/
*.egg-info/
.tox/
.coverage
staticfiles/
dr.botzo.data
dr.botzo.cfg
localsettings.py
@ -22,3 +23,4 @@ dr.botzo.markov
*.swp
*.urls
*~
_version.py

View File

@ -1,9 +0,0 @@
before_script:
- virtualenv --python=python3.4 env-py3 --clear
- source env-py3/bin/activate
- pip install -Ur requirements-dev.txt
lint:
script:
- echo "--zero-exit should be set until only a few messages remain"
- prospector --zero-exit

View File

@ -1,10 +1,44 @@
## Contributing
# Contributing Guidelines
This is a pretty fun, non-serious hacking project, so if you're interested in
contributing, sign up, clone the project, and submit some merge requests!
There's a lot to add and work on, so join in.
dr.botzo is made available under the GPLv3 license. Contributions are welcome via pull requests. This document outlines
the process to get your contribution accepted.
## Code Style
4 spaces per indent level. 120 character line length. Follow PEP8 as closely
as reasonable. There's a prospector config, use it.
## Sign Offs/Custody of Contributions
I do not request the copyright of contributions be assigned to me or to the project, and I require no provision that I
be allowed to relicense your contributions. My personal oath is to maintain inbound=outbound in my open source projects,
and the expectation is authors are responsible for their contributions.
I am following the the [Developer Certificate of Origin (DCO)](https://developercertificate.org/), also available at
`DCO.txt`. The DCO is a way for contributors to certify that they wrote or otherwise have the right to license their
code contributions to the project. Contributors must sign-off that they adhere to these requirements by adding a
`Signed-off-by` line to their commit message, and/or, for frequent contributors, by signing off on their entry in
`MAINTAINERS.md`.
This process is followed by a number of open source projects, most notably the Linux kernel. Here's the gist of it:
```
[Your normal Git commit message here.]
Signed-off-by: Random J Developer <random@developer.example.org>
```
`git help commit` has more info on adding this:
```
-s, --signoff
Add Signed-off-by line by the committer at the end of the commit log
message. The meaning of a signoff depends on the project, but it typically
certifies that committer has the rights to submit this work under the same
license and agrees to a Developer Certificate of Origin (see
http://developercertificate.org/ for more information).
```

34
DCO.txt Normal file
View File

@ -0,0 +1,34 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

View File

@ -1,7 +1,7 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@ -645,7 +645,7 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
@ -664,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

10
MAINTAINERS.md Normal file
View File

@ -0,0 +1,10 @@
# Maintainers
This file contains information about people permitted to make major decisions and direction on the project.
## Contributing Under the DCO
By adding your name and email address to this section, you certify that all of your subsequent contributions to dr.botzo
are made under the terms of the Developer's Certificate of Origin 1.1, available at `DCO.txt`.
* Brian S. Stephan (<bss@incorporeal.org>)

6
TODO.md Normal file
View File

@ -0,0 +1,6 @@
# TODO
Notes to my future self.
* Refactor recursion --- see 02e5769ab5b9406725235c4e37968f93afd0e978 --- to remove deepcopy
* maybe I can push/pop onto arguments or something instead of duplicating the whole event?

View File

@ -1,5 +1,4 @@
"""An acromania style game for IRC."""
import logging
import random
import threading
@ -9,21 +8,17 @@ from irc.client import NickMask, is_channel
from ircbot.lib import Plugin
log = logging.getLogger('acro.ircplugin')
logger = logging.getLogger('acro.ircplugin')
class Acro(Plugin):
"""Play a game where users come up with silly definitions for randomly-generated acronyms."""
class AcroGame(object):
"""Track game details."""
def __init__(self):
"""Initialize basic stuff."""
# running state
self.state = 0
self.quit = False
@ -32,12 +27,10 @@ class Acro(Plugin):
self.channel = ''
class AcroRound(object):
"""Track a particular round of a game."""
def __init__(self):
"""Initialize basic stuff."""
self.acro = ""
self.submissions = dict()
self.sub_shuffle = []
@ -51,7 +44,6 @@ class Acro(Plugin):
def __init__(self, bot, connection, event):
"""Set up the game tracking and such."""
# game state
self.game = self.AcroGame()
@ -59,7 +51,6 @@ class Acro(Plugin):
def start(self):
"""Set up handlers."""
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!acro\s+status$',
self.handle_status, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!acro\s+start$',
@ -75,7 +66,6 @@ class Acro(Plugin):
def stop(self):
"""Tear down handlers."""
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_status)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_start)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_submit)
@ -86,7 +76,6 @@ class Acro(Plugin):
def handle_status(self, connection, event, match):
"""Return the status of the currently-running game, if there is one."""
if self.game.state == 0:
return self.bot.reply(event, "there currently isn't a game running.")
else:
@ -94,7 +83,6 @@ class Acro(Plugin):
def handle_start(self, connection, event, match):
"""Start the game, notify the channel, and begin timers."""
if self.game.state != 0:
return self.bot.reply(event, "the game is already running.")
else:
@ -105,7 +93,6 @@ class Acro(Plugin):
def handle_submit(self, connection, event, match):
"""Take a submission for an acro."""
nick = NickMask(event.source).nick
submission = match.group(1)
@ -113,7 +100,6 @@ class Acro(Plugin):
def handle_vote(self, connection, event, match):
"""Take a vote for an acro."""
nick = NickMask(event.source).nick
vote = match.group(1)
@ -121,26 +107,22 @@ class Acro(Plugin):
def handle_quit(self, connection, event, match):
"""Quit the game after the current round ends."""
if self.game.state != 0:
self.game.quit = True
return self.bot.reply(event, "the game will end after the current round.")
def thread_do_process_submissions(self, sleep_time):
"""Wait for players to provide acro submissions, and then kick off voting."""
time.sleep(sleep_time)
self._start_voting()
def thread_do_process_votes(self):
"""Wait for players to provide votes, and then continue or quit."""
time.sleep(self.game.rounds[-1].seconds_to_vote)
self._end_current_round()
def _start_new_game(self, channel):
"""Begin a new game, which will have multiple rounds."""
self.game.state = 1
self.game.channel = channel
self.bot.reply(None, "starting a new game of acro. it will run until you tell it to quit.",
@ -150,12 +132,12 @@ class Acro(Plugin):
def _start_new_round(self):
"""Start a new round for play."""
self.game.state = 2
self.game.rounds.append(self.AcroRound())
acro = self._generate_acro()
self.game.rounds[-1].acro = acro
sleep_time = self.game.rounds[-1].seconds_to_submit + (self.game.rounds[-1].seconds_to_submit_step * (len(acro)-3))
sleep_time = (self.game.rounds[-1].seconds_to_submit +
(self.game.rounds[-1].seconds_to_submit_step * (len(acro)-3)))
self.bot.reply(None, "the round has started! your acronym is '{0:s}'. "
"submit within {1:d} seconds via !acro submit [meaning]".format(acro, sleep_time),
@ -166,17 +148,16 @@ class Acro(Plugin):
t.start()
@staticmethod
def _generate_acro():
def _generate_acro(): # noqa: C901
"""Generate an acro to play with.
Letter frequencies pinched from
http://www.math.cornell.edu/~mec/2003-2004/cryptography/subs/frequencies.html
"""
acro = []
# generate acro 3-8 characters long
for i in range(1, random.randint(4, 9)):
letter = random.randint(1, 182303)
for i in range(1, random.SystemRandom.randint(4, 9)):
letter = random.SystemRandom.randint(1, 182303)
if letter <= 21912:
acro.append('E')
elif letter <= 38499:
@ -234,7 +215,6 @@ class Acro(Plugin):
def _take_acro_submission(self, nick, submission):
"""Take an acro submission and record it."""
if self.game.state == 2:
sub_acro = self._turn_text_into_acro(submission)
@ -247,7 +227,6 @@ class Acro(Plugin):
@staticmethod
def _turn_text_into_acro(text):
"""Turn text into an acronym."""
words = text.split()
acro = []
for w in words:
@ -256,10 +235,9 @@ class Acro(Plugin):
def _start_voting(self):
"""Begin the voting period."""
self.game.state = 3
self.game.rounds[-1].sub_shuffle = list(self.game.rounds[-1].submissions.keys())
random.shuffle(self.game.rounds[-1].sub_shuffle)
random.SystemRandom.shuffle(self.game.rounds[-1].sub_shuffle)
self.bot.reply(None, "here are the results. vote with !acro vote [number]", explicit_target=self.game.channel)
self._print_round_acros()
@ -269,21 +247,19 @@ class Acro(Plugin):
def _print_round_acros(self):
"""Take the current round's acros and spit them to the channel."""
i = 0
for s in self.game.rounds[-1].sub_shuffle:
log.debug("%s is %s", str(i), s)
logger.debug("%s is %s", str(i), s)
self.bot.reply(None, " {0:d}: {1:s}".format(i+1, self.game.rounds[-1].submissions[s]),
explicit_target=self.game.channel)
i += 1
def _take_acro_vote(self, nick, vote):
"""Take an acro vote and record it."""
if self.game.state == 3:
acros = self.game.rounds[-1].submissions
if int(vote) > 0 and int(vote) <= len(acros):
log.debug("%s is %s", vote, self.game.rounds[-1].sub_shuffle[int(vote)-1])
logger.debug("%s is %s", vote, self.game.rounds[-1].sub_shuffle[int(vote)-1])
key = self.game.rounds[-1].sub_shuffle[int(vote)-1]
if key != nick:
@ -296,7 +272,6 @@ class Acro(Plugin):
def _end_current_round(self):
"""Clean up and output for ending the current round."""
self.game.state = 4
self.bot.reply(None, "voting's over! here are the scores for the round:", explicit_target=self.game.channel)
self._print_round_scores()
@ -308,7 +283,6 @@ class Acro(Plugin):
def _print_round_scores(self):
"""For the acros in the round, find the votes for them."""
i = 0
for s in list(self.game.rounds[-1].submissions.keys()):
votes = [x for x in list(self.game.rounds[-1].votes.values()) if x == s]
@ -318,7 +292,6 @@ class Acro(Plugin):
def _add_round_scores_to_game_scores(self):
"""Apply the final round scores to the totall scores for the game."""
for s in list(self.game.rounds[-1].votes.values()):
votes = [x for x in list(self.game.rounds[-1].votes.values()) if x == s]
if s in list(self.game.scores.keys()):
@ -328,7 +301,6 @@ class Acro(Plugin):
def _continue_or_quit(self):
"""Decide whether the game should continue or quit."""
if self.game.state == 4:
if self.game.quit:
self._end_game()
@ -337,7 +309,6 @@ class Acro(Plugin):
def _end_game(self):
"""Clean up the entire game."""
self.game.state = 0
self.game.quit = False
self.bot.reply(None, "game's over! here are the final scores:", explicit_target=self.game.channel)
@ -345,7 +316,6 @@ class Acro(Plugin):
def _print_game_scores(self):
"""Print the final calculated scores."""
for s in list(self.game.scores.keys()):
self.bot.reply(None, " {0:s}: {1:d}".format(s, self.game.scores[s]), explicit_target=self.game.channel)

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('countdown', '0007_countdownitem_reminder_target_new'),
]
operations = [
migrations.AlterField(
model_name='countdownitem',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -4,14 +4,18 @@ import random
import re
from django.conf import settings
from irc.client import NickMask
from dice.lib import cypher_roll, reaction_roll
from dice.roller import DiceRoller
from ircbot.lib import Plugin
logger = logging.getLogger(__name__)
CYPHER_ROLL_REGEX = r'((?P<type>A|T)(?P<difficulty>\d+))?(?P<mods>(?:\s*(-|\+)\d+)*)\s*(?P<comment>.*)?'
CYPHER_COMMAND_REGEX = r'^!cypher\s+(' + CYPHER_ROLL_REGEX + ')'
REACTION_COMMAND_REGEX = r'^!reaction$'
class Dice(Plugin):
"""Roll simple or complex dice strings."""
@ -24,10 +28,14 @@ class Dice(Plugin):
def start(self):
"""Set up the handlers."""
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], CYPHER_COMMAND_REGEX,
self.handle_cypher_roll, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!roll\s+(.*)$',
self.handle_roll, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!random\s+(.*)$',
self.handle_random, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], REACTION_COMMAND_REGEX,
self.handle_reaction_roll, -20)
super(Dice, self).start()
@ -35,9 +43,52 @@ class Dice(Plugin):
"""Tear down handlers."""
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_roll)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_random)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_cypher_roll)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_reaction_roll)
super(Dice, self).stop()
def handle_cypher_roll(self, connection, event, match):
"""Handle the !cypher roll."""
nick = NickMask(event.source).nick
task = match.group(1)
task_group = re.search(CYPHER_ROLL_REGEX, task, re.IGNORECASE)
difficulty = int(task_group.group('difficulty')) if task_group.group('difficulty') else None
mods = task_group.group('mods')
is_attack = True if task_group.group('type') and task_group.group('type').upper() == 'A' else False
comment = task_group.group('comment')
result, beats, success, effect = cypher_roll(difficulty=difficulty, mods=mods, is_attack=is_attack)
if success is not None:
if success:
if effect:
result_str = f"9succeeded, with {effect}!"
else:
result_str = "9succeeded!"
else:
if effect:
result_str = f"4failed, with {effect}!"
else:
result_str = "4failed."
else:
if effect:
result_str = f"beats a difficulty {beats} task, with {effect}!"
else:
result_str = f"beats a difficulty {beats} task."
if success is not None:
# show the adjusted difficulty
detail_str = f"14(d20={result} vs. diff. {difficulty}{mods})"
else:
detail_str = f"14(d20={result}{f' with {mods} levels' if mods else ''})"
if comment:
return self.bot.reply(event, f"{nick}: {comment} {result_str} {detail_str}")
else:
type_str = 'attack' if is_attack else 'check'
return self.bot.reply(event, f"{nick}: your {type_str} {result_str} {detail_str}")
def handle_random(self, connection, event, match):
"""Handle the !random command which picks an item from a list."""
nick = NickMask(event.source).nick
@ -55,6 +106,20 @@ class Dice(Plugin):
reply = "{0:s}".format(choice)
return self.bot.reply(event, reply)
def handle_reaction_roll(self, connection, event, match):
"""Handle the !reaction roll."""
nick = NickMask(event.source).nick
roll, label, summary = reaction_roll()
if summary in ('--', '-'):
result_str = f"4{label}"
elif summary == '~':
result_str = f"3{label}"
else:
result_str = f"9{label}"
return self.bot.reply(event, f"{nick}: the current disposition is {result_str} 14({roll})")
def handle_roll(self, connection, event, match):
"""Handle the !roll command which covers most common dice stuff."""
nick = NickMask(event.source).nick

71
dice/lib.py Normal file
View File

@ -0,0 +1,71 @@
"""Dice rolling operations (outside of the lex/yacc roller)."""
import random
import numexpr
rand = random.SystemRandom()
def cypher_roll(difficulty=None, mods=0, is_attack=False):
"""Make a Cypher System roll.
Args:
difficulty: the original difficulty to beat; if provided, success or failure is indicated in the results
mods: eases(-) and hindrances(+) to apply to the check, as a string (e.g. '-3+1')
is_attack: if the roll is an attack (in which case the damage-only effects are included)
Returns:
tuple of:
- the result on the d20
- the highest difficulty beaten
- if the difficulty is known, if the target was beat
- miscellaneous effects
"""
roll = rand.randint(1, 20)
if roll == 1:
return (roll, None, False if difficulty else None, 'a GM intrusion')
effect = None
if roll == 17 and is_attack:
effect = '+1 damage'
elif roll == 18 and is_attack:
effect = '+2 damage'
elif roll == 19:
effect = 'a minor effect'
elif roll == 20:
effect = 'a MAJOR EFFECT'
# if we know the difficulty, the mods would adjust the difficulty, but for the case where we don't,
# and maybe just in general, it's easier to modify the difficulty that the roll beats, so we flip the logic
# if incoming eases are a negative number, they should add to the difficulty the roll beats
beats = (roll // 3) - (numexpr.evaluate(mods).item() if mods else 0)
beats = 0 if beats < 0 else beats
return (roll, beats, difficulty <= beats if difficulty else None, effect)
def reaction_roll():
"""Make a reaction roll, which gives some oracle-like randomness to uncertain situations (for solo play or similar).
Returns:
tuple of:
- the result on the d20
- the label for the result
- a summary of the result ('++', '+', '~', '-', '--') (best to worst)
"""
roll = rand.randint(1, 20)
if 1 == roll:
label = 'VERY negative'
summary = '--'
elif 2 <= roll <= 6:
label = 'negative'
summary = '-'
elif 7 <= roll <= 14:
label = 'positive, with complications'
summary = '~'
elif 15 <= roll <= 19:
label = 'positive'
summary = '+'
else:
label = 'VERY positive'
summary = '++'
return (roll, label, summary)

View File

@ -93,8 +93,12 @@ class DiceRoller(object):
size = m[1]
if keep > dice or keep == 0:
keep = dice
assert size >= 1, f"Die must have at least one side."
assert dice >= 1, f"At least one die must be rolled."
if size < 1:
output = "# of sides for die is incorrect: %d" % size
return output
if dice < 1:
output = "# of dice is incorrect: %d" % dice
return output
res = self.roll_dice(keep, dice, size)
curr_str += "%d%s" % (res[0], res[1])
res = res[0]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dispatch', '0007_rename_type_dispatcheraction_action_type'),
]
operations = [
migrations.AlterField(
model_name='dispatcher',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='dispatcheraction',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -1,4 +1 @@
"""Set up the version number of the bot."""
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
"""Set up the core bot."""

View File

@ -1,520 +0,0 @@
# This file helps to compute a version number in source trees obtained from
# git-archive tarball (such as those provided by githubs download-from-tag
# feature). Distribution tarballs (built by setup.py sdist) and build
# directories (produced by setup.py build) will contain a much shorter file
# that just contains the computed version number.
# This file is released into the public domain. Generated by
# versioneer-0.18 (https://github.com/warner/python-versioneer)
"""Git implementation of _version.py."""
import errno
import os
import re
import subprocess
import sys
def get_keywords():
"""Get the keywords needed to look up the version information."""
# these strings will be replaced by git during git-archive.
# setup.py/versioneer.py will grep for the variable names, so they must
# each be defined on a line of their own. _version.py will just call
# get_keywords().
git_refnames = "$Format:%d$"
git_full = "$Format:%H$"
git_date = "$Format:%ci$"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
class VersioneerConfig:
"""Container for Versioneer configuration parameters."""
def get_config():
"""Create, populate and return the VersioneerConfig() object."""
# these strings are filled in when 'setup.py versioneer' creates
# _version.py
cfg = VersioneerConfig()
cfg.VCS = "git"
cfg.style = "pep440"
cfg.tag_prefix = "v"
cfg.parentdir_prefix = "None"
cfg.versionfile_source = "dr_botzo/_version.py"
cfg.verbose = False
return cfg
class NotThisMethod(Exception):
"""Exception raised if a method is not valid for the current scenario."""
LONG_VERSION_PY = {}
HANDLERS = {}
def register_vcs_handler(vcs, method): # decorator
"""Decorator to mark a method as the handler for a particular VCS."""
def decorate(f):
"""Store f in HANDLERS[vcs][method]."""
if vcs not in HANDLERS:
HANDLERS[vcs] = {}
HANDLERS[vcs][method] = f
return f
return decorate
def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
env=None):
"""Call the given command(s)."""
assert isinstance(commands, list)
p = None
for c in commands:
try:
dispcmd = str([c] + args)
# remember shell=False, so use git.cmd on windows, not just git
p = subprocess.Popen([c] + args, cwd=cwd, env=env,
stdout=subprocess.PIPE,
stderr=(subprocess.PIPE if hide_stderr
else None))
break
except EnvironmentError:
e = sys.exc_info()[1]
if e.errno == errno.ENOENT:
continue
if verbose:
print("unable to run %s" % dispcmd)
print(e)
return None, None
else:
if verbose:
print("unable to find command, tried %s" % (commands,))
return None, None
stdout = p.communicate()[0].strip()
if sys.version_info[0] >= 3:
stdout = stdout.decode()
if p.returncode != 0:
if verbose:
print("unable to run %s (error)" % dispcmd)
print("stdout was %s" % stdout)
return None, p.returncode
return stdout, p.returncode
def versions_from_parentdir(parentdir_prefix, root, verbose):
"""Try to determine the version from the parent directory name.
Source tarballs conventionally unpack into a directory that includes both
the project name and a version string. We will also support searching up
two directory levels for an appropriately named parent directory
"""
rootdirs = []
for i in range(3):
dirname = os.path.basename(root)
if dirname.startswith(parentdir_prefix):
return {"version": dirname[len(parentdir_prefix):],
"full-revisionid": None,
"dirty": False, "error": None, "date": None}
else:
rootdirs.append(root)
root = os.path.dirname(root) # up a level
if verbose:
print("Tried directories %s but none started with prefix %s" %
(str(rootdirs), parentdir_prefix))
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
@register_vcs_handler("git", "get_keywords")
def git_get_keywords(versionfile_abs):
"""Extract version information from the given file."""
# the code embedded in _version.py can just fetch the value of these
# keywords. When used from setup.py, we don't want to import _version.py,
# so we do it with a regexp instead. This function is not used from
# _version.py.
keywords = {}
try:
f = open(versionfile_abs, "r")
for line in f.readlines():
if line.strip().startswith("git_refnames ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
keywords["refnames"] = mo.group(1)
if line.strip().startswith("git_full ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
keywords["full"] = mo.group(1)
if line.strip().startswith("git_date ="):
mo = re.search(r'=\s*"(.*)"', line)
if mo:
keywords["date"] = mo.group(1)
f.close()
except EnvironmentError:
pass
return keywords
@register_vcs_handler("git", "keywords")
def git_versions_from_keywords(keywords, tag_prefix, verbose):
"""Get version information from git keywords."""
if not keywords:
raise NotThisMethod("no keywords at all, weird")
date = keywords.get("date")
if date is not None:
# git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant
# datestamp. However we prefer "%ci" (which expands to an "ISO-8601
# -like" string, which we must then edit to make compliant), because
# it's been around since git-1.5.3, and it's too difficult to
# discover which version we're using, or to work around using an
# older one.
date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
refnames = keywords["refnames"].strip()
if refnames.startswith("$Format"):
if verbose:
print("keywords are unexpanded, not using")
raise NotThisMethod("unexpanded keywords, not a git-archive tarball")
refs = set([r.strip() for r in refnames.strip("()").split(",")])
# starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of
# just "foo-1.0". If we see a "tag: " prefix, prefer those.
TAG = "tag: "
tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)])
if not tags:
# Either we're using git < 1.8.3, or there really are no tags. We use
# a heuristic: assume all version tags have a digit. The old git %d
# expansion behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us distinguish
# between branches and tags. By ignoring refnames without digits, we
# filter out many common branch names like "release" and
# "stabilization", as well as "HEAD" and "master".
tags = set([r for r in refs if re.search(r'\d', r)])
if verbose:
print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
print("likely tags: %s" % ",".join(sorted(tags)))
for ref in sorted(tags):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
return {"version": r,
"full-revisionid": keywords["full"].strip(),
"dirty": False, "error": None,
"date": date}
# no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
print("no suitable tags, using unknown + full revision id")
return {"version": "0+unknown",
"full-revisionid": keywords["full"].strip(),
"dirty": False, "error": "no suitable tags", "date": None}
@register_vcs_handler("git", "pieces_from_vcs")
def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
"""Get version from 'git describe' in the root of the source tree.
This only gets called if the git-archive 'subst' keywords were *not*
expanded, and _version.py hasn't already been rewritten with a short
version string, meaning we're inside a checked out source tree.
"""
GITS = ["git"]
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]
out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
hide_stderr=True)
if rc != 0:
if verbose:
print("Directory %s not under git control" % root)
raise NotThisMethod("'git rev-parse --git-dir' returned error")
# if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty]
# if there isn't one, this yields HEX[-dirty] (no NUM)
describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty",
"--always", "--long",
"--match", "%s*" % tag_prefix],
cwd=root)
# --long was added in git-1.5.5
if describe_out is None:
raise NotThisMethod("'git describe' failed")
describe_out = describe_out.strip()
full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root)
if full_out is None:
raise NotThisMethod("'git rev-parse' failed")
full_out = full_out.strip()
pieces = {}
pieces["long"] = full_out
pieces["short"] = full_out[:7] # maybe improved later
pieces["error"] = None
# parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
# TAG might have hyphens.
git_describe = describe_out
# look for -dirty suffix
dirty = git_describe.endswith("-dirty")
pieces["dirty"] = dirty
if dirty:
git_describe = git_describe[:git_describe.rindex("-dirty")]
# now we have TAG-NUM-gHEX or HEX
if "-" in git_describe:
# TAG-NUM-gHEX
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
if not mo:
# unparseable. Maybe git-describe is misbehaving?
pieces["error"] = ("unable to parse git-describe output: '%s'"
% describe_out)
return pieces
# tag
full_tag = mo.group(1)
if not full_tag.startswith(tag_prefix):
if verbose:
fmt = "tag '%s' doesn't start with prefix '%s'"
print(fmt % (full_tag, tag_prefix))
pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
% (full_tag, tag_prefix))
return pieces
pieces["closest-tag"] = full_tag[len(tag_prefix):]
# distance: number of commits since tag
pieces["distance"] = int(mo.group(2))
# commit: short hex revision ID
pieces["short"] = mo.group(3)
else:
# HEX: no tags
pieces["closest-tag"] = None
count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"],
cwd=root)
pieces["distance"] = int(count_out) # total number of commits
# commit date: see ISO-8601 comment in git_versions_from_keywords()
date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
cwd=root)[0].strip()
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
return pieces
def plus_or_dot(pieces):
"""Return a + if we don't already have one, else return a ."""
if "+" in pieces.get("closest-tag", ""):
return "."
return "+"
def render_pep440(pieces):
"""Build up version string, with post-release "local version identifier".
Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
Exceptions:
1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += plus_or_dot(pieces)
rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
else:
# exception #1
rendered = "0+untagged.%d.g%s" % (pieces["distance"],
pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
return rendered
def render_pep440_pre(pieces):
"""TAG[.post.devDISTANCE] -- No -dirty.
Exceptions:
1: no tags. 0.post.devDISTANCE
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"]:
rendered += ".post.dev%d" % pieces["distance"]
else:
# exception #1
rendered = "0.post.dev%d" % pieces["distance"]
return rendered
def render_pep440_post(pieces):
"""TAG[.postDISTANCE[.dev0]+gHEX] .
The ".dev0" means dirty. Note that .dev0 sorts backwards
(a dirty tree will appear "older" than the corresponding clean one),
but you shouldn't be releasing software with -dirty anyways.
Exceptions:
1: no tags. 0.postDISTANCE[.dev0]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += ".post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
rendered += plus_or_dot(pieces)
rendered += "g%s" % pieces["short"]
else:
# exception #1
rendered = "0.post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
rendered += "+g%s" % pieces["short"]
return rendered
def render_pep440_old(pieces):
"""TAG[.postDISTANCE[.dev0]] .
The ".dev0" means dirty.
Eexceptions:
1: no tags. 0.postDISTANCE[.dev0]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += ".post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
else:
# exception #1
rendered = "0.post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
return rendered
def render_git_describe(pieces):
"""TAG[-DISTANCE-gHEX][-dirty].
Like 'git describe --tags --dirty --always'.
Exceptions:
1: no tags. HEX[-dirty] (note: no 'g' prefix)
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"]:
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
else:
# exception #1
rendered = pieces["short"]
if pieces["dirty"]:
rendered += "-dirty"
return rendered
def render_git_describe_long(pieces):
"""TAG-DISTANCE-gHEX[-dirty].
Like 'git describe --tags --dirty --always -long'.
The distance/hash is unconditional.
Exceptions:
1: no tags. HEX[-dirty] (note: no 'g' prefix)
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
else:
# exception #1
rendered = pieces["short"]
if pieces["dirty"]:
rendered += "-dirty"
return rendered
def render(pieces, style):
"""Render the given version pieces into the requested style."""
if pieces["error"]:
return {"version": "unknown",
"full-revisionid": pieces.get("long"),
"dirty": None,
"error": pieces["error"],
"date": None}
if not style or style == "default":
style = "pep440" # the default
if style == "pep440":
rendered = render_pep440(pieces)
elif style == "pep440-pre":
rendered = render_pep440_pre(pieces)
elif style == "pep440-post":
rendered = render_pep440_post(pieces)
elif style == "pep440-old":
rendered = render_pep440_old(pieces)
elif style == "git-describe":
rendered = render_git_describe(pieces)
elif style == "git-describe-long":
rendered = render_git_describe_long(pieces)
else:
raise ValueError("unknown style '%s'" % style)
return {"version": rendered, "full-revisionid": pieces["long"],
"dirty": pieces["dirty"], "error": None,
"date": pieces.get("date")}
def get_versions():
"""Get version information or return default if unable to do so."""
# I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have
# __file__, we can work backwards from there to the root. Some
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
# case we can only use expanded keywords.
cfg = get_config()
verbose = cfg.verbose
try:
return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
verbose)
except NotThisMethod:
pass
try:
root = os.path.realpath(__file__)
# versionfile_source is the relative path from the top of the source
# tree (where the .git directory might live) to this file. Invert
# this to find the root from __file__.
for i in cfg.versionfile_source.split('/'):
root = os.path.dirname(root)
except NameError:
return {"version": "0+unknown", "full-revisionid": None,
"dirty": None,
"error": "unable to find root of source tree",
"date": None}
try:
pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
return render(pieces, cfg.style)
except NotThisMethod:
pass
try:
if cfg.parentdir_prefix:
return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
except NotThisMethod:
pass
return {"version": "0+unknown", "full-revisionid": None,
"dirty": None,
"error": "unable to compute version", "date": None}

View File

@ -2,20 +2,21 @@
Django settings for dr_botzo project.
For more information on this file, see
https://docs.djangoproject.com/en/1.6/topics/settings/
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.6/ref/settings/
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
from pathlib import Path
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Build paths inside the project like this: BASE_DIR / 'subdir'
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.6/howto/deployment/checklist/
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '8@frp#a5wb)40g=#rbbxy($_!ttqw(*t_^os37_a*9kbx1xuvp'
@ -28,8 +29,8 @@ ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin.apps.SimpleAdminConfig',
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
@ -37,7 +38,6 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
'django.contrib.sites',
'django_extensions',
'adminplus',
'bootstrap3',
'rest_framework',
'countdown',
@ -50,18 +50,18 @@ INSTALLED_APPS = (
'races',
'seen',
'storycraft',
)
]
MIDDLEWARE = (
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
]
ROOT_URLCONF = 'dr_botzo.urls'
@ -86,21 +86,37 @@ WSGI_APPLICATION = 'dr_botzo.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.6/ref/settings/#databases
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# inherited default, look at changing to BigAutoField
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.6/topics/i18n/
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
@ -108,8 +124,6 @@ TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
@ -118,15 +132,21 @@ SITE_ID = 1
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.6/howto/static-files/
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = '/static/'
STATIC_URL = 'static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static'),
)
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
@ -147,7 +167,7 @@ BOOTSTRAP3 = {
}
# whitenoise
STATIC_PATH = os.path.join(BASE_DIR, 'staticfiles')
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
###############
# web options #

View File

@ -1,14 +1,9 @@
"""General/baselite/site-wide URLs."""
from adminplus.sites import AdminSitePlus
from django.conf.urls import include
from django.contrib import admin
from django.urls import path
from django.views.generic import TemplateView
admin.site = AdminSitePlus()
admin.sites.site = admin.site
admin.autodiscover()
urlpatterns = [
path('', TemplateView.as_view(template_name='index.html'), name='index'),

View File

@ -2,7 +2,6 @@
from django.db import models, migrations
import datetime
from django.utils.timezone import utc
class Migration(migrations.Migration):
@ -15,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='fact',
name='time',
field=models.DateTimeField(default=datetime.datetime(2015, 6, 20, 15, 22, 20, 481856, tzinfo=utc), auto_now_add=True),
field=models.DateTimeField(default=datetime.datetime(2015, 6, 20, 15, 22, 20, 481856, tzinfo=datetime.timezone.utc), auto_now_add=True),
preserve_default=False,
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('facts', '0006_factcategory_show_all_entries'),
]
operations = [
migrations.AlterField(
model_name='fact',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='factcategory',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -1,40 +1,15 @@
"""Manage ircbot models and admin actions in the admin interface."""
import logging
import xmlrpc.client
from django.contrib import admin
from django.shortcuts import render
from ircbot.forms import PrivmsgForm
from ircbot.models import Alias, BotUser, IrcChannel, IrcPlugin, IrcServer
log = logging.getLogger('ircbot.admin')
def send_privmsg(request):
"""Send a privmsg over XML-RPC to the IRC bot."""
if request.method == 'POST':
form = PrivmsgForm(request.POST)
if form.is_valid():
target = form.cleaned_data['target']
message = form.cleaned_data['message']
bot_url = 'http://{0:s}:{1:d}/'.format(form.cleaned_data['xmlrpc_host'],
form.cleaned_data['xmlrpc_port'])
bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
bot.reply(None, message, False, target)
form = PrivmsgForm()
else:
form = PrivmsgForm()
return render(request, 'privmsg.html', {'form': form})
admin.site.register(Alias)
admin.site.register(BotUser)
admin.site.register(IrcChannel)
admin.site.register(IrcPlugin)
admin.site.register(IrcServer)
admin.site.register_view('ircbot/privmsg/', "Ircbot - privmsg", view=send_privmsg, urlname='ircbot_privmsg')

View File

@ -20,7 +20,7 @@ from irc.dict import IRCDict
from jaraco.stream import buffer
import ircbot.lib as ircbotlib
from dr_botzo import __version__
from dr_botzo._version import __version__
from ircbot.models import Alias, IrcChannel, IrcPlugin, IrcServer
log = logging.getLogger('ircbot.bot')
@ -161,7 +161,14 @@ class DrReactor(irc.client.Reactor):
channel = IrcChannel.objects.get(server=connection.server_config, name=sent_location)
if sender_nick == channel.discord_bridge:
short_what = ' '.join(what.split(' ')[1:])
real_source = re.sub(r'^<(\S+)> .*', r'\1!user@discord-bridge', what)
match = re.match(addressed_pattern, short_what, re.IGNORECASE)
event.arguments[0] = short_what
event.source = real_source
sender_nick = irc.client.NickMask(event.source).nick
event.sender_nick = sender_nick
event.in_privmsg = sender_nick == sent_location
else:
match = re.match(addressed_pattern, what, re.IGNORECASE)
except IrcChannel.DoesNotExist:
@ -174,10 +181,10 @@ class DrReactor(irc.client.Reactor):
log.debug("all_nicks: %s, addressed: %s", all_nicks, event.addressed)
# only do aliasing for pubmsg/privmsg
log.debug("checking for alias for %s", what)
log.debug("checking for alias for %s", event.arguments[0])
for alias in Alias.objects.all():
repl = alias.replace(what)
repl = alias.replace(event.arguments[0])
if repl:
# we found an alias for our given string, doing a replace
event.arguments[0] = repl
@ -530,9 +537,13 @@ class IRCBot(irc.client.SimpleIRCClient):
# run automsg commands
if self.server_config.post_connect:
for cmd in self.server_config.post_connect.split('\n'):
cmd_split = cmd.split(' ')
# TODO NOTE: if the bot is sending something that changes the vhost
# (like 'hostserv on') we don't pick it up
self.connection.privmsg(cmd.split(' ')[0], ' '.join(cmd.split(' ')[1:]))
if cmd_split[0] == '/msg':
self.connection.privmsg(cmd.split(' ')[1], ' '.join(cmd.split(' ')[2:]))
elif cmd_split[0] == '/mode':
self.connection.mode(*cmd_split[1:])
# sleep before doing autojoins
time.sleep(self.server_config.delay_before_joins)

View File

@ -0,0 +1,38 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0019_ircchannel_discord_bridge'),
]
operations = [
migrations.AlterField(
model_name='alias',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='botuser',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='ircchannel',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='ircplugin',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='ircserver',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -1,15 +0,0 @@
{% extends 'adminplus/index.html' %}
{% block title %}Ircbot - Privmsg{% endblock %}
{% block content %}
<div id="content-main">
<form id="ircbot_privmsg_form" action="{% url 'admin:ircbot_privmsg' %}" method="post">
{% csrf_token %}
<table>
{{ form }}
</table>
<input class="submit-button" type="submit" value="Privmsg"/>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('karma', '0002_auto_20150519_2156'),
]
operations = [
migrations.AlterField(
model_name='karmakey',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='karmalogentry',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -1,14 +1,13 @@
"""Karma logging models."""
from datetime import timedelta
import logging
from datetime import timedelta
import pytz
from irc.client import NickMask
from django.conf import settings
from django.db import models
from django.utils import timezone
from irc.client import NickMask
log = logging.getLogger('karma.models')
@ -44,12 +43,13 @@ class KarmaKey(models.Model):
objects = KarmaKeyManager()
def __str__(self):
"""String representation."""
"""Display the karma key and score."""
return "{0:s} ({1:d})".format(self.key, self.score())
def score(self):
"""Determine the score for this karma entry."""
return KarmaLogEntry.objects.filter(key=self).aggregate(models.Sum('delta'))['delta__sum']
score = KarmaLogEntry.objects.filter(key=self).aggregate(models.Sum('delta'))['delta__sum']
return score if score else 0
def rank(self):
"""Determine the rank of this karma entry relative to the others."""

View File

@ -1,15 +1,9 @@
"""Manage Markov models and administrative commands."""
import logging
from django.contrib import admin
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render
from markov.forms import LogUploadForm, TeachLineForm
import markov.lib as markovlib
from markov.models import MarkovContext, MarkovTarget, MarkovState
from markov.models import MarkovContext, MarkovState, MarkovTarget
log = logging.getLogger('markov.admin')
@ -17,96 +11,3 @@ log = logging.getLogger('markov.admin')
admin.site.register(MarkovContext)
admin.site.register(MarkovTarget)
admin.site.register(MarkovState)
@permission_required('import_text_file', raise_exception=True)
def import_file(request):
"""Accept a file upload and turn it into markov stuff.
Current file formats supported:
* weechat
"""
if request.method == 'POST':
form = LogUploadForm(request.POST, request.FILES)
if form.is_valid():
if form.cleaned_data['text_file_format'] == LogUploadForm.FILE_FORMAT_WEECHAT:
text_file = request.FILES['text_file']
context = form.cleaned_data['context']
ignores = form.cleaned_data['ignore_nicks'].split(',')
strips = form.cleaned_data['strip_prefixes'].split(' ')
whos = []
for line in text_file:
log.debug(line)
(timestamp, who, what) = line.decode('utf-8').split('\t', 2)
if who in ('-->', '<--', '--', ' *'):
continue
if who in ignores:
continue
whos.append(who)
# this is a line we probably care about now
what = [x for x in what.rstrip().split(' ') if x not in strips]
markovlib.learn_line(' '.join(what), context)
log.debug("learned")
log.debug(set(whos))
form = LogUploadForm()
elif form.cleaned_data['text_file_format'] == LogUploadForm.FILE_FORMAT_RAW_TEXT:
text_file = request.FILES['text_file']
context = form.cleaned_data['context']
k1 = MarkovState._start1
k2 = MarkovState._start2
for line in text_file:
for word in [x for x in line.decode('utf-8') .rstrip().split(' ')]:
log.info(word)
if word:
state, created = MarkovState.objects.get_or_create(context=context, k1=k1,
k2=k2, v=word)
state.count += 1
state.save()
if word[-1] in ['.', '?', '!']:
state, created = MarkovState.objects.get_or_create(context=context, k1=k2,
k2=word, v=MarkovState._stop)
state.count += 1
state.save()
k1 = MarkovState._start1
k2 = MarkovState._start2
else:
k1 = k2
k2 = word
else:
form = LogUploadForm()
return render(request, 'markov/import_file.html', {'form': form})
@permission_required('teach_line', raise_exception=True)
def teach_line(request):
"""Teach one line directly."""
if request.method == 'POST':
form = TeachLineForm(request.POST)
if form.is_valid():
line = form.cleaned_data['line']
context = form.cleaned_data['context']
strips = form.cleaned_data['strip_prefixes'].split(' ')
what = [x for x in line.rstrip().split(' ') if x not in strips]
markovlib.learn_line(' '.join(what), context)
form = TeachLineForm()
else:
form = TeachLineForm()
return render(request, 'markov/teach_line.html', {'form': form})
admin.site.register_view('markov/importfile/', "Markov - Import log file", view=import_file,
urlname='markov_import_file')
admin.site.register_view('markov/teach/', "Markov - Teach line", view=teach_line, urlname='markov_teach_line')

View File

@ -0,0 +1,28 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('markov', '0008_alter_markovtarget_name'),
]
operations = [
migrations.AlterField(
model_name='markovcontext',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='markovstate',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='markovtarget',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.9 on 2024-11-14 15:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('markov', '0009_alter_markovcontext_id_alter_markovstate_id_and_more'),
]
operations = [
migrations.AddField(
model_name='markovcontext',
name='web_enabled',
field=models.BooleanField(default=True),
),
]

View File

@ -12,6 +12,7 @@ class MarkovContext(models.Model):
"""Define contexts for Markov chains."""
name = models.CharField(max_length=200, unique=True)
web_enabled = models.BooleanField(default=True)
def __str__(self):
"""Provide string representation."""

View File

@ -1,15 +0,0 @@
{% extends 'adminplus/index.html' %}
{% block title %}Markov - Import log file{% endblock %}
{% block content %}
<div id="content-main">
<form id="markov_import_file_form" enctype="multipart/form-data" action="{% url 'admin:markov_import_file' %}" method="post">
{% csrf_token %}
<table>
{{ form }}
</table>
<input class="submit-button" type="submit" value="Import"/>
</form>
</div>
{% endblock %}

View File

@ -1,15 +0,0 @@
{% extends 'adminplus/index.html' %}
{% block title %}Markov - Teach line{% endblock %}
{% block content %}
<div id="content-main">
<form id="markov_teach_line_form" action="{% url 'admin:markov_teach_line' %}" method="post">
{% csrf_token %}
<table>
{{ form }}
</table>
<input class="submit-button" type="submit" value="Teach"/>
</form>
</div>
{% endblock %}

View File

@ -3,6 +3,7 @@ import json
import logging
import time
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from rest_framework.authentication import BasicAuthentication
@ -27,6 +28,8 @@ def context_index(request, context_id):
start_t = time.time()
context = get_object_or_404(MarkovContext, pk=context_id)
if not context.web_enabled:
raise PermissionDenied()
chain = " ".join(markovlib.generate_line(context))
end_t = time.time()

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pi', '0004_simulation_x_y_logging'),
]
operations = [
migrations.AlterField(
model_name='pilog',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

80
pyproject.toml Normal file
View File

@ -0,0 +1,80 @@
[build-system]
requires = ["setuptools>=61.0", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "dr.botzo"
description = "A modularized IRC bot with a Django backend."
readme = "README.md"
license = {text = "GPL-3.0"}
authors = [
{name = "Brian S. Stephan", email = "bss@incorporeal.org"},
]
requires-python = ">=3.10,<3.12"
dependencies = ["Django<5.1", "django-bootstrap3", "django-extensions", "djangorestframework", "irc<20.5.0", "numexpr",
"parsedatetime", "ply", "python-dateutil", "python-mpd2", "pytz", "requests", "whitenoise", "zalgo-text"]
dynamic = ["version"]
classifiers = [
"Framework :: Django",
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
"Topic :: Communications :: Chat :: Internet Relay Chat",
]
[project.urls]
"Homepage" = "https://git.incorporeal.org/bss/dr.botzo"
"Changelog" = "https://git.incorporeal.org/bss/dr.botzo/releases"
"Bug Tracker" = "https://git.incorporeal.org/bss/dr.botzo/issues"
[project.optional-dependencies]
dev = ["bandit", "dlint", "flake8", "flake8-blind-except", "flake8-builtins", "flake8-docstrings",
"flake8-executable", "flake8-fixme", "flake8-isort", "flake8-logging-format", "flake8-mutable",
"flake8-pyproject", "pip-tools", "pytest", "pytest-cov", "pytest-django", "reuse", "safety", "tox"]
[tool.flake8]
enable-extensions = "G,M"
exclude = [".tox/", "dr_botzo/_version.py", "**/migrations/"]
extend-ignore = "T101"
max-complexity = 10
max-line-length = 120
[tool.isort]
line_length = 120
# TODO: mypy
[tool.mypy]
ignore_missing_imports = true
[tool.pytest.ini_options]
python_files = ["*_tests.py", "tests.py", "test_*.py"]
DJANGO_SETTINGS_MODULE = "dr_botzo.settings"
django_find_project = false
# I think this can go away if I switch to a src/ based repo
[tool.setuptools]
packages = [
"acro",
"countdown",
"dice",
"dispatch",
"dr_botzo",
"facts",
"history",
"ircbot",
"karma",
"markov",
"mpdbot",
"pi",
"races",
"seen",
"static",
"storycraft",
"text_manip",
"transform",
"weather",
]
[tool.setuptools_scm]
write_to = "dr_botzo/_version.py"

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('races', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='racer',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='raceupdate',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -1,26 +0,0 @@
-r requirements.in
# testing runner, test reporting, packages used during testing (e.g. requests-mock), etc.
pytest
pytest-cov
pytest-django
# linting and other static code analysis
bandit
dlint
flake8 # flake8 and plugins, for local dev linting in vim
flake8-blind-except
flake8-builtins
flake8-docstrings
flake8-executable
flake8-fixme
flake8-isort
flake8-logging-format
flake8-mutable
safety
# maintenance utilities and tox
pip-tools # pip-compile
tox<4 # CI stuff
tox-wheel # build wheels in tox
versioneer # automatic version numbering

View File

@ -1,233 +1,292 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
# pip-compile --extra=dev --output-file=requirements/requirements-dev.txt
#
asgiref==3.6.0
annotated-types==0.7.0
# via pydantic
asgiref==3.8.1
# via django
attrs==22.2.0
# via pytest
attrs==24.2.0
# via reuse
authlib==1.3.2
# via safety
autocommand==2.2.2
# via jaraco-text
bandit==1.7.4
# via -r requirements/requirements-dev.in
build==0.10.0
backports-tarfile==1.2.0
# via jaraco-context
bandit==1.7.10
# via dr.botzo (pyproject.toml)
binaryornot==0.4.4
# via reuse
boolean-py==4.0
# via
# license-expression
# reuse
build==1.2.2.post1
# via pip-tools
certifi==2022.12.7
cachetools==5.5.0
# via tox
certifi==2024.8.30
# via requests
charset-normalizer==3.0.1
cffi==1.17.1
# via cryptography
chardet==5.2.0
# via
# binaryornot
# python-debian
# tox
charset-normalizer==3.4.0
# via requests
click==8.1.3
click==8.1.7
# via
# pip-tools
# safety
coverage[toml]==7.2.1
# typer
colorama==0.4.6
# via tox
coverage[toml]==7.6.4
# via pytest-cov
distlib==0.3.6
cryptography==43.0.3
# via authlib
distlib==0.3.9
# via virtualenv
django==3.2.18
django==5.0.9
# via
# -r requirements/requirements.in
# django-bootstrap3
# django-extensions
# djangorestframework
django-adminplus==0.5
# via -r requirements/requirements.in
django-bootstrap3==22.2
# via -r requirements/requirements.in
django-extensions==3.2.1
# via -r requirements/requirements.in
djangorestframework==3.14.0
# via -r requirements/requirements.in
dlint==0.14.0
# via -r requirements/requirements-dev.in
dparse==0.6.2
# via safety
exceptiongroup==1.1.0
# via pytest
filelock==3.9.0
# dr.botzo (pyproject.toml)
django-bootstrap3==24.3
# via dr.botzo (pyproject.toml)
django-extensions==3.2.3
# via dr.botzo (pyproject.toml)
djangorestframework==3.15.2
# via dr.botzo (pyproject.toml)
dlint==0.15.0
# via dr.botzo (pyproject.toml)
dparse==0.6.4b0
# via
# safety
# safety-schemas
filelock==3.12.4
# via
# safety
# tox
# virtualenv
flake8==6.0.0
flake8==7.1.1
# via
# -r requirements/requirements-dev.in
# dlint
# dr.botzo (pyproject.toml)
# flake8-builtins
# flake8-docstrings
# flake8-executable
# flake8-isort
# flake8-mutable
# flake8-pyproject
flake8-blind-except==0.2.1
# via -r requirements/requirements-dev.in
flake8-builtins==2.1.0
# via -r requirements/requirements-dev.in
# via dr.botzo (pyproject.toml)
flake8-builtins==2.5.0
# via dr.botzo (pyproject.toml)
flake8-docstrings==1.7.0
# via -r requirements/requirements-dev.in
# via dr.botzo (pyproject.toml)
flake8-executable==2.1.3
# via -r requirements/requirements-dev.in
# via dr.botzo (pyproject.toml)
flake8-fixme==1.1.1
# via -r requirements/requirements-dev.in
flake8-isort==6.0.0
# via -r requirements/requirements-dev.in
flake8-logging-format==0.9.0
# via -r requirements/requirements-dev.in
# via dr.botzo (pyproject.toml)
flake8-isort==6.1.1
# via dr.botzo (pyproject.toml)
flake8-logging-format==2024.24.12
# via dr.botzo (pyproject.toml)
flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in
gitdb==4.0.10
# via gitpython
gitpython==3.1.31
# via bandit
idna==3.4
# via dr.botzo (pyproject.toml)
flake8-pyproject==1.2.3
# via dr.botzo (pyproject.toml)
idna==3.10
# via requests
inflect==6.0.2
# via jaraco-text
iniconfig==2.0.0
# via pytest
irc==20.1.0
# via -r requirements/requirements.in
isort==5.12.0
irc==20.4.3
# via dr.botzo (pyproject.toml)
isort==5.13.2
# via flake8-isort
jaraco-classes==3.2.3
# via jaraco-collections
jaraco-collections==3.8.0
jaraco-collections==5.1.0
# via irc
jaraco-context==4.3.0
jaraco-context==6.0.1
# via jaraco-text
jaraco-functools==3.6.0
jaraco-functools==4.1.0
# via
# irc
# jaraco-text
# tempora
jaraco-logging==3.1.2
jaraco-logging==3.3.0
# via irc
jaraco-stream==3.0.3
jaraco-stream==3.0.4
# via irc
jaraco-text==3.11.1
jaraco-text==4.0.0
# via
# irc
# jaraco-collections
jinja2==3.1.4
# via
# reuse
# safety
license-expression==30.4.0
# via reuse
markdown-it-py==3.0.0
# via rich
markupsafe==3.0.2
# via jinja2
marshmallow==3.23.0
# via safety
mccabe==0.7.0
# via flake8
more-itertools==9.1.0
mdurl==0.1.2
# via markdown-it-py
more-itertools==10.5.0
# via
# irc
# jaraco-classes
# jaraco-functools
# jaraco-stream
# jaraco-text
packaging==21.3
numexpr==2.10.1
# via dr.botzo (pyproject.toml)
numpy==2.1.3
# via numexpr
packaging==24.1
# via
# build
# dparse
# marshmallow
# pyproject-api
# pytest
# safety
# safety-schemas
# tox
parsedatetime==2.6
# via -r requirements/requirements.in
pbr==5.11.1
# via dr.botzo (pyproject.toml)
pbr==6.1.0
# via stevedore
pip-tools==6.12.3
# via -r requirements/requirements-dev.in
platformdirs==3.0.0
# via virtualenv
pluggy==1.0.0
pip-tools==7.4.1
# via dr.botzo (pyproject.toml)
platformdirs==4.3.6
# via
# tox
# virtualenv
pluggy==1.5.0
# via
# pytest
# tox
ply==3.11
# via -r requirements/requirements.in
py==1.11.0
# via tox
pycodestyle==2.10.0
# via dr.botzo (pyproject.toml)
psutil==6.0.0
# via safety
pycodestyle==2.12.1
# via flake8
pydantic==1.10.5
# via inflect
pycparser==2.22
# via cffi
pydantic==2.5.3
# via
# safety
# safety-schemas
pydantic-core==2.14.6
# via pydantic
pydocstyle==6.3.0
# via flake8-docstrings
pyflakes==3.0.1
pyflakes==3.2.0
# via flake8
pyparsing==3.0.9
# via packaging
pyproject-hooks==1.0.0
# via build
pytest==7.2.1
pygments==2.18.0
# via rich
pyproject-api==1.8.0
# via tox
pyproject-hooks==1.2.0
# via
# -r requirements/requirements-dev.in
# build
# pip-tools
pytest==8.3.3
# via
# dr.botzo (pyproject.toml)
# pytest-cov
# pytest-django
pytest-cov==4.0.0
# via -r requirements/requirements-dev.in
pytest-django==4.5.2
# via -r requirements/requirements-dev.in
python-dateutil==2.8.2
# via -r requirements/requirements.in
python-mpd2==3.0.5
# via -r requirements/requirements.in
pytz==2022.7.1
pytest-cov==6.0.0
# via dr.botzo (pyproject.toml)
pytest-django==4.9.0
# via dr.botzo (pyproject.toml)
python-dateutil==2.9.0.post0
# via
# -r requirements/requirements.in
# django
# djangorestframework
# irc
# dr.botzo (pyproject.toml)
# tempora
pyyaml==6.0
# via bandit
requests==2.28.2
# via safety
ruamel-yaml==0.17.21
# via safety
ruamel-yaml-clib==0.2.7
# via ruamel-yaml
safety==2.3.5
# via -r requirements/requirements-dev.in
six==1.16.0
python-debian==0.1.49
# via reuse
python-mpd2==3.1.1
# via dr.botzo (pyproject.toml)
pytz==2024.2
# via
# python-dateutil
# tox
smmap==5.0.0
# via gitdb
# dr.botzo (pyproject.toml)
# irc
pyyaml==6.0.2
# via bandit
requests==2.32.3
# via
# dr.botzo (pyproject.toml)
# safety
reuse==4.0.3
# via dr.botzo (pyproject.toml)
rich==13.9.3
# via
# bandit
# safety
# typer
ruamel-yaml==0.18.6
# via
# safety
# safety-schemas
ruamel-yaml-clib==0.2.12
# via ruamel-yaml
safety==3.2.10
# via dr.botzo (pyproject.toml)
safety-schemas==0.0.8
# via safety
shellingham==1.5.4
# via typer
six==1.16.0
# via python-dateutil
snowballstemmer==2.2.0
# via pydocstyle
sqlparse==0.4.3
sqlparse==0.5.1
# via django
stevedore==5.0.0
stevedore==5.3.0
# via bandit
tempora==5.2.1
tempora==5.7.0
# via
# irc
# jaraco-logging
toml==0.10.2
# via dparse
tomli==2.0.1
tomlkit==0.13.2
# via reuse
tox==4.11.4
# via dr.botzo (pyproject.toml)
typer==0.12.5
# via safety
typing-extensions==4.12.2
# via
# build
# coverage
# pyproject-hooks
# pytest
# tox
tox==3.28.0
# pydantic
# pydantic-core
# safety
# safety-schemas
# typer
urllib3==2.2.3
# via
# -r requirements/requirements-dev.in
# tox-wheel
tox-wheel==1.0.0
# via -r requirements/requirements-dev.in
typing-extensions==4.5.0
# via pydantic
urllib3==1.26.14
# via requests
versioneer==0.28
# via -r requirements/requirements-dev.in
virtualenv==20.20.0
# requests
# safety
virtualenv==20.27.1
# via tox
wheel==0.38.4
# via
# pip-tools
# tox-wheel
whitenoise==6.7.0
# via -r requirements/requirements.in
wheel==0.44.0
# via pip-tools
whitenoise==6.11.0
# via dr.botzo (pyproject.toml)
zalgo-text==0.6
# via -r requirements/requirements.in
# via dr.botzo (pyproject.toml)
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@ -1,13 +0,0 @@
Django<4.0 # core
django-adminplus # admin.site.register_view
django-bootstrap3 # bootstrap layout
django-extensions # more commands
djangorestframework # WS API
irc # core
parsedatetime # relative date stuff in countdown
ply # dice lex/yacc compiler
python-dateutil # countdown relative math
python-mpd2 # client for mpd
pytz # timezone awareness
whitenoise # easier static files
zalgo-text # zalgoify text

View File

@ -1,84 +1,89 @@
#
# This file is autogenerated by pip-compile with Python 3.10
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
# pip-compile --output-file=requirements/requirements.txt
#
asgiref==3.6.0
asgiref==3.8.1
# via django
autocommand==2.2.2
# via jaraco-text
django==3.2.18
backports-tarfile==1.2.0
# via jaraco-context
certifi==2024.8.30
# via requests
charset-normalizer==3.4.0
# via requests
django==5.0.9
# via
# -r requirements/requirements.in
# django-bootstrap3
# django-extensions
# djangorestframework
django-adminplus==0.5
# via -r requirements/requirements.in
django-bootstrap3==22.2
# via -r requirements/requirements.in
django-extensions==3.2.1
# via -r requirements/requirements.in
djangorestframework==3.14.0
# via -r requirements/requirements.in
inflect==6.0.2
# via jaraco-text
irc==20.1.0
# via -r requirements/requirements.in
jaraco-classes==3.2.3
# via jaraco-collections
jaraco-collections==3.8.0
# dr.botzo (pyproject.toml)
django-bootstrap3==24.3
# via dr.botzo (pyproject.toml)
django-extensions==3.2.3
# via dr.botzo (pyproject.toml)
djangorestframework==3.15.2
# via dr.botzo (pyproject.toml)
idna==3.10
# via requests
irc==20.4.3
# via dr.botzo (pyproject.toml)
jaraco-collections==5.1.0
# via irc
jaraco-context==4.3.0
jaraco-context==6.0.1
# via jaraco-text
jaraco-functools==3.6.0
jaraco-functools==4.1.0
# via
# irc
# jaraco-text
# tempora
jaraco-logging==3.1.2
jaraco-logging==3.3.0
# via irc
jaraco-stream==3.0.3
jaraco-stream==3.0.4
# via irc
jaraco-text==3.11.1
jaraco-text==4.0.0
# via
# irc
# jaraco-collections
more-itertools==9.1.0
more-itertools==10.5.0
# via
# irc
# jaraco-classes
# jaraco-functools
# jaraco-stream
# jaraco-text
numexpr==2.10.1
# via dr.botzo (pyproject.toml)
numpy==2.1.3
# via numexpr
parsedatetime==2.6
# via -r requirements/requirements.in
# via dr.botzo (pyproject.toml)
ply==3.11
# via -r requirements/requirements.in
pydantic==1.10.5
# via inflect
python-dateutil==2.8.2
# via -r requirements/requirements.in
python-mpd2==3.0.5
# via -r requirements/requirements.in
pytz==2022.7.1
# via dr.botzo (pyproject.toml)
python-dateutil==2.9.0.post0
# via
# -r requirements/requirements.in
# django
# djangorestframework
# irc
# dr.botzo (pyproject.toml)
# tempora
python-mpd2==3.1.1
# via dr.botzo (pyproject.toml)
pytz==2024.2
# via
# dr.botzo (pyproject.toml)
# irc
requests==2.32.3
# via dr.botzo (pyproject.toml)
six==1.16.0
# via python-dateutil
sqlparse==0.4.3
sqlparse==0.5.1
# via django
tempora==5.2.1
tempora==5.7.0
# via
# irc
# jaraco-logging
typing-extensions==4.5.0
# via pydantic
whitenoise==6.7.0
# via -r requirements/requirements.in
urllib3==2.2.3
# via requests
whitenoise==6.11.0
# via dr.botzo (pyproject.toml)
zalgo-text==0.6
# via -r requirements/requirements.in
# via dr.botzo (pyproject.toml)

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('seen', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='seennick',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -1,6 +0,0 @@
[versioneer]
VCS = git
style = pep440
versionfile_source = dr_botzo/_version.py
versionfile_build = dr_botzo/_version.py
tag_prefix = v

View File

@ -1,28 +0,0 @@
"""Setuptools configuration."""
import os
from setuptools import find_packages, setup
import versioneer
HERE = os.path.dirname(os.path.abspath(__file__))
def extract_requires():
with open(os.path.join(HERE, 'requirements/requirements.in'), 'r') as reqs:
return [line.split(' ')[0] for line in reqs if not line[0] == '-']
setup(
name="dr.botzo",
description="A Django-backed IRC bot that also provides a WS framework for other bots to call.",
url="https://git.incorporeal.org/bss/dr.botzo",
license='GPLv3',
author="Brian S. Stephan",
author_email="bss@incorporeal.org",
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
packages=find_packages(),
include_package_data=True,
zip_safe=False,
install_requires=extract_requires(),
)

View File

@ -2,7 +2,6 @@
from django.db import models, migrations
import datetime
from django.utils.timezone import utc
class Migration(migrations.Migration):
@ -15,7 +14,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='storycraftgame',
name='create_time',
field=models.DateTimeField(default=datetime.datetime(2015, 6, 20, 1, 51, 18, 778824, tzinfo=utc), auto_now_add=True),
field=models.DateTimeField(default=datetime.datetime(2015, 6, 20, 1, 51, 18, 778824, tzinfo=datetime.timezone.utc), auto_now_add=True),
preserve_default=False,
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 5.0.4 on 2024-05-03 17:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('storycraft', '0005_storycraftgame_create_time'),
]
operations = [
migrations.AlterField(
model_name='storycraftgame',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='storycraftline',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
migrations.AlterField(
model_name='storycraftplayer',
name='id',
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
]

View File

@ -0,0 +1,135 @@
"""Test IRC behavior of the dice plugin."""
import re
from unittest import mock
from django.test import TestCase
import dice.ircplugin
from ircbot.models import IrcServer
class MarkovTestCase(TestCase):
"""Test the markov plugin."""
fixtures = ['tests/fixtures/irc_server_fixture.json']
def setUp(self):
"""Create common objects."""
self.mock_bot = mock.MagicMock()
self.mock_connection = mock.MagicMock()
self.mock_connection.get_nickname.return_value = 'test_bot'
self.mock_connection.server_config = IrcServer.objects.get(pk=1)
self.plugin = dice.ircplugin.Dice(self.mock_bot, self.mock_connection, mock.MagicMock())
def test_cypher_roll_strings(self):
"""Simulate incoming Cypher System requests."""
mock_event = mock.MagicMock()
mock_event.source = 'test!test@test'
mock_event.target = '#test'
mock_event.recursing = False
# general task roll (no damage output on a 17)
mock_event.arguments = ['!cypher T3']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=17):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: your check 9succeeded! 14(d20=17 vs. diff. 3)'
)
# general attack roll (incl. damage output on a 17)
mock_event.arguments = ['!cypher A3']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=17):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: your attack 9succeeded, with +1 damage! 14(d20=17 vs. diff. 3)'
)
# general task roll, case insensitive
mock_event.arguments = ['!cypher t3']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=17):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: your check 9succeeded! 14(d20=17 vs. diff. 3)'
)
# unknown target roll
mock_event.arguments = ['!cypher +1']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=17):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: your check beats a difficulty 4 task. 14(d20=17 with +1 levels)'
)
# no mod or known difficulty
mock_event.arguments = ['!cypher unmodded attempt']
match = re.search(dice.ircplugin.CYPHER_COMMAND_REGEX, mock_event.arguments[0])
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
with mock.patch('random.SystemRandom.randint', return_value=9):
self.plugin.handle_cypher_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: unmodded attempt beats a difficulty 3 task. 14(d20=9)'
)
def test_reaction_roll_strings(self):
"""Simulate incoming reaction requests."""
mock_event = mock.MagicMock()
mock_event.source = 'test!test@test'
mock_event.target = '#test'
mock_event.recursing = False
# good and very good are bold green
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=18):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is 9positive 14(18)'
)
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=20):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is 9VERY positive 14(20)'
)
# decent is green
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=10):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is 3positive, with complications 14(10)'
)
# bad and very bad are bold red
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=4):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is 4negative 14(4)'
)
mock_event.arguments = ['!reaction']
match = re.search(dice.ircplugin.REACTION_COMMAND_REGEX, mock_event.arguments[0])
with mock.patch('random.SystemRandom.randint', return_value=1):
self.plugin.handle_reaction_roll(self.mock_connection, mock_event, match)
self.mock_bot.reply.assert_called_with(
mock_event,
'test: the current disposition is 4VERY negative 14(1)'
)

106
tests/test_dice_lib.py Normal file
View File

@ -0,0 +1,106 @@
"""Tests for dice operations."""
from unittest import mock
from django.test import TestCase
import dice.lib
class DiceLibTestCase(TestCase):
"""Test that a variety of dice rolls work as expected."""
def test_cypher_rolls(self):
"""Roll a variety of Cypher System rolls."""
# simple task, simple check
with mock.patch('random.SystemRandom.randint', return_value=5):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (5, 1, True, None))
# simple failure
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (2, 0, False, None))
# rolled a 1
with mock.patch('random.SystemRandom.randint', return_value=1):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (1, None, False, 'a GM intrusion'))
# rolled a 17 on an attack
with mock.patch('random.SystemRandom.randint', return_value=17):
result = dice.lib.cypher_roll(difficulty=1, is_attack=True)
self.assertEqual(result, (17, 5, True, '+1 damage'))
# rolled a 18 on an attack
with mock.patch('random.SystemRandom.randint', return_value=18):
result = dice.lib.cypher_roll(difficulty=1, is_attack=True)
self.assertEqual(result, (18, 6, True, '+2 damage'))
# rolled a 17
with mock.patch('random.SystemRandom.randint', return_value=17):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (17, 5, True, None))
# rolled a 18
with mock.patch('random.SystemRandom.randint', return_value=18):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (18, 6, True, None))
# rolled a 19
with mock.patch('random.SystemRandom.randint', return_value=19):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (19, 6, True, 'a minor effect'))
# rolled a 20
with mock.patch('random.SystemRandom.randint', return_value=20):
result = dice.lib.cypher_roll(difficulty=1)
self.assertEqual(result, (20, 6, True, 'a MAJOR EFFECT'))
# mods affect the result of what the roll beats
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(difficulty=1, mods='-5')
self.assertEqual(result, (2, 5, True, None))
# complex mods
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(difficulty=3, mods='+1-4')
self.assertEqual(result, (2, 3, True, None))
# complex mods
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(difficulty=3, mods='-4+1')
self.assertEqual(result, (2, 3, True, None))
# ...even without a difficulty known
with mock.patch('random.SystemRandom.randint', return_value=2):
result = dice.lib.cypher_roll(mods='-5')
self.assertEqual(result, (2, 5, None, None))
# general "don't know the difficulty" kind of check
with mock.patch('random.SystemRandom.randint', return_value=10):
result = dice.lib.cypher_roll(mods='-2')
self.assertEqual(result, (10, 5, None, None))
# general "don't know the difficulty" kind of check in the other direction
with mock.patch('random.SystemRandom.randint', return_value=10):
result = dice.lib.cypher_roll(mods='2')
self.assertEqual(result, (10, 1, None, None))
def test_reaction_roll(self):
"""Roll possible reactions."""
with mock.patch('random.SystemRandom.randint', return_value=1):
self.assertEqual(dice.lib.reaction_roll(), (1, 'VERY negative', '--'))
with mock.patch('random.SystemRandom.randint', return_value=2):
self.assertEqual(dice.lib.reaction_roll(), (2, 'negative', '-'))
with mock.patch('random.SystemRandom.randint', return_value=6):
self.assertEqual(dice.lib.reaction_roll(), (6, 'negative', '-'))
with mock.patch('random.SystemRandom.randint', return_value=7):
self.assertEqual(dice.lib.reaction_roll(), (7, 'positive, with complications', '~'))
with mock.patch('random.SystemRandom.randint', return_value=14):
self.assertEqual(dice.lib.reaction_roll(), (14, 'positive, with complications', '~'))
with mock.patch('random.SystemRandom.randint', return_value=15):
self.assertEqual(dice.lib.reaction_roll(), (15, 'positive', '+'))
with mock.patch('random.SystemRandom.randint', return_value=19):
self.assertEqual(dice.lib.reaction_roll(), (19, 'positive', '+'))
with mock.patch('random.SystemRandom.randint', return_value=20):
self.assertEqual(dice.lib.reaction_roll(), (20, 'VERY positive', '++'))

30
tests/test_dice_roller.py Normal file
View File

@ -0,0 +1,30 @@
"""Test the dice parsing and generator."""
from unittest import mock
from django.test import TestCase
import dice.roller
class DiceRollerTestCase(TestCase):
"""Test that a variety of dice rolls can be parsed."""
def setUp(self):
"""Create the dice roller."""
self.roller = dice.roller.DiceRoller()
def test_standard_rolls(self):
"""Roll a variety of normal rolls."""
with mock.patch('random.randint', return_value=5):
result = self.roller.do_roll('1d20')
self.assertEqual(result, '5 (5[5])')
with mock.patch('random.randint', side_effect=[5, 6]):
result = self.roller.do_roll('2d20')
self.assertEqual(result, '11 (11[5,6])')
with mock.patch('random.randint', side_effect=[1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3,
4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6]):
result = self.roller.do_roll('6#3/4d6')
self.assertEqual(result, '3 (3[1,1,1,1]), 6 (6[2,2,2,2]), 9 (9[3,3,3,3]), '
'12 (12[4,4,4,4]), 15 (15[5,5,5,5]), 18 (18[6,6,6,6])')

121
tox.ini
View File

@ -4,21 +4,11 @@
# and then run "tox" from this directory.
[tox]
envlist = begin,py38,py39,py310,coverage,security,lint,bundle
isolated_build = true
envlist = begin,py310,py311,coverage,security,lint
[testenv]
# build a wheel and test it
wheel = true
wheel_build_env = build
# whitelist commands we need
whitelist_externals = cp
# install everything via requirements-dev.txt, so that developer environment
# is the same as the tox environment (for ease of use/no weird gotchas in
# local dev results vs. tox results) and also to avoid ticky-tacky maintenance
# of "oh this particular env has weird results unless I install foo" --- just
# shotgun blast install everything everywhere
allow_externals = pytest, coverage
deps =
-rrequirements/requirements-dev.txt
@ -31,50 +21,6 @@ deps = setuptools
skip_install = true
commands = coverage erase
[testenv:py38]
# run pytest with coverage
commands =
pytest --cov-append --cov-branch \
--cov={envsitepackagesdir}/acro/ \
--cov={envsitepackagesdir}/countdown/ \
--cov={envsitepackagesdir}/dice/ \
--cov={envsitepackagesdir}/dispatch/ \
--cov={envsitepackagesdir}/dr_botzo/ \
--cov={envsitepackagesdir}/facts/ \
--cov={envsitepackagesdir}/gitlab_bot/ \
--cov={envsitepackagesdir}/ircbot/ \
--cov={envsitepackagesdir}/karma/ \
--cov={envsitepackagesdir}/markov/ \
--cov={envsitepackagesdir}/mpdbot/ \
--cov={envsitepackagesdir}/pi/ \
--cov={envsitepackagesdir}/races/ \
--cov={envsitepackagesdir}/seen/ \
--cov={envsitepackagesdir}/storycraft/ \
--cov={envsitepackagesdir}/transform/ \
--cov={envsitepackagesdir}/weather/
[testenv:py39]
# run pytest with coverage
commands =
pytest --cov-append --cov-branch \
--cov={envsitepackagesdir}/acro/ \
--cov={envsitepackagesdir}/countdown/ \
--cov={envsitepackagesdir}/dice/ \
--cov={envsitepackagesdir}/dispatch/ \
--cov={envsitepackagesdir}/dr_botzo/ \
--cov={envsitepackagesdir}/facts/ \
--cov={envsitepackagesdir}/gitlab_bot/ \
--cov={envsitepackagesdir}/ircbot/ \
--cov={envsitepackagesdir}/karma/ \
--cov={envsitepackagesdir}/markov/ \
--cov={envsitepackagesdir}/mpdbot/ \
--cov={envsitepackagesdir}/pi/ \
--cov={envsitepackagesdir}/races/ \
--cov={envsitepackagesdir}/seen/ \
--cov={envsitepackagesdir}/storycraft/ \
--cov={envsitepackagesdir}/transform/ \
--cov={envsitepackagesdir}/weather/
[testenv:py310]
# run pytest with coverage
commands =
@ -85,7 +31,7 @@ commands =
--cov={envsitepackagesdir}/dispatch/ \
--cov={envsitepackagesdir}/dr_botzo/ \
--cov={envsitepackagesdir}/facts/ \
--cov={envsitepackagesdir}/gitlab_bot/ \
--cov={envsitepackagesdir}/history/ \
--cov={envsitepackagesdir}/ircbot/ \
--cov={envsitepackagesdir}/karma/ \
--cov={envsitepackagesdir}/markov/ \
@ -94,6 +40,30 @@ commands =
--cov={envsitepackagesdir}/races/ \
--cov={envsitepackagesdir}/seen/ \
--cov={envsitepackagesdir}/storycraft/ \
--cov={envsitepackagesdir}/text_manip/ \
--cov={envsitepackagesdir}/transform/ \
--cov={envsitepackagesdir}/weather/
[testenv:py311]
# run pytest with coverage
commands =
pytest --cov-append --cov-branch \
--cov={envsitepackagesdir}/acro/ \
--cov={envsitepackagesdir}/countdown/ \
--cov={envsitepackagesdir}/dice/ \
--cov={envsitepackagesdir}/dispatch/ \
--cov={envsitepackagesdir}/dr_botzo/ \
--cov={envsitepackagesdir}/facts/ \
--cov={envsitepackagesdir}/history/ \
--cov={envsitepackagesdir}/ircbot/ \
--cov={envsitepackagesdir}/karma/ \
--cov={envsitepackagesdir}/markov/ \
--cov={envsitepackagesdir}/mpdbot/ \
--cov={envsitepackagesdir}/pi/ \
--cov={envsitepackagesdir}/races/ \
--cov={envsitepackagesdir}/seen/ \
--cov={envsitepackagesdir}/storycraft/ \
--cov={envsitepackagesdir}/text_manip/ \
--cov={envsitepackagesdir}/transform/ \
--cov={envsitepackagesdir}/weather/
@ -116,7 +86,7 @@ commands =
{envsitepackagesdir}/dispatch/ \
{envsitepackagesdir}/dr_botzo/ \
{envsitepackagesdir}/facts/ \
{envsitepackagesdir}/gitlab_bot/ \
{envsitepackagesdir}/history/ \
{envsitepackagesdir}/ircbot/ \
{envsitepackagesdir}/karma/ \
{envsitepackagesdir}/markov/ \
@ -125,6 +95,7 @@ commands =
{envsitepackagesdir}/races/ \
{envsitepackagesdir}/seen/ \
{envsitepackagesdir}/storycraft/ \
{envsitepackagesdir}/text_manip/ \
{envsitepackagesdir}/transform/ \
{envsitepackagesdir}/weather/ \
-r
@ -136,12 +107,6 @@ commands =
flake8
- flake8 --disable-noqa --ignore= --select=E,W,F,C,D,A,G,B,I,T,M,DUO
[testenv:bundle]
# take extra actions (build sdist, sphinx, whatever) to completely package the app
commands =
cp -r {distdir} .
python setup.py sdist
[coverage:paths]
source =
./
@ -160,7 +125,7 @@ include =
{envsitepackagesdir}/dispatch/
{envsitepackagesdir}/dr_botzo/
{envsitepackagesdir}/facts/
{envsitepackagesdir}/gitlab_bot/
{envsitepackagesdir}/history/
{envsitepackagesdir}/ircbot/
{envsitepackagesdir}/karma/
{envsitepackagesdir}/markov/
@ -169,31 +134,9 @@ include =
{envsitepackagesdir}/races/
{envsitepackagesdir}/seen/
{envsitepackagesdir}/storycraft/
{envsitepackagesdir}/text_manip/
{envsitepackagesdir}/transform/
{envsitepackagesdir}/weather/
omit =
**/_version.py
[flake8]
enable-extensions = G,M
exclude =
.tox/
versioneer.py
_version.py
**/migrations/
extend-ignore = T101
max-complexity = 10
max-line-length = 120
[isort]
line_length = 120
[pytest]
python_files =
*_tests.py
tests.py
test_*.py
log_level=DEBUG
DJANGO_SETTINGS_MODULE = dr_botzo.settings
django_find_project = false

File diff suppressed because it is too large Load Diff