diff --git a/.gitignore b/.gitignore index 178c9e4e..5a271ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ db.sqlite3 .env *.pyc venv/ +venv3/ diff --git a/bornhack/settings.py b/bornhack/settings.py index 7640f228..8a7a12d1 100644 --- a/bornhack/settings.py +++ b/bornhack/settings.py @@ -44,6 +44,7 @@ INSTALLED_APPS = [ 'program', 'info', 'sponsors', + 'ircbot', 'allauth', 'allauth.account', diff --git a/ircbot/__init__.py b/ircbot/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ircbot/admin.py b/ircbot/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/ircbot/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/ircbot/apps.py b/ircbot/apps.py new file mode 100644 index 00000000..f25a77f9 --- /dev/null +++ b/ircbot/apps.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class IrcbotConfig(AppConfig): + name = 'ircbot' diff --git a/ircbot/irc3module.py b/ircbot/irc3module.py new file mode 100644 index 00000000..feb28369 --- /dev/null +++ b/ircbot/irc3module.py @@ -0,0 +1,91 @@ +import irc3 +from ircbot.models import OutgoingIrcMessage +from django.conf import settings + + +@irc3.plugin +class Plugin(object): + """BornHack IRC3 class""" + + requires = [ + 'irc3.plugins.core', # makes the bot able to connect to an irc server and do basic irc stuff + 'irc3.plugins.userlist', # maintains a convenient list of the channels the bot is in and their users + 'irc3.plugins.command', # what does this do? + ] + + def __init__(self, bot): + self.bot = bot + self.log = self.bot.log + + + ############################################################################################### + ### builtin irc3 event methods + + def server_ready(self, **kwargs): + """triggered after the server sent the MOTD (require core plugin)""" + if settings.DEBUG: + print("inside server_ready(), kwargs: %s" % kwargs) + + + def connection_lost(self, **kwargs): + """triggered when connection is lost""" + if settings.DEBUG: + print("inside connection_lost(), kwargs: %s" % kwargs) + + + def connection_made(self, **kwargs): + """triggered when connection is up""" + if settings.DEBUG: + print("inside connection_made(), kwargs: %s" % kwargs) + + + ############################################################################################### + ### decorated irc3 event methods + + @irc3.event(irc3.rfc.JOIN_PART_QUIT) + def on_join_part_quit(self, **kwargs): + """triggered when there is a join part or quit on a channel the bot is in""" + if settings.DEBUG: + print("inside on_join_part_quit(), kwargs: %s" % kwargs) + if self.bot.nick == kwargs['mask'].split("!")[0] and kwargs['channel'] == "#tirsdagsfilm": + self.bot.loop.call_later(1, self.bot.get_outgoing_messages) + + + @irc3.event(irc3.rfc.PRIVMSG) + def on_privmsg(self, **kwargs): + """triggered when a privmsg is sent to the bot or to a channel the bot is in""" + if settings.DEBUG: + print("inside on_privmsg(), kwargs: %s" % kwargs) + + + @irc3.event(irc3.rfc.KICK) + def on_kick(self, **kwargs): + if settings.DEBUG: + print("inside on_kick(), kwargs: %s" % kwargs) + + + ############################################################################################### + ### custom irc3 methods + @irc3.extend + def get_outgoing_messages(self): + """ + This method gets unprocessed OutgoingIrcMessage objects and attempts to send them to + the target channel. Messages are skipped if the bot is not in the channel. + """ + # TODO: handle privmsg to users + # TODO: set a timeout for messages.. a few minutes maybe? + # TODO: make sleep time configurable + print("inside get_outgoing_messages()") + for msg in OutgoingIrcMessage.objects.filter(processed=False).order_by('created_date'): + if msg.target[0] == "#" and msg.target in self.bot.channels: + print("sending privmsg to %s: %s" % (msg.target, msg.message)) + self.bot.privmsg(msg.target, msg.message) + msg.processed=True + msg.save() + else: + print("skipping message to channel %s because the bot is not in the channel" % msg.target) + + # call this function again in 60 seconds + self.bot.loop.call_later(60, self.bot.get_outgoing_messages) + + diff --git a/ircbot/management/__init__.py b/ircbot/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ircbot/management/commands/__init__.py b/ircbot/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ircbot/management/commands/ircbot.py b/ircbot/management/commands/ircbot.py new file mode 100644 index 00000000..a9ceb799 --- /dev/null +++ b/ircbot/management/commands/ircbot.py @@ -0,0 +1,30 @@ +from django.core.management.base import BaseCommand +from django.conf import settings +from django.utils import timezone +from time import sleep +import irc3, sys, asyncio + + +class Command(BaseCommand): + args = 'none' + help = 'Runs the BornHack IRC bot to announce talks and manage team channel permissions' + + def output(self, message): + self.stdout.write('%s: %s' % (timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message)) + + def handle(self, *args, **options): + self.output('IRC bot worker running...') + # connect to IRC + config = { + 'nick': 'BornHack', + 'autojoins': ['#tirsdagsfilm'], + 'host': 'ircd.tyknet.dk', + 'port': 6697, + 'ssl': True, + 'timeout': 30, + 'includes': [ + 'ircbot.irc3module', + ], + } + irc3.IrcBot(**config).run(forever=True) + diff --git a/ircbot/migrations/0001_initial.py b/ircbot/migrations/0001_initial.py new file mode 100644 index 00000000..ed1a4d90 --- /dev/null +++ b/ircbot/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-01-29 20:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='OutgoingIrcMessage', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('target', models.CharField(max_length=100)), + ('message', models.CharField(max_length=200)), + ('processed', models.BooleanField(default=False)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/ircbot/migrations/__init__.py b/ircbot/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ircbot/models.py b/ircbot/models.py new file mode 100644 index 00000000..06eb7b68 --- /dev/null +++ b/ircbot/models.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +from utils.models import UUIDModel, CreatedUpdatedModel +from django.db import models + + +class OutgoingIrcMessage(CreatedUpdatedModel): + target = models.CharField(max_length=100) + message = models.CharField(max_length=200) + processed = models.BooleanField(default=False) + diff --git a/ircbot/views.py b/ircbot/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/ircbot/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/requirements.txt b/requirements.txt index e7d4f986..08d64d35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ Pillow==4.0.0 PyPDF2==1.26.0 Unidecode==0.04.20 argparse==1.2.1 +asyncio==3.4.3 bleach==1.5.0 django-allauth==0.30.0 django-bleach==0.3.0 @@ -11,8 +12,10 @@ django-bootstrap3==8.1.0 django-debug-toolbar==1.6 django-environ==0.4.1 django-wkhtmltopdf==3.1.0 +docopt==0.6.2 future==0.16.0 html5lib==0.9999999 +irc3==0.9.8 oauthlib==2.0.1 olefile==0.44 psycopg2==2.6.2 @@ -23,5 +26,6 @@ requests==2.12.5 requests-oauthlib==0.7.0 six==1.10.0 sqlparse==0.2.2 +venusian==1.0 webencodings==0.5 wsgiref==0.1.2 diff --git a/requirements3.txt b/requirements3.txt new file mode 100644 index 00000000..3b3a534d --- /dev/null +++ b/requirements3.txt @@ -0,0 +1,30 @@ +CommonMark==0.7.3 +Django==1.10.5 +Pillow==4.0.0 +PyPDF2==1.26.0 +Unidecode==0.04.20 +argparse==1.2.1 +asyncio==3.4.3 +bleach==1.5.0 +django-allauth==0.30.0 +django-bleach==0.3.0 +django-bootstrap3==8.1.0 +django-debug-toolbar==1.6 +django-environ==0.4.1 +django-wkhtmltopdf==3.1.0 +docopt==0.6.2 +future==0.16.0 +html5lib==0.9999999 +irc3==0.9.8 +oauthlib==2.0.1 +olefile==0.44 +psycopg2==2.6.2 +python-openid==2.2.5 +pytz==2016.10 +qrcode==5.3 +requests==2.12.5 +requests-oauthlib==0.7.0 +six==1.10.0 +sqlparse==0.2.2 +venusian==1.0 +webencodings==0.5 diff --git a/shop/urls.py b/shop/urls.py index 704cd7ea..7a6f2636 100644 --- a/shop/urls.py +++ b/shop/urls.py @@ -1,5 +1,5 @@ from django.conf.urls import url -from views import * +from .views import * urlpatterns = [ url(r'^$', ShopIndexView.as_view(), name='index'), diff --git a/shop/views.py b/shop/views.py index 1c6583fe..d60d6dc8 100644 --- a/shop/views.py +++ b/shop/views.py @@ -447,21 +447,18 @@ class EpayCallbackView(SingleObjectMixin, View): if 'orderid' in request.GET: query = OrderedDict( - map( - lambda x: tuple(x.split('=')), - request.META['QUERY_STRING'].split('&') - ) + [tuple(x.split('=')) for x in request.META['QUERY_STRING'].split('&')] ) order = get_object_or_404(Order, pk=query.get('orderid')) if order.pk != self.get_object().pk: - print "bad epay callback, orders do not match!" + print("bad epay callback, orders do not match!") return HttpResponse(status=400) if validate_epay_callback(query): callback.md5valid=True callback.save() else: - print "bad epay callback!" + print("bad epay callback!") return HttpResponse(status=400) if order.paid: @@ -479,7 +476,7 @@ class EpayCallbackView(SingleObjectMixin, View): ### and mark order as paid (this will create tickets) order.mark_as_paid() else: - print "valid epay callback with wrong amount detected" + print("valid epay callback with wrong amount detected") else: return HttpResponse(status=400) @@ -541,7 +538,7 @@ class CoinifyRedirectView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUn # check if it expired if parse_datetime(order.coinifyapiinvoice.invoicejson['expire_time']) < timezone.now(): # this coinifyinvoice expired, delete it - print "deleting expired coinifyinvoice id %s" % order.coinifyapiinvoice.invoicejson['id'] + print("deleting expired coinifyinvoice id %s" % order.coinifyapiinvoice.invoicejson['id']) order.coinifyapiinvoice.delete() order = self.get_object() @@ -568,10 +565,10 @@ class CoinifyRedirectView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUn # Parse response if not response['success']: api_error = response['error'] - print "API error: %s (%s)" % ( + print("API error: %s (%s)" % ( api_error['message'], api_error['code'] - ) + )) messages.error(request, "There was a problem with the payment provider. Please try again later") return HttpResponseRedirect(reverse_lazy('shop:order_detail', kwargs={'pk': self.get_object().pk})) else: @@ -580,7 +577,7 @@ class CoinifyRedirectView(LoginRequiredMixin, EnsureUserOwnsOrderMixin, EnsureUn invoicejson = response['data'], order = order, ) - print "created new coinifyinvoice id %s" % coinifyinvoice.invoicejson['id'] + print("created new coinifyinvoice id %s" % coinifyinvoice.invoicejson['id']) return super(CoinifyRedirectView, self).dispatch( request, *args, **kwargs ) @@ -603,7 +600,7 @@ class CoinifyCallbackView(SingleObjectMixin, View): # make a dict with all HTTP_ headers headerdict = {} - for key, value in request.META.iteritems(): + for key, value in request.META.items(): if key[:5] == 'HTTP_': headerdict[key[5:]] = value @@ -625,7 +622,7 @@ class CoinifyCallbackView(SingleObjectMixin, View): try: coinifyinvoice = CoinifyAPIInvoice.objects.get(invoicejson__id=callbackjson['data']['id']) except CoinifyAPIInvoice.DoesNotExist: - print "unable to find CoinifyAPIInvoice with id %s" % callbackjson['data']['id'] + print("unable to find CoinifyAPIInvoice with id %s" % callbackjson['data']['id']) return HttpResponseBadRequest('bad coinifyinvoice id') # save new coinifyinvoice payload @@ -641,7 +638,7 @@ class CoinifyCallbackView(SingleObjectMixin, View): else: return HttpResponseBadRequest('unsupported event') else: - print "invalid coinify callback detected" + print("invalid coinify callback detected") return HttpResponseBadRequest('something is fucky')