Compare commits

...

22 Commits

Author SHA1 Message Date
Brian S. Stephan 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
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
40 changed files with 286 additions and 706 deletions

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>)

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

@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2023-03-01 00:26
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('dispatch', '0006_xmlrpc_settings'),
]
operations = [
migrations.RenameField(
model_name='dispatcheraction',
old_name='type',
new_name='action_type',
),
]

View File

@ -38,10 +38,10 @@ class DispatcherAction(models.Model):
)
dispatcher = models.ForeignKey('Dispatcher', related_name='actions', on_delete=models.CASCADE)
type = models.CharField(max_length=16, choices=TYPE_CHOICES)
action_type = models.CharField(max_length=16, choices=TYPE_CHOICES)
destination = models.CharField(max_length=200)
include_key = models.BooleanField(default=False)
def __str__(self):
"""Provide string representation."""
return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.type, self.destination)
return "{0:s} -> {1:s} {2:s}".format(self.dispatcher.key, self.action_type, self.destination)

View File

@ -12,7 +12,7 @@ class DispatcherActionSerializer(serializers.ModelSerializer):
"""Meta options."""
model = DispatcherAction
fields = ('id', 'dispatcher', 'type', 'destination')
fields = ('id', 'dispatcher', 'action_type', 'destination')
class DispatcherSerializer(serializers.ModelSerializer):

View File

@ -6,11 +6,11 @@ from dispatch.views import (DispatcherActionDetail, DispatcherActionList, Dispat
urlpatterns = [
path('api/dispatchers/', DispatcherList.as_view(), name='dispatch_api_dispatchers'),
path('api/dispatchers/<pk>/', DispatcherDetail.as_view(), name='dispatch_api_dispatcher_detail'),
path('api/dispatchers/<pk>/message', DispatchMessage.as_view(), name='dispatch_api_dispatch_message'),
path('api/dispatchers/<int:pk>/', DispatcherDetail.as_view(), name='dispatch_api_dispatcher_detail'),
path('api/dispatchers/<int:pk>/message', DispatchMessage.as_view(), name='dispatch_api_dispatch_message'),
path('api/dispatchers/<key>/', DispatcherDetailByKey.as_view(), name='dispatch_api_dispatcher_detail'),
path('api/dispatchers/<key>/message', DispatchMessageByKey.as_view(), name='dispatch_api_dispatch_message'),
path('api/actions/', DispatcherActionList.as_view(), name='dispatch_api_actions'),
path('api/actions/<pk>/', DispatcherActionDetail.as_view(), name='dispatch_api_action_detail'),
path('api/actions/<int:pk>/', DispatcherActionDetail.as_view(), name='dispatch_api_action_detail'),
]

View File

@ -28,6 +28,8 @@ class HasSendMessagePermission(IsAuthenticated):
class DispatcherList(generics.ListAPIView):
"""List all dispatchers."""
permission_classes = (IsAuthenticated,)
queryset = Dispatcher.objects.all()
serializer_class = DispatcherSerializer
@ -35,6 +37,8 @@ class DispatcherList(generics.ListAPIView):
class DispatcherDetail(generics.RetrieveAPIView):
"""Detail the given dispatcher."""
permission_classes = (IsAuthenticated,)
queryset = Dispatcher.objects.all()
serializer_class = DispatcherSerializer
@ -71,19 +75,19 @@ class DispatchMessage(generics.GenericAPIView):
else:
text = message.data['message']
if action.type == DispatcherAction.PRIVMSG_TYPE:
if action.action_type == DispatcherAction.PRIVMSG_TYPE:
# connect over XML-RPC and send
try:
bot_url = 'http://{0:s}:{1:d}/'.format(dispatcher.bot_xmlrpc_host, dispatcher.bot_xmlrpc_port)
bot = xmlrpc.client.ServerProxy(bot_url, allow_none=True)
log.debug("sending '%s' to channel %s", text, action.destination)
bot.reply(None, text, False, action.destination)
except Exception as e:
except xmlrpc.client.Fault as xmlex:
new_data = copy.deepcopy(message.data)
new_data['status'] = "FAILED - {0:s}".format(str(e))
new_data['status'] = "FAILED - {0:s}".format(str(xmlex))
new_message = self.serializer_class(data=new_data)
return Response(new_message.initial_data)
elif action.type == DispatcherAction.FILE_TYPE:
elif action.action_type == DispatcherAction.FILE_TYPE:
# write to file
filename = os.path.abspath(action.destination)
log.debug("sending '%s' to file %s", text, filename)
@ -107,6 +111,8 @@ class DispatchMessageByKey(DispatchMessage):
class DispatcherActionList(generics.ListAPIView):
"""List all dispatchers."""
permission_classes = (IsAuthenticated,)
queryset = DispatcherAction.objects.all()
serializer_class = DispatcherActionSerializer
@ -114,5 +120,7 @@ class DispatcherActionList(generics.ListAPIView):
class DispatcherActionDetail(generics.RetrieveAPIView):
"""Detail the given dispatcher."""
permission_classes = (IsAuthenticated,)
queryset = DispatcherAction.objects.all()
serializer_class = DispatcherActionSerializer

View File

@ -1,5 +1,5 @@
"""Site processors to add additional template tags and whatnot."""
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.utils.functional import SimpleLazyObject
@ -11,4 +11,5 @@ def site(request):
return {
'site': site,
'site_root': SimpleLazyObject(lambda: "{0}://{1}".format(protocol, site.domain)),
'WEB_ENABLED_APPS': settings.WEB_ENABLED_APPS,
}

View File

@ -43,7 +43,6 @@ INSTALLED_APPS = (
'countdown',
'dispatch',
'facts',
'gitlab_bot',
'ircbot',
'karma',
'markov',
@ -54,13 +53,13 @@ INSTALLED_APPS = (
)
MIDDLEWARE = (
'django.middleware.security.SecurityMiddleware',
'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',
'django.middleware.security.SecurityMiddleware',
)
ROOT_URLCONF = 'dr_botzo.urls'
@ -146,6 +145,16 @@ BOOTSTRAP3 = {
'javascript_in_head': True,
}
###############
# web options #
###############
# choose which apps to display in the web UI, for those that support this config
WEB_ENABLED_APPS = [
'karma',
'races',
]
# IRC module stuff
@ -173,10 +182,6 @@ STORYCRAFT_DEFAULT_GAME_LENGTH = 20
STORYCRAFT_DEFAULT_LINE_LENGTH = 140
STORYCRAFT_DEFAULT_LINES_PER_TURN = 2
# weather
WEATHER_WEATHER_UNDERGROUND_API_KEY = None
# load local settings

View File

@ -30,9 +30,9 @@
{% block navbar_menu %}
<ul class="nav navbar-nav">
<li><a href="{% url 'facts_index' %}">Item Sets</a></li>
<li><a href="{% url 'karma_index' %}">Karma</a></li>
{% if "karma" in WEB_ENABLED_APPS %}<li><a href="{% url 'karma_index' %}">Karma</a></li>{% endif %}
<li><a href="{% url 'markov_index' %}">Markov</a></li>
<li><a href="{% url 'races_index' %}">Races</a></li>
{% if "races" in WEB_ENABLED_APPS %}<li><a href="{% url 'races_index' %}">Races</a></li>{% endif %}
</ul>
{% endblock %}
<div class="navbar-right">

View File

View File

@ -1,8 +0,0 @@
"""Admin stuff for GitLab bot models."""
from django.contrib import admin
from gitlab_bot.models import GitlabConfig, GitlabProjectConfig
admin.site.register(GitlabConfig)
admin.site.register(GitlabProjectConfig)

View File

@ -1,253 +0,0 @@
"""Wrapped client for the configured GitLab bot."""
import logging
import random
import re
import gitlab
from gitlab_bot.models import GitlabConfig
log = logging.getLogger(__name__)
class GitlabBot(object):
"""Bot for doing GitLab stuff. Might be used by the IRC bot, or daemons, or whatever."""
REVIEWS_START_FORMAT = "Starting review process."
REVIEWS_RESET_FORMAT = "Review process reset."
REVIEWS_REVIEW_REGISTERED_FORMAT = "Review identified."
REVIEWS_PROGRESS_FORMAT = "{0:d} reviews of {1:d} necessary to proceed."
REVIEWS_REVIEWS_COMPLETE = "Reviews complete."
NEW_REVIEWER_FORMAT = "Assigning to {0:s} to review merge request."
NEW_ACCEPTER_FORMAT = "Assigning to {0:s} to accept merge request."
NOTE_COMMENT = [
"The computer is your friend.",
"Trust the computer.",
"Quality is mandatory.",
"Errors show your disloyalty to the computer.",
]
def __init__(self):
"""Initialize the actual GitLab client."""
config = GitlabConfig.objects.first()
self.client = gitlab.Gitlab(config.url, private_token=config.token)
self.client.auth()
def random_reviews_start_message(self):
return "{0:s} {1:s} {2:s}".format(self.REVIEWS_START_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
random.choice(self.NOTE_COMMENT))
def random_reviews_reset_message(self):
return "{0:s} {1:s} {2:s}".format(self.REVIEWS_RESET_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
random.choice(self.NOTE_COMMENT))
def random_review_progress_message(self):
return "{0:s} {1:s} {2:s}".format(self.REVIEWS_REVIEW_REGISTERED_FORMAT, self.REVIEWS_PROGRESS_FORMAT,
random.choice(self.NOTE_COMMENT))
def random_reviews_complete_message(self):
return "{0:s} {1:s}".format(self.REVIEWS_REVIEWS_COMPLETE, random.choice(self.NOTE_COMMENT))
def random_new_reviewer_message(self):
return "{0:s} {1:s}".format(self.NEW_REVIEWER_FORMAT, random.choice(self.NOTE_COMMENT))
def random_reviews_done_message(self):
return "{0:s} {1:s}".format(self.NEW_ACCEPTER_FORMAT, random.choice(self.NOTE_COMMENT))
def scan_project_for_reviews(self, project, merge_request_ids=None):
project_obj = self.client.projects.get(project.project_id)
if not project_obj:
return
if merge_request_ids:
merge_requests = []
for merge_request_id in merge_request_ids:
merge_requests.append(project_obj.mergerequests.get(id=merge_request_id))
else:
merge_requests = project_obj.mergerequests.list(state='opened')
for merge_request in merge_requests:
log.debug("scanning merge request '%s'", merge_request.title)
if merge_request.state in ['merged', 'closed']:
log.info("merge request '%s' is already %s, doing nothing", merge_request.title,
merge_request.state)
continue
request_state = _MergeRequestScanningState()
notes = sorted(self.client.project_mergerequest_notes.list(project_id=merge_request.project_id,
merge_request_id=merge_request.id,
all=True),
key=lambda x: x.id)
for note in notes:
if not note.system:
log.debug("merge request '%s', note '%s' is a normal message", merge_request.title, note.id)
# note that we can't ignore note.system = True + merge_request.author, that might have been
# a new push or something, so we only ignore normal notes from the author
if note.author == merge_request.author:
log.debug("skipping note from the merge request author")
elif note.author.username == self.client.user.username:
log.debug("saw a message from myself, i might have already sent a notice i'm sitting on")
if note.body.find(self.REVIEWS_RESET_FORMAT) >= 0 and request_state.unlogged_approval_reset:
log.debug("saw a reset message, unsetting the flag")
request_state.unlogged_approval_reset = False
elif note.body.find(self.REVIEWS_START_FORMAT) >= 0 and request_state.unlogged_review_start:
log.debug("saw a start message, unsetting the flag")
request_state.unlogged_review_start = False
elif note.body.find(self.REVIEWS_REVIEW_REGISTERED_FORMAT) >= 0 and request_state.unlogged_approval:
log.debug("saw a review log message, unsetting the flag")
request_state.unlogged_approval = False
elif note.body.find(self.REVIEWS_REVIEWS_COMPLETE) >= 0 and request_state.unlogged_review_complete:
log.debug("saw a review complete message, unsetting the flag")
request_state.unlogged_review_complete = False
else:
log.debug("nothing in particular relevant in '%s'", note.body)
else:
if note.body.find("LGTM") >= 0:
log.debug("merge request '%s', note '%s' has a LGTM", merge_request.title, note.id)
request_state.unlogged_approval = True
request_state.approver_list.append(note.author.username)
log.debug("approvers: %s", request_state.approver_list)
if len(request_state.approver_list) < project.code_reviews_necessary:
log.debug("not enough code reviews yet, setting needs_reviewer")
request_state.needs_reviewer = True
request_state.needs_accepter = False
else:
log.debug("enough code reviews, setting needs_accepter")
request_state.needs_accepter = True
request_state.needs_reviewer = False
request_state.unlogged_review_complete = True
else:
log.debug("merge request '%s', note '%s' does not have a LGTM", merge_request.title,
note.id)
else:
log.debug("merge request '%s', note '%s' is a system message", merge_request.title, note.id)
if re.match(r'Added \d+ commit', note.body):
log.debug("resetting approval list, '%s' looks like a push!", note.body)
# only set the unlogged approval reset flag if there's some kind of progress
if len(request_state.approver_list) > 0:
request_state.unlogged_approval_reset = True
request_state.needs_reviewer = True
request_state.approver_list.clear()
else:
log.debug("leaving the approval list as it is, i don't think '%s' is a push", note.body)
# do some cleanup
excluded_review_candidates = request_state.approver_list + [merge_request.author.username]
review_candidates = [x for x in project.code_reviewers.split(',') if x not in excluded_review_candidates]
if merge_request.assignee:
if request_state.needs_reviewer and merge_request.assignee.username in review_candidates:
log.debug("unsetting the needs_reviewer flag, the request is already assigned to one")
request_state.needs_reviewer = False
elif request_state.needs_reviewer and merge_request.assignee.username == merge_request.author.username:
log.info("unsetting the needs_reviewer flag, the request is assigned to the author")
log.info("in this case we are assuming that the author has work to do, and will re/unassign")
request_state.needs_reviewer = False
excluded_accept_candidates = [merge_request.author.username]
accept_candidates = [x for x in project.code_review_final_merge_assignees.split(',') if x not in excluded_accept_candidates]
if merge_request.assignee:
if request_state.needs_accepter and merge_request.assignee.username in accept_candidates:
log.debug("unsetting the needs_accepter flag, the request is already assigned to one")
request_state.needs_accepter = False
elif request_state.needs_accepter and merge_request.assignee.username == merge_request.author.username:
log.info("unsetting the needs_accepter flag, the request is assigned to the author")
log.info("in this case we are assuming that the author has work to do, and will re/unassign")
request_state.needs_accepter = False
log.debug("%s", request_state.__dict__)
# status message stuff
if request_state.unlogged_review_start:
log.info("sending message for start of reviews")
msg = {'body': self.random_reviews_start_message().format(len(request_state.approver_list),
project.code_reviews_necessary)}
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
merge_request_id=merge_request.id)
if request_state.unlogged_approval_reset:
log.info("sending message for review reset")
msg = {'body': self.random_reviews_reset_message().format(len(request_state.approver_list),
project.code_reviews_necessary)}
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
merge_request_id=merge_request.id)
if request_state.unlogged_approval:
log.info("sending message for code review progress")
msg = {'body': self.random_review_progress_message().format(len(request_state.approver_list),
project.code_reviews_necessary)}
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
merge_request_id=merge_request.id)
if request_state.unlogged_review_complete:
log.info("sending message for code review complete")
msg = {'body': self.random_reviews_complete_message()}
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
merge_request_id=merge_request.id)
# if there's a reviewer necessary, assign the merge request
if len(request_state.approver_list) < project.code_reviews_necessary and request_state.needs_reviewer:
log.debug("%s needs a code review", merge_request.title)
if merge_request.assignee is not None:
log.debug("%s currently assigned to %s", merge_request.title, merge_request.assignee.username)
if merge_request.assignee is None or merge_request.assignee.username not in review_candidates:
if len(review_candidates) > 0:
new_reviewer = review_candidates[merge_request.iid % len(review_candidates)]
log.debug("%s is the new reviewer", new_reviewer)
# get the user object for the new reviewer
new_reviewer_obj = self.client.users.get_by_username(new_reviewer)
# create note for the update
msg = {'body': self.random_new_reviewer_message().format(new_reviewer_obj.name)}
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
merge_request_id=merge_request.id)
# assign the merge request to the new reviewer
self.client.update(merge_request, assignee_id=new_reviewer_obj.id)
else:
log.warning("no reviewers left to review %s, doing nothing", merge_request.title)
else:
log.debug("needs_reviewer set but the request is assigned to a reviewer, doing nothing")
# if there's an accepter necessary, assign the merge request
if len(request_state.approver_list) >= project.code_reviews_necessary and request_state.needs_accepter:
log.debug("%s needs an accepter", merge_request.title)
if merge_request.assignee is not None:
log.debug("%s currently assigned to %s", merge_request.title, merge_request.assignee.username)
if merge_request.assignee is None or merge_request.assignee.username not in accept_candidates:
if len(accept_candidates) > 0:
new_accepter = accept_candidates[merge_request.iid % len(accept_candidates)]
log.debug("%s is the new accepter", new_accepter)
# get the user object for the new accepter
new_accepter_obj = self.client.users.get_by_username(new_accepter)
# create note for the update
msg = {'body': self.random_reviews_done_message().format(new_accepter_obj.name)}
self.client.project_mergerequest_notes.create(msg, project_id=project_obj.id,
merge_request_id=merge_request.id)
# assign the merge request to the new reviewer
self.client.update(merge_request, assignee_id=new_accepter_obj.id)
else:
log.warning("no accepters left to accept %s, doing nothing", merge_request.title)
class _MergeRequestScanningState(object):
"""Track the state of a merge request as it is scanned and appropriate action identified."""
def __init__(self):
"""Set default flags/values."""
self.approver_list = []
self.unlogged_review_start = True
self.unlogged_approval_reset = False
self.unlogged_approval = False
self.unlogged_review_complete = False
self.needs_reviewer = True
self.needs_accepter = False

View File

@ -1,20 +0,0 @@
"""Find merge requests that need code reviewers."""
import logging
from django.core.management import BaseCommand
from gitlab_bot.lib import GitlabBot
from gitlab_bot.models import GitlabProjectConfig
log = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Find merge requests needing code reviewers/accepters"
def handle(self, *args, **options):
bot = GitlabBot()
projects = GitlabProjectConfig.objects.filter(manage_merge_request_code_reviews=True)
for project in projects:
bot.scan_project_for_reviews(project)

View File

@ -1,25 +0,0 @@
"""Run the code review process on a specific merge request."""
import logging
from django.core.management import BaseCommand
from gitlab_bot.lib import GitlabBot
from gitlab_bot.models import GitlabProjectConfig
log = logging.getLogger(__name__)
class Command(BaseCommand):
help = "Assign code reviewers/accepters for a specific merge request"
def add_arguments(self, parser):
parser.add_argument('project_id', type=int)
parser.add_argument('merge_request_id', type=int)
def handle(self, *args, **options):
project = GitlabProjectConfig.objects.get(pk=options['project_id'])
merge_request_ids = [options['merge_request_id'], ]
bot = GitlabBot()
bot.scan_project_for_reviews(project, merge_request_ids=merge_request_ids)

View File

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='GitlabConfig',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
('url', models.URLField()),
('token', models.CharField(max_length=64)),
],
),
migrations.CreateModel(
name='GitlabProjectConfig',
fields=[
('id', models.AutoField(auto_created=True, verbose_name='ID', primary_key=True, serialize=False)),
('project_id', models.CharField(max_length=64)),
('manage_merge_request_code_reviews', models.BooleanField(default=False)),
('code_reviews_necessary', models.PositiveSmallIntegerField(default=0)),
('code_reviewers', models.TextField(blank=True, default='')),
('code_review_final_merge_assignees', models.TextField(blank=True, default='')),
('gitlab_config', models.ForeignKey(to='gitlab_bot.GitlabConfig', null=True, on_delete=models.CASCADE)),
],
),
]

View File

@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gitlab_bot', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='gitlabprojectconfig',
name='project_id',
field=models.CharField(unique=True, max_length=64),
),
]

View File

@ -1,36 +0,0 @@
"""Bot/daemons for doing stuff with GitLab."""
import logging
from django.db import models
log = logging.getLogger(__name__)
class GitlabConfig(models.Model):
"""Maintain bot-wide settings (URL, auth key, etc.)."""
url = models.URLField()
token = models.CharField(max_length=64)
def __str__(self):
"""String representation."""
return "bot @ {0:s}".format(self.url)
class GitlabProjectConfig(models.Model):
"""Maintain settings for a particular project in GitLab."""
gitlab_config = models.ForeignKey('GitlabConfig', null=True, on_delete=models.CASCADE)
project_id = models.CharField(max_length=64, unique=True)
manage_merge_request_code_reviews = models.BooleanField(default=False)
code_reviews_necessary = models.PositiveSmallIntegerField(default=0)
code_reviewers = models.TextField(default='', blank=True)
code_review_final_merge_assignees = models.TextField(default='', blank=True)
def __str__(self):
"""String representation."""
return "configuration for {0:s} @ {1:s}".format(self.project_id, self.gitlab_config.url)

View File

@ -1,46 +1,36 @@
"""Provide the base IRC client bot which other code can latch onto."""
import bisect
import collections
import copy
import importlib
import logging
import re
from xmlrpc.server import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
import socket
import ssl
import sys
import threading
import time
from xmlrpc.server import SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
from django.conf import settings
import irc.buffer
import irc.client
import irc.modes
from irc.bot import Channel
from irc.connection import Factory
from irc.dict import IRCDict
import irc.modes
from jaraco.stream import buffer
import ircbot.lib as ircbotlib
from dr_botzo import __version__
from ircbot.models import Alias, IrcChannel, IrcPlugin, IrcServer
log = logging.getLogger('ircbot.bot')
class IrcBotXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
"""Override the basic request handler to change the logging."""
def log_message(self, format, *args):
"""Use a logger rather than stderr."""
log.debug("XML-RPC - %s - %s", self.client_address[0], format % args)
class PrioritizedRegexHandler(collections.namedtuple('Base', ('priority', 'regex', 'callback'))):
"""Regex handler that still uses the normal handler priority stuff."""
def __lt__(self, other):
"""When sorting prioritized handlers, only use the priority"""
"""When sorting prioritized handlers, only use the priority."""
return self.priority < other.priority
@ -52,7 +42,7 @@ class LenientServerConnection(irc.client.ServerConnection):
method on a Reactor object.
"""
buffer_class = irc.buffer.LenientDecodingLineBuffer
buffer_class = buffer.LenientDecodingLineBuffer
server_config = None
@ -65,6 +55,9 @@ class LenientServerConnection(irc.client.ServerConnection):
class DrReactor(irc.client.Reactor):
"""Customize the basic IRC library's Reactor with more features."""
# used by Reactor.server() to initialize
connection_class = LenientServerConnection
def __do_nothing(*args, **kwargs):
pass
@ -73,18 +66,10 @@ class DrReactor(irc.client.Reactor):
super(DrReactor, self).__init__(on_connect=on_connect, on_disconnect=on_disconnect)
self.regex_handlers = {}
def server(self):
"""Creates and returns a ServerConnection object."""
c = LenientServerConnection(self)
with self.mutex:
self.connections.append(c)
return c
def add_global_regex_handler(self, events, regex, handler, priority=0):
"""Adds a global handler function for a specific event type and regex.
"""Add a global handler function for a specific event type and regex.
Arguments:
events --- Event type(s) (a list of strings).
handler -- Callback function taking connection and event
@ -114,10 +99,9 @@ class DrReactor(irc.client.Reactor):
bisect.insort(event_regex_handlers, handler)
def remove_global_regex_handler(self, events, handler):
"""Removes a global regex handler function.
"""Remove a global regex handler function.
Arguments:
events -- Event type(s) (a list of strings).
handler -- Callback function.
@ -178,6 +162,7 @@ class DrReactor(irc.client.Reactor):
if sender_nick == channel.discord_bridge:
short_what = ' '.join(what.split(' ')[1:])
match = re.match(addressed_pattern, short_what, re.IGNORECASE)
event.arguments[0] = short_what
else:
match = re.match(addressed_pattern, what, re.IGNORECASE)
except IrcChannel.DoesNotExist:
@ -228,8 +213,7 @@ class DrReactor(irc.client.Reactor):
if result == "NO MORE":
return
except Exception as ex:
log.error("caught exception!")
log.exception(ex)
log.exception("caught exception!")
connection.privmsg(event.target, str(ex))
def try_recursion(self, connection, event):
@ -416,7 +400,7 @@ class IRCBot(irc.client.SimpleIRCClient):
# load XML-RPC server
self.xmlrpc = SimpleXMLRPCServer((self.server_config.xmlrpc_host, self.server_config.xmlrpc_port),
requestHandler=IrcBotXMLRPCRequestHandler, allow_none=True)
requestHandler=SimpleXMLRPCRequestHandler, allow_none=True)
self.xmlrpc.register_introspection_functions()
t = threading.Thread(target=self._xmlrpc_listen, args=())
@ -429,8 +413,7 @@ class IRCBot(irc.client.SimpleIRCClient):
def _connected_checker(self):
if not self.connection.is_connected():
self.connection.execute_delayed(self.reconnection_interval,
self._connected_checker)
self.reactor.scheduler.execute_after(self.reconnection_interval, self._connected_checker)
self.jump_server()
def _connect(self):
@ -448,8 +431,7 @@ class IRCBot(irc.client.SimpleIRCClient):
def _on_disconnect(self, c, e):
self.channels = IRCDict()
self.connection.execute_delayed(self.reconnection_interval,
self._connected_checker)
self.reactor.scheduler.execute_after(self.reconnection_interval, self._connected_checker)
def _on_join(self, c, e):
ch = e.target
@ -558,7 +540,7 @@ class IRCBot(irc.client.SimpleIRCClient):
for chan in IrcChannel.objects.filter(autojoin=True, server=connection.server_config):
log.info("autojoining %s", chan.name)
self.connection.join(chan)
self.connection.join(chan.name)
for plugin in IrcPlugin.objects.filter(autoload=True):
log.info("autoloading %s", plugin.path)
@ -592,12 +574,11 @@ class IRCBot(irc.client.SimpleIRCClient):
self.connection.disconnect(msg)
def get_version(self):
"""Returns the bot version.
"""Return the bot version.
Used when answering a CTCP VERSION request.
"""
return "Python irc.bot ({version})".format(
version=irc.client.VERSION_STRING)
return f"dr.botzo {__version__}"
def jump_server(self, msg="Changing servers"):
"""Connect to a new server, potentially disconnecting from the current one."""
@ -607,7 +588,7 @@ class IRCBot(irc.client.SimpleIRCClient):
self._connect()
def on_ctcp(self, c, e):
"""Default handler for ctcp events.
"""Handle for ctcp events.
Replies to VERSION and PING requests and relays DCC requests
to the on_dccchat method.
@ -997,165 +978,3 @@ class IRCBot(irc.client.SimpleIRCClient):
del sys.modules[path]
self.die(msg="Shutting down...")
class Channel(object):
"""A class for keeping information about an IRC channel."""
def __init__(self):
"""Initialize channel object."""
self.userdict = IRCDict()
self.operdict = IRCDict()
self.voiceddict = IRCDict()
self.ownerdict = IRCDict()
self.halfopdict = IRCDict()
self.modes = {}
def users(self):
"""Returns an unsorted list of the channel's users."""
return list(self.userdict.keys())
def opers(self):
"""Returns an unsorted list of the channel's operators."""
return list(self.operdict.keys())
def voiced(self):
"""Returns an unsorted list of the persons that have voice mode set in the channel."""
return list(self.voiceddict.keys())
def owners(self):
"""Returns an unsorted list of the channel's owners."""
return list(self.ownerdict.keys())
def halfops(self):
"""Returns an unsorted list of the channel's half-operators."""
return list(self.halfopdict.keys())
def has_user(self, nick):
"""Check whether the channel has a user."""
return nick in self.userdict
def is_oper(self, nick):
"""Check whether a user has operator status in the channel."""
return nick in self.operdict
def is_voiced(self, nick):
"""Check whether a user has voice mode set in the channel."""
return nick in self.voiceddict
def is_owner(self, nick):
"""Check whether a user has owner status in the channel."""
return nick in self.ownerdict
def is_halfop(self, nick):
"""Check whether a user has half-operator status in the channel."""
return nick in self.halfopdict
def add_user(self, nick):
"""Add user."""
self.userdict[nick] = 1
def remove_user(self, nick):
"""Remove user."""
for d in self.userdict, self.operdict, self.voiceddict:
if nick in d:
del d[nick]
def change_nick(self, before, after):
"""Handle a nick change."""
self.userdict[after] = self.userdict.pop(before)
if before in self.operdict:
self.operdict[after] = self.operdict.pop(before)
if before in self.voiceddict:
self.voiceddict[after] = self.voiceddict.pop(before)
def set_userdetails(self, nick, details):
"""Set user details."""
if nick in self.userdict:
self.userdict[nick] = details
def set_mode(self, mode, value=None):
"""Set mode on the channel.
Arguments:
mode -- The mode (a single-character string).
value -- Value
"""
if mode == "o":
self.operdict[value] = 1
elif mode == "v":
self.voiceddict[value] = 1
elif mode == "q":
self.ownerdict[value] = 1
elif mode == "h":
self.halfopdict[value] = 1
else:
self.modes[mode] = value
def clear_mode(self, mode, value=None):
"""Clear mode on the channel.
Arguments:
mode -- The mode (a single-character string).
value -- Value
"""
try:
if mode == "o":
del self.operdict[value]
elif mode == "v":
del self.voiceddict[value]
elif mode == "q":
del self.ownerdict[value]
elif mode == "h":
del self.halfopdict[value]
else:
del self.modes[mode]
except KeyError:
pass
def has_mode(self, mode):
"""Return if mode is in channel modes."""
return mode in self.modes
def is_moderated(self):
"""Return if the channel is +m."""
return self.has_mode("m")
def is_secret(self):
"""Return if the channel is +s."""
return self.has_mode("s")
def is_protected(self):
"""Return if the channel is +p."""
return self.has_mode("p")
def has_topic_lock(self):
"""Return if the channel is +t."""
return self.has_mode("t")
def is_invite_only(self):
"""Return if the channel is +i."""
return self.has_mode("i")
def has_allow_external_messages(self):
"""Return if the channel is +n."""
return self.has_mode("n")
def has_limit(self):
"""Return if the channel is +l."""
return self.has_mode("l")
def limit(self):
"""Return the channel limit count."""
if self.has_limit():
return self.modes["l"]
else:
return None
def has_key(self):
"""Return if the channel is +k."""
return self.has_mode("k")

View File

@ -118,7 +118,7 @@ class IrcChannel(models.Model):
def __str__(self):
"""Provide string representation."""
return "{0:s}".format(self.name)
return "{0:s} on {1:s}".format(self.name, self.server.name)
class IrcPlugin(models.Model):

View File

@ -1,6 +1,6 @@
"""URL patterns for the karma views."""
from django.conf.urls import include
from django.urls import path
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter
from karma.views import KarmaKeyViewSet, index, key_detail
@ -10,7 +10,7 @@ router.register(r'keys', KarmaKeyViewSet)
urlpatterns = [
path('', index, name='karma_index'),
path('key/<karma_key>/', key_detail, name='karma_key_detail'),
re_path(r'^key/(?P<karma_key>.+)/', key_detail, name='karma_key_detail'),
path('api/', include(router.urls)),
]

View File

@ -1,27 +1,32 @@
"""Present karma data."""
import logging
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, render
from rest_framework import viewsets
from karma.models import KarmaKey
from karma.serializers import KarmaKeySerializer
log = logging.getLogger('karma.views')
log = logging.getLogger(__name__)
def index(request):
"""Display all karma keys."""
entries = KarmaKey.objects.all().order_by('key')
if 'karma' not in settings.WEB_ENABLED_APPS:
raise PermissionDenied()
entries = KarmaKey.objects.all().order_by('key')
return render(request, 'karma/index.html', {'entries': entries})
def key_detail(request, karma_key):
"""Display the requested karma key."""
entry = get_object_or_404(KarmaKey, key=karma_key.lower())
if 'karma' not in settings.WEB_ENABLED_APPS:
raise PermissionDenied()
entry = get_object_or_404(KarmaKey, key=karma_key.lower())
return render(request, 'karma/karma_key.html', {'entry': entry, 'entry_history': entry.history(mode='date')})

View File

@ -119,12 +119,12 @@ class Markov(Plugin):
target_name = target_name.lower()
# find the stuff, or create it
channel, c = IrcChannel.objects.get_or_create(name=target_name, server=self.connection.server_config)
try:
target = MarkovTarget.objects.get(name=target_name)
target = MarkovTarget.objects.get(channel=channel)
except MarkovTarget.DoesNotExist:
# we need to create a context and a target, and we have to make the context first
# make a context --- lacking a good idea, just create one with this target name until configured otherwise
channel, c = IrcChannel.objects.get_or_create(name=target_name, server=self.connection.server_config)
context, c = MarkovContext.objects.get_or_create(name=target_name)
target, c = MarkovTarget.objects.get_or_create(name=target_name, context=context, channel=channel)

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2023-05-04 22:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('markov', '0007_alter_markovtarget_channel'),
]
operations = [
migrations.AlterField(
model_name='markovtarget',
name='name',
field=models.CharField(max_length=200),
),
]

View File

@ -21,7 +21,7 @@ class MarkovContext(models.Model):
class MarkovTarget(models.Model):
"""Define IRC targets that relate to a context, and can occasionally be talked to."""
name = models.CharField(max_length=200, unique=True)
name = models.CharField(max_length=200)
context = models.ForeignKey(MarkovContext, on_delete=models.CASCADE)
channel = models.ForeignKey(IrcChannel, on_delete=models.CASCADE)
@ -29,7 +29,7 @@ class MarkovTarget(models.Model):
def __str__(self):
"""Provide string representation."""
return "{0:s} -> {1:s}".format(self.name, self.context.name)
return "{0:s} -> {1:s}".format(str(self.channel), self.context.name)
class MarkovState(models.Model):

View File

@ -1,26 +1,28 @@
"""Display race statuses and whatnot."""
import logging
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404, render
from races.models import Race, Racer, RaceUpdate
log = logging.getLogger('races.views')
log = logging.getLogger(__name__)
def index(request):
"""Display a list of races."""
if 'races' not in settings.WEB_ENABLED_APPS:
raise PermissionDenied()
races = Race.objects.all()
return render(request, 'races/index.html', {'races': races})
def race_detail(request, race_id):
"""Display a race detail."""
if 'races' not in settings.WEB_ENABLED_APPS:
raise PermissionDenied()
race = get_object_or_404(Race, pk=race_id)
return render(request, 'races/race_detail.html', {'race': race})

View File

@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.8
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
@ -22,7 +22,7 @@ click==8.1.3
# via
# pip-tools
# safety
coverage[toml]==7.1.0
coverage[toml]==7.2.1
# via pytest-cov
distlib==0.3.6
# via virtualenv
@ -77,19 +77,15 @@ flake8-mutable==1.2.0
# via -r requirements/requirements-dev.in
gitdb==4.0.10
# via gitpython
gitpython==3.1.30
gitpython==3.1.31
# via bandit
idna==3.4
# via requests
importlib-resources==5.10.2
# via jaraco-text
inflect==6.0.2
# via
# jaraco-itertools
# jaraco-text
# via jaraco-text
iniconfig==2.0.0
# via pytest
irc==15.0.6
irc==20.1.0
# via -r requirements/requirements.in
isort==5.12.0
# via flake8-isort
@ -99,13 +95,11 @@ jaraco-collections==3.8.0
# via irc
jaraco-context==4.3.0
# via jaraco-text
jaraco-functools==3.5.2
jaraco-functools==3.6.0
# via
# irc
# jaraco-text
# tempora
jaraco-itertools==6.2.1
# via irc
jaraco-logging==3.1.2
# via irc
jaraco-stream==3.0.3
@ -116,12 +110,11 @@ jaraco-text==3.11.1
# jaraco-collections
mccabe==0.7.0
# via flake8
more-itertools==9.0.0
more-itertools==9.1.0
# via
# irc
# jaraco-classes
# jaraco-functools
# jaraco-itertools
# jaraco-text
packaging==21.3
# via
@ -134,7 +127,7 @@ parsedatetime==2.6
# via -r requirements/requirements.in
pbr==5.11.1
# via stevedore
pip-tools==6.12.2
pip-tools==6.12.3
# via -r requirements/requirements-dev.in
platformdirs==3.0.0
# via virtualenv
@ -169,8 +162,6 @@ pytest-django==4.5.2
# via -r requirements/requirements-dev.in
python-dateutil==2.8.2
# via -r requirements/requirements.in
python-gitlab==3.13.0
# via -r requirements/requirements.in
python-mpd2==3.0.5
# via -r requirements/requirements.in
pytz==2022.7.1
@ -183,12 +174,7 @@ pytz==2022.7.1
pyyaml==6.0
# via bandit
requests==2.28.2
# via
# python-gitlab
# requests-toolbelt
# safety
requests-toolbelt==0.10.1
# via python-gitlab
# via safety
ruamel-yaml==0.17.21
# via safety
ruamel-yaml-clib==0.2.7
@ -197,7 +183,6 @@ safety==2.3.5
# via -r requirements/requirements-dev.in
six==1.16.0
# via
# irc
# python-dateutil
# tox
smmap==5.0.0
@ -233,7 +218,7 @@ urllib3==1.26.14
# via requests
versioneer==0.28
# via -r requirements/requirements-dev.in
virtualenv==20.19.0
virtualenv==20.20.0
# via tox
wheel==0.38.4
# via
@ -241,8 +226,6 @@ wheel==0.38.4
# tox-wheel
zalgo-text==0.6
# via -r requirements/requirements.in
zipp==3.13.0
# via importlib-resources
# The following packages are considered to be unsafe in a requirements file:
# pip

View File

@ -2,12 +2,11 @@ Django<4.0 # core
django-adminplus # admin.site.register_view
django-bootstrap3 # bootstrap layout
django-extensions # more commands
djangorestframework # dispatch WS API
irc==15.0.6 # core, pinned until I can bother to update --- 17.x has API changes
djangorestframework # WS API
irc # core
parsedatetime # relative date stuff in countdown
ply # dice lex/yacc compiler
python-dateutil # countdown relative math
python-gitlab # client for the gitlab bot
python-mpd2 # client for mpd
pytz # timezone awareness
zalgo-text # zalgoify text

View File

@ -1,5 +1,5 @@
#
# This file is autogenerated by pip-compile with Python 3.8
# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --output-file=requirements/requirements.txt requirements/requirements.in
@ -8,10 +8,6 @@ asgiref==3.6.0
# via django
autocommand==2.2.2
# via jaraco-text
certifi==2022.12.7
# via requests
charset-normalizer==3.0.1
# via requests
django==3.2.18
# via
# -r requirements/requirements.in
@ -26,15 +22,9 @@ django-extensions==3.2.1
# via -r requirements/requirements.in
djangorestframework==3.14.0
# via -r requirements/requirements.in
idna==3.4
# via requests
importlib-resources==5.10.2
# via jaraco-text
inflect==6.0.2
# via
# jaraco-itertools
# jaraco-text
irc==15.0.6
# via jaraco-text
irc==20.1.0
# via -r requirements/requirements.in
jaraco-classes==3.2.3
# via jaraco-collections
@ -42,13 +32,11 @@ jaraco-collections==3.8.0
# via irc
jaraco-context==4.3.0
# via jaraco-text
jaraco-functools==3.5.2
jaraco-functools==3.6.0
# via
# irc
# jaraco-text
# tempora
jaraco-itertools==6.2.1
# via irc
jaraco-logging==3.1.2
# via irc
jaraco-stream==3.0.3
@ -57,12 +45,11 @@ jaraco-text==3.11.1
# via
# irc
# jaraco-collections
more-itertools==9.0.0
more-itertools==9.1.0
# via
# irc
# jaraco-classes
# jaraco-functools
# jaraco-itertools
# jaraco-text
parsedatetime==2.6
# via -r requirements/requirements.in
@ -72,8 +59,6 @@ pydantic==1.10.5
# via inflect
python-dateutil==2.8.2
# via -r requirements/requirements.in
python-gitlab==3.13.0
# via -r requirements/requirements.in
python-mpd2==3.0.5
# via -r requirements/requirements.in
pytz==2022.7.1
@ -83,16 +68,8 @@ pytz==2022.7.1
# djangorestframework
# irc
# tempora
requests==2.28.2
# via
# python-gitlab
# requests-toolbelt
requests-toolbelt==0.10.1
# via python-gitlab
six==1.16.0
# via
# irc
# python-dateutil
# via python-dateutil
sqlparse==0.4.3
# via django
tempora==5.2.1
@ -101,9 +78,5 @@ tempora==5.2.1
# jaraco-logging
typing-extensions==4.5.0
# via pydantic
urllib3==1.26.14
# via requests
zalgo-text==0.6
# via -r requirements/requirements.in
zipp==3.13.0
# via importlib-resources

View File

@ -1,6 +1,6 @@
"""Test the dispatch package's webservice."""
from django.contrib.auth.models import User
from rest_framework.status import HTTP_200_OK
from rest_framework.status import HTTP_200_OK, HTTP_403_FORBIDDEN
from rest_framework.test import APITestCase
from dispatch.models import Dispatcher, DispatcherAction
@ -27,3 +27,18 @@ class DispatchAPITest(APITestCase):
resp = self.client.get('/dispatch/api/actions/')
self.assertEqual(resp.status_code, HTTP_200_OK)
self.assertEqual(len(resp.json()), DispatcherAction.objects.count())
def test_unauthed_dispatch_object_retrieval(self):
"""Test that the list endpoints require authentication."""
client = self.client_class()
resp = client.get('/dispatch/api/dispatchers/')
self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN)
resp = client.get('/dispatch/api/dispatchers/111/')
self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN)
resp = client.get('/dispatch/api/dispatchers/fake/')
self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN)
resp = client.get('/dispatch/api/actions/')
self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN)
resp = client.get('/dispatch/api/actions/111/')
self.assertEqual(resp.status_code, HTTP_403_FORBIDDEN)

View File

@ -1,8 +1,5 @@
"""Test the race views."""
from unittest import mock
from django.test import TestCase
from django.utils.timezone import now
from races.models import Race, Racer, RaceUpdate

View File

@ -0,0 +1,46 @@
"""Test views and templates' adherence to WEB_ENABLED_APPS."""
from django.conf import settings
from django.test import TestCase
class WebEnabledAppsAPITest(TestCase):
"""Test that certain display elements and views can be enabled/disabled via settings."""
def setUp(self):
"""Store the old setting so that we can restore it later."""
self.old_sites_list = settings.WEB_ENABLED_APPS
print("butt")
def tearDown(self):
"""Restore the old setting stored earlier."""
settings.WEB_ENABLED_APPS = self.old_sites_list
print("butt")
def test_default_enabled(self):
"""Test that the expected sites can be reached and displayed by default."""
resp = self.client.get('/karma/')
self.assertEqual(resp.status_code, 200)
self.assertIn(b'<a href="/karma/">', resp.content)
resp = self.client.get('/races/')
self.assertEqual(resp.status_code, 200)
self.assertIn(b'<a href="/races/">', resp.content)
def test_one_disabled(self):
"""Test that we can disable one site but not all sites using this setting."""
settings.WEB_ENABLED_APPS = ['karma']
resp = self.client.get('/karma/')
self.assertEqual(resp.status_code, 200)
self.assertIn(b'<a href="/karma/">', resp.content)
resp = self.client.get('/races/')
self.assertEqual(resp.status_code, 403)
self.assertNotIn(b'<a href="/races/">', resp.content)
def test_all_disabled(self):
"""Test that we can disable all sites using this setting."""
settings.WEB_ENABLED_APPS = []
resp = self.client.get('/karma/')
self.assertEqual(resp.status_code, 403)
self.assertNotIn(b'<a href="/karma/">', resp.content)
resp = self.client.get('/races/')
self.assertEqual(resp.status_code, 403)
self.assertNotIn(b'<a href="/races/">', resp.content)

View File

@ -181,7 +181,7 @@ exclude =
.tox/
versioneer.py
_version.py
instance/
**/migrations/
extend-ignore = T101
max-complexity = 10
max-line-length = 120

View File

@ -1,20 +1,17 @@
"""Report on the weather via wttr.in."""
import logging
from ircbot.lib import Plugin
from weather.lib import weather_summary
log = logging.getLogger('weather.ircplugin')
class Weather(Plugin):
"""Have IRC commands to do IRC things (join channels, quit, etc.)."""
def start(self):
"""Set up the handlers."""
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!weather\s+(.*)$',
self.handle_weather, -20)
@ -22,12 +19,12 @@ class Weather(Plugin):
def stop(self):
"""Tear down handlers."""
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_weather)
super(Weather, self).stop()
def handle_weather(self, connection, event, match):
"""Make the weather query and format it for IRC."""
query = match.group(1)
queryitems = query.split(" ")
if len(queryitems) <= 0: