Compare commits

...

125 Commits

Author SHA1 Message Date
Brian S. Stephan 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
Brian S. Stephan 333424025b support markov targets with identical names on different servers
markov targets are queried and autogenerated based on chatter, but had a
legacy name which is no longer in use for this, preferring the foreign
keys to channel and consequently server. the name is really just
informative these days, but was still being used to find targets, and
thus was breaking when two servers had the same channel name in them.
this fixes that
2023-05-04 17:24:07 -05:00
Brian S. Stephan 98abab560e
flake8 cleanups 2023-03-27 16:14:11 -05:00
Brian S. Stephan 86e55cb812
ignore migrations in the flake8 checking 2023-03-27 16:13:52 -05:00
Brian S. Stephan 420a7b1472
add the ability to disable the web display of some apps 2023-03-27 16:05:42 -05:00
Brian S. Stephan a214d8acfd
remove unused weather underground key 2023-03-27 14:26:51 -05:00
Brian S. Stephan 93c522037f
use r-string for the regex path 2023-03-27 14:20:07 -05:00
Brian S. Stephan b7976c46d1 fix the loading of the karma UI 2023-03-07 15:04:35 -06:00
Brian S. Stephan d962b275ff
remove the gitlab bot, it's its own project now 2023-03-02 08:05:42 -06:00
Brian S. Stephan f898f35ce6
replace execute_delayed with reactor.scheduler.execute_after
the former was deprecated forever ago, and apparently removed. this may
fix the disconnect detection logic
2023-03-02 00:51:22 -06:00
Brian S. Stephan 4289f95800
report on the version of dr.botzo in CTCP VERSION 2023-03-02 00:45:55 -06:00
Brian S. Stephan 572ecddceb
do some small cleanups 2023-03-02 00:45:29 -06:00
Brian S. Stephan 3aadde4b71
remove XMLRPC inheritence that overrode a method no longer in existence
this is probably from python 2 days; we inherited from
SimpleXMLRPCRequestHandler to change the logging, but the method
overrode no longer exists so this did nothing
2023-03-02 00:20:25 -06:00
Brian S. Stephan c2d26f404e
deduplicate Channel object from irc library
I think this is an extremely ancient copy and paste job I never fully
corrected
2023-03-02 00:19:27 -06:00
Brian S. Stephan ecaabbce89
unpin the irc library 2023-03-02 00:16:32 -06:00
Brian S. Stephan 051e656a82
fix errant reference to IrcChannel object rather than just the name 2023-03-02 00:15:06 -06:00
Brian S. Stephan 0ea54a5ee2
require authentication to get dispatch objects via API 2023-02-28 18:37:05 -06:00
Brian S. Stephan ffcdc3f8d8
rename dispatcher action type to action_type 2023-02-28 18:31:53 -06:00
Brian S. Stephan cff1a183cf
fix dispatcher API URLs to allow key-by-name 2023-02-28 18:19:46 -06:00
Brian S. Stephan 68f7c80b7e put the security middleware as the first middleware
I don't think I've ever gotten a solid idea that this is *necessary*,
but I've seen other docs refer to/assume this, so sure?
2023-02-20 10:30:13 -06:00
Brian S. Stephan 7baa70d8f6 customize the list view in the django admin 2023-02-20 08:59:54 -06:00
Brian S. Stephan 39290fb63c
allow : and , after @bot mentions 2023-02-19 22:55:14 -06:00
Brian S. Stephan 55d856b8fd
account for the discord bridge in the core bot addressed flag 2023-02-19 21:12:01 -06:00
Brian S. Stephan 88ea0dbbb4
test that <somebody> is only stripped when from the bridge user itself 2023-02-19 21:00:12 -06:00
Brian S. Stephan cfeddfdc4e
markov state queries need the context to be unique 2023-02-19 19:36:16 -06:00
Brian S. Stephan d516c1b08e
also clean up mentions that weren't cleaned because of the bridge nick 2023-02-19 18:39:36 -06:00
Brian S. Stephan 667a85aa46
add a basic learn/retrieve test since I broke it a while back 2023-02-19 18:27:05 -06:00
Brian S. Stephan 0227b74eee
when creating a markov target, tie it to ircbot models 2023-02-19 18:26:00 -06:00
Brian S. Stephan 76a052e091
genericize chain remover to use it for bridge and addressed chains 2023-02-19 18:26:00 -06:00
Brian S. Stephan 19cd23879f
management command to remove nicks from chains due to bridge 2023-02-19 18:26:00 -06:00
Brian S. Stephan f59dc35b25
test the combination of bridge and addressing learning 2023-02-19 18:26:00 -06:00
Brian S. Stephan debf086b8d
test the ability to not learn our nick when addressed 2023-02-19 18:26:00 -06:00
Brian S. Stephan ec1767e38b
remove the speaker from messages coming over the bridge when learning 2023-02-19 18:26:00 -06:00
Brian S. Stephan 0bfe3f9549
variable tweak to match other plugins (nick -> who) 2023-02-19 18:26:00 -06:00
Brian S. Stephan 363ec49097
add test to confirm markov irc plugin behavior 2023-02-19 18:25:57 -06:00
Brian S. Stephan 8549c2ef8a
set pytest settings to aid testing 2023-02-19 17:55:42 -06:00
Brian S. Stephan d24f74e53f
don't build trimmed_what until we know not to ignore chatter 2023-02-19 17:55:41 -06:00
Brian S. Stephan f812857d75
add discord bridge field to the channel model
will be used in a future change to clean up markov chains
2023-02-19 17:55:41 -06:00
Brian S. Stephan 3d0be3c25a
linter fixes for markov library methods 2023-02-19 17:55:38 -06:00
Brian S. Stephan 8b3caabb57
linting fixes on the markov models 2023-02-16 16:17:49 -06:00
Brian S. Stephan 1cf0364268
move templates aroudn to satisfy packaging now that it's being tested 2023-02-16 16:14:06 -06:00
Brian S. Stephan 337e4db650
update urls.pyes to use path() and add some tests 2023-02-16 00:04:25 -06:00
Brian S. Stephan 95396802de
add safety dependency checking 2023-02-15 20:09:26 -06:00
Brian S. Stephan 4bcf06d5e7
version bumps, with some hopefully temporary pins
* django pinned to <4 since there are compatibility things to fix first
* tox pinned to <4 because of issues where it's incompatbile with safety

(safety itself coming in a moment)
2023-02-15 19:51:46 -06:00
Brian S. Stephan 54bf00b167
remove references from deleted twitter module 2023-02-15 19:49:27 -06:00
Brian S. Stephan 133c1df638
add python 3.9, 3.10 to support via tox 2023-02-15 18:17:32 -06:00
Brian S. Stephan 7c44becaa0
drop unsupported python 3.6, 3.7 2023-02-15 18:00:21 -06:00
Brian S. Stephan 76bcdd5fef
config flag for having !dice/!random prefix the nick
with the discord-irc bridge in play, the bot was sending all !roll
results to the bridge user, which wasn't all that useful
2022-07-14 23:00:05 -05:00
Brian S. Stephan 1f5dc50d89 remove dead auth_login login URL variable 2021-12-13 15:08:24 -06:00
Brian S. Stephan bc7108cfb7 properly calculate relative offsets via parseDT 2021-12-01 07:51:58 -06:00
Brian S. Stephan dfdd6d6dc5 remove registration stuff from templates 2021-10-27 07:50:57 -05:00
Brian S. Stephan 75a1b8d7f3 version bump (new Django!) and remove twitter lib 2021-10-27 07:50:13 -05:00
Brian S. Stephan fd5c4dad1a remove unused import 2021-10-26 23:10:45 -05:00
Brian S. Stephan 7264deee2a remove the long unsupported and unused twitter module 2021-10-26 22:58:24 -05:00
Brian S. Stephan 76e5546bcb remove django registration stuff, unused/desired 2021-10-26 22:25:08 -05:00
Brian S. Stephan 739d3fa2b7 new coat of paint on facts: !f shortcut, natural language fact adding
note that for now anyone can add a fact, going to assume responsible
usage on the part of users on my networks
2021-10-14 08:17:54 -05:00
Brian S. Stephan 6ab86f773c irc plugin to turn text into zalgo text 2021-05-17 10:07:34 -05:00
Brian S. Stephan 3d5e6754e8 add zalgo-text and bump pip-tools 2021-05-17 09:46:43 -05:00
Brian S. Stephan ab1d3456ad only show PRIVMSGed if we had something to PRIVMSG 2021-05-06 09:36:28 -05:00
Brian S. Stephan a550bf07ac discard from set to ignore KeyError 2021-05-06 09:36:28 -05:00
Brian S. Stephan 3fe0a8e59e initialize state stuff in __init__
this shouldn't practically matter but whatever
2021-05-06 09:36:23 -05:00
Brian S. Stephan 0ccb49e7ed make catchup regex insensitive, use alt wording 2021-05-06 09:27:41 -05:00
Brian S. Stephan 651399f5fc delete history pointer after reporting 2021-05-05 08:44:40 -05:00
Brian S. Stephan 0b3386e183 display channel count in the privmsg report 2021-05-05 08:42:23 -05:00
Brian S. Stephan b21421a395 give feedback on lines missed regardless of pub/privmsg 2021-05-05 08:36:32 -05:00
Brian S. Stephan 9368c8b823 also report on where history was said
kind of an obvious thing to do which I naturally missed
2021-05-05 08:13:04 -05:00
Brian S. Stephan def5964658 track, display timestamp of history 2021-05-05 08:08:47 -05:00
Brian S. Stephan e43807fb27 initial (and working?) version of history tracking plugin 2021-05-05 08:03:56 -05:00
Brian S. Stephan b3b8f832a2 remove unused import 2021-05-04 19:30:11 -05:00
Brian S. Stephan b971b72af8 put current weather and forecast on separate lines
the long location string means this is often bleeding into two lines at
the very end anyway, so this just looks nicer
2021-04-26 11:52:43 -05:00
Brian S. Stephan 40286eeafc replace IRC color codes with nothing 2021-04-25 23:19:18 -05:00
Brian S. Stephan d888c5f03b add Pi simulation values to serializer 2021-04-25 21:08:38 -05:00
Brian S. Stephan d7b7bdf73d add another word match for countdown text triggers 2021-04-25 21:05:09 -05:00
Brian S. Stephan a03c69258f use per-server IrcChannel for reminders
this makes it so that if we have multiple bot instances running, they
will only pay attention to the countdown items for their current server
2021-04-25 21:04:11 -05:00
Brian S. Stephan 43f2b09057 don't add the empty string to additional nicks
thinko on my part, this was making the regex for matching all nicks to
'|nick' when the field is '', because of split producing ['']. in
particular this was making markov trigger on every line
2021-04-25 21:00:34 -05:00
Brian S. Stephan 3aa3fb14e4 add RPC API to retrieve fact(s) 2021-04-25 18:48:34 -05:00
Brian S. Stephan 1fc8af09f8 use nearest area to produce return location 2021-04-25 16:34:01 -05:00
Brian S. Stephan 53c874dc21 option to replace IRC control chars with markdown
^C^B isn't allowed through Discord's API, and I'm sure some other stuff
like colors that I don't use. this makes it a server option to replace
them with Markdown, though I think this would only ever be interesting
for BitlBee + Discord
2021-04-25 12:11:59 -05:00
Brian S. Stephan 1036c08147 only autojoin channels for this connection 2021-04-25 11:38:19 -05:00
Brian S. Stephan 9c1109107b relate channels to their server
this is necessary for supporting multiple irc servers in one bot config.
this also has the side effect of requiring some code in ircbot and
markov which autocreates channels to also include the server (retrieved
via the connection). this will again help keep channels coherent for
multi-server arrangements

the twitter bot change here is untested but seems like the right idea (I
haven't used the twitter package in forever)
2021-04-25 11:13:10 -05:00
Brian S. Stephan 6136127c5f move IRC server settings to database
this is the first step in trying to get the bot to support multiple
servers with different channels, countdown triggers, and so on

this also ends up affecting some configuration around:
* dispatch
* markov
* admin privmsg form
2021-04-25 10:17:41 -05:00
Brian S. Stephan 44d8b7db00 lint cleanups 2021-04-24 20:49:19 -05:00
Brian S. Stephan d518cb2b77 lint cleanups 2021-04-24 20:49:14 -05:00
Brian S. Stephan cbbf6eb311 Merge branch 'backend-frameworkification' of bss/dr.botzo into master
Merge the (aborted) backend/Discord attempt. See PR #2
2021-04-24 15:36:22 -05:00
Brian S. Stephan 2f4156ce26 quote wttr.in requests 2021-04-24 13:00:18 -05:00
Brian S. Stephan e64af1a0a1 report on karma keys 2021-04-24 13:00:18 -05:00
Brian S. Stephan ca8798453c fix Dice plugin init 2021-04-24 13:00:18 -05:00
Brian S. Stephan dc5f243608 optionally provide name of new countdown items 2021-04-24 13:00:18 -05:00
Brian S. Stephan ab0d738851 for migrated pi history, set their x,y to -1.0 to be more obvious 2020-10-25 12:28:08 -05:00
Brian S. Stephan 9d94155f66 implement basic API GETs for countdown items 2020-10-25 12:20:39 -05:00
Brian S. Stephan a6f8fc5dc1 make more countdown item fields optional in admin 2020-10-25 12:16:33 -05:00
Brian S. Stephan bcc5f767ba little cleanup TODO in PiLog simulate() 2020-10-25 11:19:20 -05:00
Brian S. Stephan a0a1aa10f4 wow, unit tests! pi is covered (except for the irc plugin)
I think I'm going to move the irc stuff into a separate project in the
future so I'm not worried about testing that one yet
2020-10-25 11:16:48 -05:00
Brian S. Stephan d5e5343193 have pi simulate via API return a 201 2020-10-25 11:16:23 -05:00
Brian S. Stephan da815a1fc3 provide DRF action to run a pi simulation 2020-10-24 23:58:45 -05:00
Brian S. Stephan ef08cec0fb fix field reference in the PiLog.hit property 2020-10-24 23:58:02 -05:00
Brian S. Stephan 691ee7696b serve basic list/detail pi log simulation views 2020-10-24 11:58:43 -05:00
Brian S. Stephan 665f56a430 avoid divide by 0 in getting pi simulation value 2020-10-24 11:56:48 -05:00
Brian S. Stephan 4d94322c55 refactor PiLog to retain x,y values 2020-10-24 11:47:36 -05:00
Brian S. Stephan 10d73f570a start using tox, despite 100000 errors 2020-10-24 10:06:41 -05:00
Brian S. Stephan 819bbe74c6 recompile requirements
basically everything changed here, so... fingers crossed
2020-10-24 10:06:37 -05:00
Brian S. Stephan 1833430c5d put requirements in my now-usual spot 2020-10-24 09:54:58 -05:00
Brian S. Stephan 42a1efed79 Merge branch 'backend-frameworkification' of bss/dr.botzo into master 2019-10-11 09:00:37 -05:00
Brian S. Stephan c670072c86 add versioneer stuff to the project 2019-10-11 08:37:37 -05:00
Brian S. Stephan d5e1a2ed45 actually use an f-string when querying wttr.in 2019-10-06 10:44:21 -05:00
Brian S. Stephan 5f6e255ded fix some line length violations in weather/ircplugin.py 2019-10-06 09:34:01 -05:00
Brian S. Stephan c0c0306419 expose weather report as an rpc view 2019-10-06 09:33:46 -05:00
Brian S. Stephan 56d0e26c6d reimplement !weather in the IRC bot 2019-10-06 09:13:51 -05:00
Brian S. Stephan b42d0ac0e9 pin irc==15.0.6 in requirements 2019-10-05 11:34:28 -05:00
Brian S. Stephan 31758b80b6 weather: start rewriting weather plugin to use wttr.in 2019-10-05 10:36:28 -05:00
Brian S. Stephan 802072caed add markov RPC method for learning a line 2019-09-19 00:21:18 -05:00
Brian S. Stephan 9e4bc595a4 add markov RPC method for generating a line from a context 2019-09-19 00:12:36 -05:00
Brian S. Stephan d34fb18949 rename 'home' view as 'index', fixes DEBUG page behavior 2019-06-29 09:41:28 -05:00
Brian S. Stephan abce0262f3 provide dice_str in exceptions, where applicable 2019-06-29 09:24:16 -05:00
Brian S. Stephan b917f78ca5 replace dice sanity checks as asserts 2019-06-29 09:23:35 -05:00
Brian S. Stephan c2aa3df13f fix some '!= None' checks, rewrite as 'is not None' 2019-06-29 09:23:02 -05:00
Brian S. Stephan 6b8dd1a645 dos2unix dice/* 2019-06-22 11:45:09 -05:00
Brian S. Stephan 649a148209 don't assert on trials if they aren't provided 2019-06-22 11:43:30 -05:00
Brian S. Stephan 8fcc8365e3 add a dice rolling API view to the django app 2019-06-21 18:07:10 -05:00
Brian S. Stephan f78d407d4c move DiceRoller to its own module
while I'm doing that, standardize the usage of raising exceptions when parsing goes wrong
2019-06-21 16:53:40 -05:00
Brian S. Stephan 8528152483 remove dice cthulhutech roll. hasn't been used in forever 2019-06-21 16:51:09 -05:00
Brian S. Stephan f2fb0a26a4 remove unnecessary unicode_literal future imports, we py3 now 2019-06-21 15:23:33 -05:00
Brian S. Stephan 0f88715ffd remove unnecessary requirements-server.* 2019-06-21 10:06:17 -05:00
Brian S. Stephan 2f98a64cdd version bumps and migration to django 2.2 2019-06-21 10:05:40 -05:00
Brian S. Stephan 0589939137 support multiple strings as counting as nick highlights
also, treat @nicks as being addressed, since we are doing discord
through bitlbee now
2019-01-10 08:48:15 -06:00
167 changed files with 5284 additions and 2721 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
dr_botzo/_version.py export-subst

26
.gitignore vendored
View File

@ -1,14 +1,10 @@
*.facts
*.json
*.log
*.pyc
*.sqlite3
*.swo
*.swp
*.urls
*~
.idea
tags
.idea/
build/
dist/
tags/
*.egg-info/
.tox/
.coverage
dr.botzo.data
dr.botzo.cfg
localsettings.py
@ -18,3 +14,11 @@ parsetab.py
megahal.*
dr.botzo.log
dr.botzo.markov
*.facts
*.log
*.pyc
*.sqlite3
*.swo
*.swp
*.urls
*~

View File

@ -1,20 +0,0 @@
doc-warnings: true
strictness: high
ignore-paths:
- migrations
ignore-patterns:
- \.log$
- localsettings.py$
- parsetab.py$
pylint:
enable:
- relative-import
options:
max-line-length: 120
good-names: log
pep8:
options:
max-line-length: 120
pep257:
disable:
- D203

8
MANIFEST.in Normal file
View File

@ -0,0 +1,8 @@
include versioneer.py
include dr_botzo/_version.py
graft dr_botzo/templates
graft facts/templates
graft ircbot/templates
graft karma/templates
graft markbot/templates
graft races/templates

View File

@ -5,4 +5,10 @@ from django.contrib import admin
from countdown.models import CountdownItem
admin.site.register(CountdownItem)
class CountdownItemAdmin(admin.ModelAdmin):
"""Custom display for the countdown items."""
list_display = ('__str__', 'reminder_message')
admin.site.register(CountdownItem, CountdownItemAdmin)

View File

@ -12,6 +12,7 @@ from django.utils import timezone
from countdown.models import CountdownItem
from ircbot.lib import Plugin, most_specific_message
from ircbot.models import IrcChannel
log = logging.getLogger('countdown.ircplugin')
@ -22,12 +23,14 @@ class Countdown(Plugin):
new_reminder_regex = (r'remind\s+(?P<who>[^\s]+)\s+(?P<when_type>at|in|on)\s+(?P<when>.*?)\s+'
r'(and\s+every\s+(?P<recurring_period>.*?)\s+)?'
r'(until\s+(?P<recurring_until>.*?)\s+)?'
r'(to|that|about)\s+(?P<text>.*)')
r'(to|that|about|with)\s+(?P<text>.*?)'
r'(?=\s+\("(?P<name>.*)"\)|$)')
def __init__(self, bot, connection, event):
"""Initialize some stuff."""
self.running_reminders = []
self.send_reminders = True
self.server_config = connection.server_config
t = threading.Thread(target=self.reminder_thread)
t.daemon = True
@ -42,7 +45,7 @@ class Countdown(Plugin):
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+list$',
self.handle_item_list, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+(\S+)$',
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!countdown\s+(.+)$',
self.handle_item_detail, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], self.new_reminder_regex,
self.handle_new_reminder, -50)
@ -65,20 +68,22 @@ class Countdown(Plugin):
# TODO: figure out if we need this sleep, which exists so we don't send reminders while still connecting to IRC
time.sleep(30)
while self.send_reminders:
reminders = CountdownItem.objects.filter(is_reminder=True, sent_reminder=False, at_time__lte=timezone.now())
reminders = CountdownItem.objects.filter(is_reminder=True, sent_reminder=False,
at_time__lte=timezone.now(),
reminder_target_new__server=self.server_config)
for reminder in reminders:
log.debug("%s @ %s", reminder.reminder_message, reminder.at_time)
if reminder.at_time <= timezone.now():
log.info("sending %s to %s", reminder.reminder_message, reminder.reminder_target)
self.bot.reply(None, reminder.reminder_message, explicit_target=reminder.reminder_target)
log.info("sending %s to %s", reminder.reminder_message, reminder.reminder_target_new.name)
self.bot.reply(None, reminder.reminder_message, explicit_target=reminder.reminder_target_new.name)
# if recurring and not hit until, set a new at time, otherwise stop reminding
if reminder.recurring_until is not None and timezone.now() >= reminder.recurring_until:
reminder.sent_reminder = True
elif reminder.recurring_period != '':
calendar = pdt.Calendar()
when_t = calendar.parseDT(reminder.recurring_period, reminder.at_time,
when_t = calendar.parseDT(f'in one {reminder.recurring_period}', reminder.at_time,
tzinfo=reminder.at_time.tzinfo)[0]
if reminder.recurring_until is None or when_t <= reminder.recurring_until:
reminder.at_time = when_t
@ -99,9 +104,16 @@ class Countdown(Plugin):
recurring_period = match.group('recurring_period')
recurring_until = match.group('recurring_until')
text = match.group('text')
name = match.group('name')
log.debug("%s / %s / %s", who, when, text)
item_name = '{0:s}-{1:s}'.format(event.sender_nick, timezone.now().strftime('%s'))
if not name:
item_name = '{0:s}-{1:s}'.format(event.sender_nick, timezone.now().strftime('%s'))
else:
if CountdownItem.objects.filter(name=name).count() > 0:
self.bot.reply(event, "item with name '{0:s}' already exists".format(name))
return 'NO MORE'
item_name = name
# parse when to send the notification
if when_type == 'in':
@ -135,8 +147,12 @@ class Countdown(Plugin):
log.debug("%s / %s / %s", item_name, when_t, message)
# get the IrcChannel to send to
reminder_target, _ = IrcChannel.objects.get_or_create(name=event.sent_location,
server=connection.server_config)
countdown_item = CountdownItem.objects.create(name=item_name, at_time=when_t, is_reminder=True,
reminder_message=message, reminder_target=event.sent_location)
reminder_message=message, reminder_target_new=reminder_target)
if recurring_period:
countdown_item.recurring_period = recurring_period
if recurring_until:

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-23 02:25
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-24 01:06
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-02-24 01:12
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.2 on 2020-10-25 17:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('countdown', '0005_countdownitem_recurring_until'),
]
operations = [
migrations.AlterField(
model_name='countdownitem',
name='recurring_period',
field=models.CharField(blank=True, default='', max_length=64),
),
migrations.AlterField(
model_name='countdownitem',
name='reminder_target',
field=models.CharField(blank=True, default='', max_length=64),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.1.2 on 2021-04-26 00:10
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ircbot', '0018_ircserver_replace_irc_control_with_markdown'),
('countdown', '0006_auto_20201025_1716'),
]
operations = [
migrations.AddField(
model_name='countdownitem',
name='reminder_target_new',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='ircbot.ircchannel'),
),
]

View File

@ -1,14 +1,8 @@
"""Countdown item models."""
import logging
from django.db import models
from django.utils import timezone
log = logging.getLogger('countdown.models')
class CountdownItem(models.Model):
"""Track points in time."""
@ -19,13 +13,15 @@ class CountdownItem(models.Model):
sent_reminder = models.BooleanField(default=False)
reminder_message = models.TextField(default="")
reminder_target = models.CharField(max_length=64, default='')
reminder_target = models.CharField(max_length=64, blank=True, default='')
reminder_target_new = models.ForeignKey('ircbot.IrcChannel', null=True, blank=True,
default=None, on_delete=models.CASCADE)
recurring_period = models.CharField(max_length=64, default='')
recurring_period = models.CharField(max_length=64, blank=True, default='')
recurring_until = models.DateTimeField(null=True, blank=True, default=None)
created_time = models.DateTimeField(auto_now_add=True)
def __str__(self):
"""String representation."""
"""Summarize object."""
return "{0:s} @ {1:s}".format(self.name, timezone.localtime(self.at_time).strftime('%Y-%m-%d %H:%M:%S %Z'))

14
countdown/serializers.py Normal file
View File

@ -0,0 +1,14 @@
"""REST serializers for countdown items."""
from rest_framework import serializers
from countdown.models import CountdownItem
class CountdownItemSerializer(serializers.ModelSerializer):
"""Countdown item serializer for the REST API."""
class Meta:
"""Meta options."""
model = CountdownItem
fields = '__all__'

13
countdown/urls.py Normal file
View File

@ -0,0 +1,13 @@
"""URL patterns for the countdown views."""
from django.conf.urls import include
from django.urls import path
from rest_framework.routers import DefaultRouter
from countdown.views import CountdownItemViewSet
router = DefaultRouter()
router.register(r'items', CountdownItemViewSet)
urlpatterns = [
path('api/', include(router.urls)),
]

14
countdown/views.py Normal file
View File

@ -0,0 +1,14 @@
"""Provide an interface to countdown items."""
from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet
from countdown.models import CountdownItem
from countdown.serializers import CountdownItemSerializer
class CountdownItemViewSet(ReadOnlyModelViewSet):
"""Provide list and detail actions for countdown items."""
queryset = CountdownItem.objects.all()
serializer_class = CountdownItemSerializer
permission_classes = [IsAuthenticated]

View File

@ -1,36 +1,31 @@
"""Roll dice when asked, intended for RPGs."""
# this breaks yacc, but ply might be happy in py3
#from __future__ import unicode_literals
import logging
import math
import re
import random
import re
from django.conf import settings
from irc.client import NickMask
import ply.lex as lex
import ply.yacc as yacc
from dice.roller import DiceRoller
from ircbot.lib import Plugin
logger = logging.getLogger(__name__)
class Dice(Plugin):
"""Roll simple or complex dice strings."""
def __init__(self, bot, connection, event):
"""Set up the plugin."""
self.roller = DiceRoller()
super(Dice, self).__init__(bot, connection, event)
def start(self):
"""Set up the handlers."""
self.roller = DiceRoller()
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'^!ctech\s+(.*)$',
self.handle_ctech, -20)
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!random\s+(.*)$',
self.handle_random, -20)
@ -38,398 +33,48 @@ class Dice(Plugin):
def stop(self):
"""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_ctech)
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_random)
super(Dice, self).stop()
def handle_random(self, connection, event, match):
"""Handle the !random command which picks an item from a list."""
nick = NickMask(event.source).nick
choices = match.group(1)
choices_list = choices.split(' ')
choice = random.choice(choices_list)
choice = random.SystemRandom().choice(choices_list)
logger.debug(event.recursing)
if event.recursing:
reply = "{0:s}".format(choice)
else:
elif settings.DICE_PREFIX_ROLLER:
reply = "{0:s}: {1:s}".format(nick, choice)
else:
reply = "{0:s}".format(choice)
return self.bot.reply(event, reply)
def handle_roll(self, connection, event, match):
"""Handle the !roll command which covers most common dice stuff."""
nick = NickMask(event.source).nick
dicestr = match.group(1)
logger.debug(event.recursing)
try:
reply_str = self.roller.do_roll(dicestr)
except AssertionError as aex:
reply_str = f"Could not roll dice: {aex}"
except ValueError:
reply_str = "Unable to parse roll"
if event.recursing:
reply = "{0:s}".format(self.roller.do_roll(dicestr))
reply = "{0:s}".format(reply_str)
elif settings.DICE_PREFIX_ROLLER:
reply = "{0:s}: {1:s}".format(nick, reply_str)
else:
reply = "{0:s}: {1:s}".format(nick, self.roller.do_roll(dicestr))
reply = "{0:s}".format(reply_str)
return self.bot.reply(event, re.sub(r'(\d+)(.*?\s+)(\(.*?\))', r'\1\214\3', reply))
def handle_ctech(self, connection, event, match):
"""Handle cthulhutech dice rolls."""
nick = NickMask(event.source).nick
rollitrs = re.split(';\s*', match.group(1))
reply = ""
for count, roll in enumerate(rollitrs):
pattern = '^(\d+)d(?:(\+|\-)(\d+))?(?:\s+(.*))?'
regex = re.compile(pattern)
matches = regex.search(roll)
if matches is not None:
dice = int(matches.group(1))
modifier = 0
if matches.group(2) is not None and matches.group(3) is not None:
if str(matches.group(2)) == '-':
modifier = -1 * int(matches.group(3))
else:
modifier = int(matches.group(3))
result = roll + ': '
rolls = []
for d in range(dice):
rolls.append(random.randint(1, 10))
rolls.sort()
rolls.reverse()
# highest single die method
method1 = rolls[0]
# highest set method
method2 = 0
rolling_sum = 0
for i, r in enumerate(rolls):
# if next roll is same as current, sum and continue, else see if sum is best so far
if i+1 < len(rolls) and rolls[i+1] == r:
if rolling_sum == 0:
rolling_sum = r
rolling_sum += r
else:
if rolling_sum > method2:
method2 = rolling_sum
rolling_sum = 0
# check for set in progress (e.g. lots of 1s)
if rolling_sum > method2:
method2 = rolling_sum
# straight method
method3 = 0
rolling_sum = 0
count = 0
for i, r in enumerate(rolls):
# if next roll is one less as current, sum and continue, else check len and see if sum is best so far
if i+1 < len(rolls) and rolls[i+1] == r-1:
if rolling_sum == 0:
rolling_sum = r
count += 1
rolling_sum += r-1
count += 1
else:
if count >= 3 and rolling_sum > method3:
method3 = rolling_sum
rolling_sum = 0
# check for straight in progress (e.g. straight ending in 1)
if count >= 3 and rolling_sum > method3:
method3 = rolling_sum
# get best roll
best = max([method1, method2, method3])
# check for critical failure
botch = False
ones = 0
for r in rolls:
if r == 1:
ones += 1
if ones >= math.ceil(float(len(rolls))/2):
botch = True
if botch:
result += 'BOTCH'
else:
result += str(best + modifier)
rollres = ''
for i,r in enumerate(rolls):
rollres += str(r)
if i is not len(rolls)-1:
rollres += ','
result += ' [' + rollres
if modifier != 0:
if modifier > 0:
result += ' +' + str(modifier)
else:
result += ' -' + str(modifier * -1)
result += ']'
reply += result
if count is not len(rollitrs)-1:
reply += "; "
if reply is not "":
msg = "{0:s}: {1:s}".format(nick, reply)
return self.bot.reply(event, msg)
class DiceRoller(object):
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
literals = ['#', '/', '+', '-', 'd']
t_TEXT = r'\s+[^;]+'
t_ROLLSEP = r';\s*'
def build(self):
lex.lex(module=self)
yacc.yacc(module=self)
def t_NUMBER(self, t):
r'\d+'
t.value = int(t.value)
return t
def t_error(self, t):
t.lexer.skip(1)
precedence = (
('left', 'ROLLSEP'),
('left', '+', '-'),
('right', 'd'),
('left', '#'),
('left', '/')
)
output = ""
def roll_dice(self, keep, dice, size):
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
the actual roll. Returns a string representing the result.
"""
a = list(range(dice))
for i in range(dice):
a[i] = random.randint(1, size)
if keep != dice:
b = sorted(a, reverse=True)
b = b[0:keep]
else:
b = a
total = sum(b)
outstr = "[" + ",".join(str(i) for i in a) + "]"
return (total, outstr)
def process_roll(self, trials, mods, comment):
"""Processes rolls coming from the parser.
This generates the inputs for the roll_dice() command, and returns
the full string representing the whole current dice string (the part
up to a semicolon or end of line).
"""
output = ""
repeat = 1
if trials != None:
repeat = trials
for i in range(repeat):
mode = 1
total = 0
curr_str = ""
if i > 0:
output += ", "
for m in mods:
keep = 0
dice = 1
res = 0
# if m is a tuple, then it is a die roll
# m[0] = (keep, num dice)
# m[1] = num faces on the die
if type(m) == tuple:
if m[0] != None:
if m[0][0] != None:
keep = m[0][0]
dice = m[0][1]
size = m[1]
if keep > dice or keep == 0:
keep = dice
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]
elif m == "+":
mode = 1
curr_str += "+"
elif m == "-":
mode = -1
curr_str += "-"
else:
res = m
curr_str += str(m)
total += mode * res
if repeat == 1:
if comment != None:
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
else:
output = "%d (%s)" % (total, curr_str)
else:
output += "%d (%s)" % (total, curr_str)
if i == repeat - 1:
if comment != None:
output += " (%s)" % (comment.strip())
return output
def p_roll_r(self, p):
# Chain rolls together.
# General idea I had when creating this grammar: A roll string is a chain
# of modifiers, which may be repeated for a certain number of trials. It can
# have a comment that describes the roll
# Multiple roll strings can be chained with semicolon
'roll : roll ROLLSEP roll'
global output
p[0] = p[1] + "; " + p[3]
output = p[0]
def p_roll(self, p):
# Parse a basic roll string.
'roll : trial modifier comment'
global output
mods = []
if type(p[2]) == list:
mods = p[2]
else:
mods = [p[2]]
p[0] = self.process_roll(p[1], mods, p[3])
output = p[0]
def p_roll_no_trials(self, p):
# Parse a roll string without trials.
'roll : modifier comment'
global output
mods = []
if type(p[1]) == list:
mods = p[1]
else:
mods = [p[1]]
p[0] = self.process_roll(None, mods, p[2])
output = p[0]
def p_comment(self, p):
# Parse a comment.
'''comment : TEXT
|'''
if len(p) == 2:
p[0] = p[1]
else:
p[0] = None
def p_modifier(self, p):
# Parse a modifier on a roll string.
'''modifier : modifier "+" modifier
| modifier "-" modifier'''
# Use append to prevent nested lists (makes dealing with this easier)
if type(p[1]) == list:
p[1].append(p[2])
p[1].append(p[3])
p[0] = p[1]
elif type(p[3]) == list:
p[3].insert(0, p[2])
p[3].insert(0, p[1])
p[0] = p[3]
else:
p[0] = [p[1], p[2], p[3]]
def p_die(self, p):
# Return the left side before the "d", and the number of faces.
'modifier : left NUMBER'
p[0] = (p[1], p[2])
def p_die_num(self, p):
'modifier : NUMBER'
p[0] = p[1]
def p_left(self, p):
# Parse the number of dice we are rolling, and how many we are keeping.
'left : keep dice'
if p[1] == None:
p[0] = [None, p[2]]
else:
p[0] = [p[1], p[2]]
def p_left_all(self, p):
'left : dice'
p[0] = [None, p[1]]
def p_left_e(self, p):
'left :'
p[0] = None
def p_total(self, p):
'trial : NUMBER "#"'
if len(p) > 1:
p[0] = p[1]
else:
p[0] = None
def p_keep(self, p):
'keep : NUMBER "/"'
if p[1] != None:
p[0] = p[1]
else:
p[0] = None
def p_dice(self, p):
'dice : NUMBER "d"'
p[0] = p[1]
def p_dice_one(self, p):
'dice : "d"'
p[0] = 1
def p_error(self, p):
# Provide the user with something (albeit not much) when the roll can't be parsed.
global output
output = "Unable to parse roll"
def get_result(self):
global output
return output
def do_roll(self, dicestr):
"""
Roll some dice and get the result (with broken out rolls).
Keyword arguments:
dicestr - format:
N#X/YdS+M label
N#: do the following roll N times (optional)
X/: take the top X rolls of the Y times rolled (optional)
Y : roll the die specified Y times (optional, defaults to 1)
dS: roll a S-sided die
+M: add M to the result (-M for subtraction) (optional)
"""
self.build()
yacc.parse(dicestr)
return self.get_result()
plugin = Dice

260
dice/roller.py Normal file
View File

@ -0,0 +1,260 @@
"""Dice rollers used by the views, bots, etc."""
import logging
import random
import ply.lex as lex
import ply.yacc as yacc
logger = logging.getLogger(__name__)
class DiceRoller(object):
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
literals = ['#', '/', '+', '-', 'd']
t_TEXT = r'\s+[^;]+'
t_ROLLSEP = r';\s*'
def build(self):
lex.lex(module=self)
yacc.yacc(module=self)
def t_NUMBER(self, t):
r'\d+'
t.value = int(t.value)
return t
def t_error(self, t):
t.lexer.skip(1)
precedence = (
('left', 'ROLLSEP'),
('left', '+', '-'),
('right', 'd'),
('left', '#'),
('left', '/')
)
output = ""
def roll_dice(self, keep, dice, size):
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
the actual roll. Returns a string representing the result.
"""
a = list(range(dice))
for i in range(dice):
a[i] = random.randint(1, size)
if keep != dice:
b = sorted(a, reverse=True)
b = b[0:keep]
else:
b = a
total = sum(b)
outstr = "[" + ",".join(str(i) for i in a) + "]"
return (total, outstr)
def process_roll(self, trials, mods, comment):
"""Processes rolls coming from the parser.
This generates the inputs for the roll_dice() command, and returns
the full string representing the whole current dice string (the part
up to a semicolon or end of line).
"""
rolls = mods[0][0][1] if mods[0][0][1] else 1
if trials:
assert trials <= 10, "Too many rolls (max: 10)."
assert rolls <= 50, "Too many dice (max: 50) in roll."
output = ""
repeat = 1
if trials is not None:
repeat = trials
for i in range(repeat):
mode = 1
total = 0
curr_str = ""
if i > 0:
output += ", "
for m in mods:
keep = 0
dice = 1
res = 0
# if m is a tuple, then it is a die roll
# m[0] = (keep, num dice)
# m[1] = num faces on the die
if type(m) == tuple:
if m[0] is not None:
if m[0][0] is not None:
keep = m[0][0]
dice = m[0][1]
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."
res = self.roll_dice(keep, dice, size)
curr_str += "%d%s" % (res[0], res[1])
res = res[0]
elif m == "+":
mode = 1
curr_str += "+"
elif m == "-":
mode = -1
curr_str += "-"
else:
res = m
curr_str += str(m)
total += mode * res
if repeat == 1:
if comment is not None:
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
else:
output = "%d (%s)" % (total, curr_str)
else:
output += "%d (%s)" % (total, curr_str)
if i == repeat - 1:
if comment is not None:
output += " (%s)" % (comment.strip())
return output
def p_roll_r(self, p):
# Chain rolls together.
# General idea I had when creating this grammar: A roll string is a chain
# of modifiers, which may be repeated for a certain number of trials. It can
# have a comment that describes the roll
# Multiple roll strings can be chained with semicolon
'roll : roll ROLLSEP roll'
global output
p[0] = p[1] + "; " + p[3]
output = p[0]
def p_roll(self, p):
# Parse a basic roll string.
'roll : trial modifier comment'
global output
mods = []
if type(p[2]) == list:
mods = p[2]
else:
mods = [p[2]]
p[0] = self.process_roll(p[1], mods, p[3])
output = p[0]
def p_roll_no_trials(self, p):
# Parse a roll string without trials.
'roll : modifier comment'
global output
mods = []
if type(p[1]) == list:
mods = p[1]
else:
mods = [p[1]]
p[0] = self.process_roll(None, mods, p[2])
output = p[0]
def p_comment(self, p):
# Parse a comment.
'''comment : TEXT
|'''
if len(p) == 2:
p[0] = p[1]
else:
p[0] = None
def p_modifier(self, p):
# Parse a modifier on a roll string.
'''modifier : modifier "+" modifier
| modifier "-" modifier'''
# Use append to prevent nested lists (makes dealing with this easier)
if type(p[1]) == list:
p[1].append(p[2])
p[1].append(p[3])
p[0] = p[1]
elif type(p[3]) == list:
p[3].insert(0, p[2])
p[3].insert(0, p[1])
p[0] = p[3]
else:
p[0] = [p[1], p[2],