Compare commits
No commits in common. "fd5c4dad1a9b0b586f60fe2d9546362705d28e1a" and "739d3fa2b74bd2a6bf1d32697a5510285217fbd8" have entirely different histories.
fd5c4dad1a
...
739d3fa2b7
@ -1,4 +1,6 @@
|
|||||||
"""Provide an interface to countdown items."""
|
"""Provide an interface to countdown items."""
|
||||||
|
# from rest_framework.decorators import action
|
||||||
|
# from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
|
||||||
|
@ -11,6 +11,8 @@ https://docs.djangoproject.com/en/1.6/ref/settings/
|
|||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from django.urls import reverse_lazy
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
@ -39,6 +41,7 @@ INSTALLED_APPS = (
|
|||||||
'django_extensions',
|
'django_extensions',
|
||||||
'adminplus',
|
'adminplus',
|
||||||
'bootstrap3',
|
'bootstrap3',
|
||||||
|
'registration',
|
||||||
'rest_framework',
|
'rest_framework',
|
||||||
'countdown',
|
'countdown',
|
||||||
'dispatch',
|
'dispatch',
|
||||||
@ -51,6 +54,7 @@ INSTALLED_APPS = (
|
|||||||
'races',
|
'races',
|
||||||
'seen',
|
'seen',
|
||||||
'storycraft',
|
'storycraft',
|
||||||
|
'twitter',
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE = (
|
MIDDLEWARE = (
|
||||||
@ -144,6 +148,12 @@ BOOTSTRAP3 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# registration
|
||||||
|
LOGIN_REDIRECT_URL = reverse_lazy('index')
|
||||||
|
ACCOUNT_ACTIVATION_DAYS = 7
|
||||||
|
REGISTRATION_AUTO_LOGIN = True
|
||||||
|
|
||||||
|
|
||||||
# IRC module stuff
|
# IRC module stuff
|
||||||
|
|
||||||
# karma
|
# karma
|
||||||
@ -166,6 +176,11 @@ STORYCRAFT_DEFAULT_GAME_LENGTH = 20
|
|||||||
STORYCRAFT_DEFAULT_LINE_LENGTH = 140
|
STORYCRAFT_DEFAULT_LINE_LENGTH = 140
|
||||||
STORYCRAFT_DEFAULT_LINES_PER_TURN = 2
|
STORYCRAFT_DEFAULT_LINES_PER_TURN = 2
|
||||||
|
|
||||||
|
# twitter
|
||||||
|
|
||||||
|
TWITTER_CONSUMER_KEY = None
|
||||||
|
TWITTER_CONSUMER_SECRET = None
|
||||||
|
|
||||||
# weather
|
# weather
|
||||||
|
|
||||||
WEATHER_WEATHER_UNDERGROUND_API_KEY = None
|
WEATHER_WEATHER_UNDERGROUND_API_KEY = None
|
||||||
|
@ -21,5 +21,6 @@ urlpatterns = [
|
|||||||
url(r'^races/', include('races.urls')),
|
url(r'^races/', include('races.urls')),
|
||||||
url(r'^weather/', include('weather.urls')),
|
url(r'^weather/', include('weather.urls')),
|
||||||
|
|
||||||
|
url(r'^accounts/', include('registration.backends.default.urls')),
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
]
|
]
|
||||||
|
@ -4,23 +4,23 @@
|
|||||||
#
|
#
|
||||||
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
|
# pip-compile --output-file=requirements/requirements-dev.txt requirements/requirements-dev.in
|
||||||
#
|
#
|
||||||
|
appdirs==1.4.4
|
||||||
|
# via virtualenv
|
||||||
asgiref==3.2.10
|
asgiref==3.2.10
|
||||||
# via django
|
# via django
|
||||||
attrs==21.2.0
|
attrs==20.2.0
|
||||||
# via pytest
|
# via pytest
|
||||||
backports.entry-points-selectable==1.1.0
|
bandit==1.6.2
|
||||||
# via virtualenv
|
|
||||||
bandit==1.7.0
|
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
certifi==2020.6.20
|
certifi==2020.6.20
|
||||||
# via requests
|
# via requests
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
# via requests
|
# via requests
|
||||||
click==8.0.3
|
click==8.0.0
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
coverage[toml]==6.0.2
|
coverage==5.3
|
||||||
# via pytest-cov
|
# via pytest-cov
|
||||||
distlib==0.3.3
|
distlib==0.3.1
|
||||||
# via virtualenv
|
# via virtualenv
|
||||||
django-adminplus==0.5
|
django-adminplus==0.5
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
@ -28,6 +28,8 @@ django-bootstrap3==14.2.0
|
|||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
django-extensions==3.0.9
|
django-extensions==3.0.9
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
|
django-registration-redux==2.8
|
||||||
|
# via -r requirements/requirements.in
|
||||||
django==3.1.2
|
django==3.1.2
|
||||||
# via
|
# via
|
||||||
# -r requirements/requirements.in
|
# -r requirements/requirements.in
|
||||||
@ -35,29 +37,29 @@ django==3.1.2
|
|||||||
# djangorestframework
|
# djangorestframework
|
||||||
djangorestframework==3.12.1
|
djangorestframework==3.12.1
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
dlint==0.11.0
|
dlint==0.10.3
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
filelock==3.3.1
|
filelock==3.0.12
|
||||||
# via
|
# via
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
flake8-blind-except==0.2.0
|
flake8-blind-except==0.1.1
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
flake8-builtins==1.5.3
|
flake8-builtins==1.5.3
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
flake8-docstrings==1.6.0
|
flake8-docstrings==1.5.0
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
flake8-executable==2.1.1
|
flake8-executable==2.0.4
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
flake8-fixme==1.1.1
|
flake8-fixme==1.1.1
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
flake8-isort==4.1.1
|
flake8-isort==4.0.0
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
flake8-logging-format==0.6.0
|
flake8-logging-format==0.6.0
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
flake8-mutable==1.2.0
|
flake8-mutable==1.2.0
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
flake8==3.9.2
|
flake8==3.8.4
|
||||||
# via
|
# via
|
||||||
# -r requirements/requirements-dev.in
|
# -r requirements/requirements-dev.in
|
||||||
# dlint
|
# dlint
|
||||||
@ -66,9 +68,9 @@ flake8==3.9.2
|
|||||||
# flake8-executable
|
# flake8-executable
|
||||||
# flake8-isort
|
# flake8-isort
|
||||||
# flake8-mutable
|
# flake8-mutable
|
||||||
gitdb==4.0.9
|
gitdb==4.0.5
|
||||||
# via gitpython
|
# via gitpython
|
||||||
gitpython==3.1.24
|
gitpython==3.1.11
|
||||||
# via bandit
|
# via bandit
|
||||||
idna==2.10
|
idna==2.10
|
||||||
# via requests
|
# via requests
|
||||||
@ -78,7 +80,7 @@ iniconfig==1.1.1
|
|||||||
# via pytest
|
# via pytest
|
||||||
irc==15.0.6
|
irc==15.0.6
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
isort==5.9.3
|
isort==5.6.4
|
||||||
# via flake8-isort
|
# via flake8-isort
|
||||||
jaraco.classes==3.1.0
|
jaraco.classes==3.1.0
|
||||||
# via jaraco.collections
|
# via jaraco.collections
|
||||||
@ -109,43 +111,41 @@ more-itertools==8.5.0
|
|||||||
# jaraco.itertools
|
# jaraco.itertools
|
||||||
oauthlib==3.1.0
|
oauthlib==3.1.0
|
||||||
# via requests-oauthlib
|
# via requests-oauthlib
|
||||||
packaging==21.0
|
packaging==20.4
|
||||||
# via
|
# via
|
||||||
# pytest
|
# pytest
|
||||||
# tox
|
# tox
|
||||||
parsedatetime==2.6
|
parsedatetime==2.6
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
pbr==5.6.0
|
pbr==5.5.1
|
||||||
# via stevedore
|
# via stevedore
|
||||||
pep517==0.12.0
|
pep517==0.10.0
|
||||||
# via pip-tools
|
# via pip-tools
|
||||||
pip-tools==6.4.0
|
pip-tools==6.1.0
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
platformdirs==2.4.0
|
pluggy==0.13.1
|
||||||
# via virtualenv
|
|
||||||
pluggy==1.0.0
|
|
||||||
# via
|
# via
|
||||||
# pytest
|
# pytest
|
||||||
# tox
|
# tox
|
||||||
ply==3.11
|
ply==3.11
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
py==1.10.0
|
py==1.9.0
|
||||||
# via
|
# via
|
||||||
# pytest
|
# pytest
|
||||||
# tox
|
# tox
|
||||||
pycodestyle==2.7.0
|
pycodestyle==2.6.0
|
||||||
# via flake8
|
# via flake8
|
||||||
pydocstyle==6.1.1
|
pydocstyle==5.1.1
|
||||||
# via flake8-docstrings
|
# via flake8-docstrings
|
||||||
pyflakes==2.3.1
|
pyflakes==2.2.0
|
||||||
# via flake8
|
# via flake8
|
||||||
pyparsing==3.0.1
|
pyparsing==2.4.7
|
||||||
# via packaging
|
# via packaging
|
||||||
pytest-cov==3.0.0
|
pytest-cov==2.10.1
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
pytest-django==4.4.0
|
pytest-django==4.1.0
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
pytest==6.2.5
|
pytest==6.1.1
|
||||||
# via
|
# via
|
||||||
# -r requirements/requirements-dev.in
|
# -r requirements/requirements-dev.in
|
||||||
# pytest-cov
|
# pytest-cov
|
||||||
@ -162,7 +162,7 @@ pytz==2020.1
|
|||||||
# django
|
# django
|
||||||
# irc
|
# irc
|
||||||
# tempora
|
# tempora
|
||||||
pyyaml==6.0
|
pyyaml==5.3.1
|
||||||
# via bandit
|
# via bandit
|
||||||
requests-oauthlib==1.3.0
|
requests-oauthlib==1.3.0
|
||||||
# via twython
|
# via twython
|
||||||
@ -178,51 +178,45 @@ six==1.15.0
|
|||||||
# jaraco.collections
|
# jaraco.collections
|
||||||
# jaraco.logging
|
# jaraco.logging
|
||||||
# jaraco.text
|
# jaraco.text
|
||||||
|
# packaging
|
||||||
# python-dateutil
|
# python-dateutil
|
||||||
# tox
|
# tox
|
||||||
# virtualenv
|
# virtualenv
|
||||||
smmap==5.0.0
|
smmap==3.0.4
|
||||||
# via gitdb
|
# via gitdb
|
||||||
snowballstemmer==2.1.0
|
snowballstemmer==2.0.0
|
||||||
# via pydocstyle
|
# via pydocstyle
|
||||||
sqlparse==0.4.1
|
sqlparse==0.4.1
|
||||||
# via django
|
# via django
|
||||||
stevedore==3.5.0
|
stevedore==3.2.2
|
||||||
# via bandit
|
# via bandit
|
||||||
tempora==4.0.0
|
tempora==4.0.0
|
||||||
# via
|
# via
|
||||||
# irc
|
# irc
|
||||||
# jaraco.logging
|
# jaraco.logging
|
||||||
testfixtures==6.18.3
|
testfixtures==6.15.0
|
||||||
# via flake8-isort
|
# via flake8-isort
|
||||||
toml==0.10.2
|
toml==0.10.1
|
||||||
# via
|
# via
|
||||||
|
# pep517
|
||||||
# pytest
|
# pytest
|
||||||
# tox
|
# tox
|
||||||
tomli==1.2.2
|
tox-wheel==0.5.0
|
||||||
# via
|
|
||||||
# coverage
|
|
||||||
# pep517
|
|
||||||
tox-wheel==0.6.0
|
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
tox==3.24.4
|
tox==3.20.1
|
||||||
# via
|
# via
|
||||||
# -r requirements/requirements-dev.in
|
# -r requirements/requirements-dev.in
|
||||||
# tox-wheel
|
# tox-wheel
|
||||||
twython==3.8.2
|
twython==3.8.2
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
typing-extensions==3.10.0.2
|
|
||||||
# via gitpython
|
|
||||||
urllib3==1.25.11
|
urllib3==1.25.11
|
||||||
# via requests
|
# via requests
|
||||||
versioneer==0.21
|
versioneer==0.18
|
||||||
# via -r requirements/requirements-dev.in
|
# via -r requirements/requirements-dev.in
|
||||||
virtualenv==20.9.0
|
virtualenv==20.0.35
|
||||||
# via tox
|
# via tox
|
||||||
wheel==0.37.0
|
wheel==0.35.1
|
||||||
# via
|
# via tox-wheel
|
||||||
# pip-tools
|
|
||||||
# tox-wheel
|
|
||||||
zalgo-text==0.6
|
zalgo-text==0.6
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ Django # core
|
|||||||
django-adminplus # admin.site.register_view
|
django-adminplus # admin.site.register_view
|
||||||
django-bootstrap3 # bootstrap layout
|
django-bootstrap3 # bootstrap layout
|
||||||
django-extensions # more commands
|
django-extensions # more commands
|
||||||
|
django-registration-redux # registration views/forms
|
||||||
djangorestframework # dispatch WS API
|
djangorestframework # dispatch WS API
|
||||||
irc==15.0.6 # core, pinned until I can bother to update --- 17.x has API changes
|
irc==15.0.6 # core, pinned until I can bother to update --- 17.x has API changes
|
||||||
parsedatetime # relative date stuff in countdown
|
parsedatetime # relative date stuff in countdown
|
||||||
|
@ -16,6 +16,8 @@ django-bootstrap3==14.2.0
|
|||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
django-extensions==3.0.9
|
django-extensions==3.0.9
|
||||||
# via -r requirements/requirements.in
|
# via -r requirements/requirements.in
|
||||||
|
django-registration-redux==2.8
|
||||||
|
# via -r requirements/requirements.in
|
||||||
django==3.1.2
|
django==3.1.2
|
||||||
# via
|
# via
|
||||||
# -r requirements/requirements.in
|
# -r requirements/requirements.in
|
||||||
|
0
twitter/__init__.py
Normal file
0
twitter/__init__.py
Normal file
8
twitter/admin.py
Normal file
8
twitter/admin.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""Manage twitter models in the admin interface."""
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from twitter.models import TwitterClient
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(TwitterClient)
|
348
twitter/ircplugin.py
Normal file
348
twitter/ircplugin.py
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
"""Access to Twitter through bot commands."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import twython
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from ircbot.lib import Plugin, has_permission
|
||||||
|
from twitter.models import TwitterClient
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger('twitter.ircplugin')
|
||||||
|
|
||||||
|
|
||||||
|
class Twitter(Plugin):
|
||||||
|
|
||||||
|
"""Access Twitter via the bot as an authenticated client."""
|
||||||
|
|
||||||
|
def __init__(self, bot, connection, event):
|
||||||
|
"""Initialize some stuff."""
|
||||||
|
|
||||||
|
self.authed = False
|
||||||
|
self.twit_args = {'timeout': 30.0}
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
client_args=self.twit_args)
|
||||||
|
self.temp_token = None
|
||||||
|
self.temp_token_secret = None
|
||||||
|
|
||||||
|
self.poll_mentions = False
|
||||||
|
|
||||||
|
self.server = connection.server_config
|
||||||
|
|
||||||
|
super(Twitter, self).__init__(bot, connection, event)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""Prepare for oauth stuff (but don't execute it yet)."""
|
||||||
|
|
||||||
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
|
||||||
|
r'^!twitter\s+getstatus(\s+nosource)?(\s+noid)?\s+(\S+)$',
|
||||||
|
self.handle_getstatus, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'],
|
||||||
|
r'^!twitter\s+getuserstatus(\s+nosource)?(\s+noid)?\s+(\S+)(\s+.*|$)',
|
||||||
|
self.handle_getuserstatus, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+tweet\s+(.*)',
|
||||||
|
self.handle_tweet, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+gettoken$',
|
||||||
|
self.handle_gettoken, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+auth\s+(\S+)$',
|
||||||
|
self.handle_auth, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+replyto\s+(\S+)\s+(.*)',
|
||||||
|
self.handle_replyto, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+mentionpoll\s+start',
|
||||||
|
self.handle_start_mentionpoll, -20)
|
||||||
|
self.connection.reactor.add_global_regex_handler(['pubmsg', 'privmsg'], r'^!twitter\s+mentionpoll\s+stop',
|
||||||
|
self.handle_stop_mentionpoll, -20)
|
||||||
|
|
||||||
|
# try getting the stored auth tokens and logging in
|
||||||
|
try:
|
||||||
|
twittersettings = TwitterClient.objects.get(pk=1)
|
||||||
|
if twittersettings.oauth_token != '' and twittersettings.oauth_token_secret != '':
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
twittersettings.oauth_token, twittersettings.oauth_token_secret,
|
||||||
|
client_args=self.twit_args)
|
||||||
|
if self.twit.verify_credentials():
|
||||||
|
self.authed = True
|
||||||
|
log.debug("Logged in to Twitter with saved token.")
|
||||||
|
else:
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
client_args=self.twit_args)
|
||||||
|
else:
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
client_args=self.twit_args)
|
||||||
|
except TwitterClient.DoesNotExist:
|
||||||
|
log.error("twitter settings module does not exist")
|
||||||
|
|
||||||
|
super(Twitter, self).start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""Tear down handlers."""
|
||||||
|
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_getstatus)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_getuserstatus)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_tweet)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_gettoken)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_auth)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_replyto)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_start_mentionpoll)
|
||||||
|
self.connection.reactor.remove_global_regex_handler(['pubmsg', 'privmsg'], self.handle_stop_mentionpoll)
|
||||||
|
|
||||||
|
self.poll_mentions = False
|
||||||
|
|
||||||
|
super(Twitter, self).stop()
|
||||||
|
|
||||||
|
def handle_start_mentionpoll(self, connection, event, match):
|
||||||
|
if not has_permission(event.source, 'twitter.manage_threads'):
|
||||||
|
return self.bot.reply(event, "You do not have permission to start/stop threads.")
|
||||||
|
|
||||||
|
self.poll_mentions = True
|
||||||
|
t = threading.Thread(target=self.thread_watch_mentions)
|
||||||
|
t.daemon = True
|
||||||
|
t.start()
|
||||||
|
self.bot.reply(event, "Now polling for mentions.")
|
||||||
|
|
||||||
|
def handle_stop_mentionpoll(self, connection, event, match):
|
||||||
|
if not has_permission(event.source, 'twitter.manage_threads'):
|
||||||
|
return self.bot.reply(event, "You do not have permission to start/stop threads.")
|
||||||
|
|
||||||
|
self.poll_mentions = False
|
||||||
|
self.bot.reply(event, "No longer polling for mentions.")
|
||||||
|
|
||||||
|
def handle_getstatus(self, connection, event, match):
|
||||||
|
"""Get a status by tweet ID."""
|
||||||
|
|
||||||
|
print_source = True
|
||||||
|
print_id = True
|
||||||
|
if match.group(1):
|
||||||
|
print_source = False
|
||||||
|
if match.group(2):
|
||||||
|
print_id = False
|
||||||
|
status = match.group(3)
|
||||||
|
try:
|
||||||
|
tweet = self.twit.show_status(id=status)
|
||||||
|
return self._reply_with_tweet_or_retweet_text(event, tweet=tweet, print_source=print_source,
|
||||||
|
print_id=print_id)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("couldn't obtain status")
|
||||||
|
log.exception(e)
|
||||||
|
return self.bot.reply(event, "Couldn't obtain status: {0:s}".format(str(e)))
|
||||||
|
|
||||||
|
def handle_getuserstatus(self, connection, event, match):
|
||||||
|
"""Get a status for a user. Allows for getting one other than the most recent."""
|
||||||
|
|
||||||
|
print_source = True
|
||||||
|
print_id = True
|
||||||
|
if match.group(1):
|
||||||
|
print_source = False
|
||||||
|
if match.group(2):
|
||||||
|
print_id = False
|
||||||
|
user = match.group(3)
|
||||||
|
index = match.group(4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if index:
|
||||||
|
index = int(index)
|
||||||
|
if index > 0:
|
||||||
|
index = 0
|
||||||
|
else:
|
||||||
|
index = 0
|
||||||
|
except ValueError as e:
|
||||||
|
log.error("Couldn't convert index")
|
||||||
|
log.exception(e)
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
count = (-1*index) + 1
|
||||||
|
|
||||||
|
try:
|
||||||
|
tweets = self.twit.get_user_timeline(screen_name=user, count=count, include_rts=True)
|
||||||
|
if tweets:
|
||||||
|
tweet = tweets[-1*index]
|
||||||
|
return self._reply_with_tweet_or_retweet_text(event, tweet=tweet, print_source=print_source,
|
||||||
|
print_id=print_id)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("couldn't obtain status")
|
||||||
|
log.exception(e)
|
||||||
|
return self.bot.reply(event, "Couldn't obtain status: {0:s}".format(str(e)))
|
||||||
|
|
||||||
|
def handle_tweet(self, connection, event, match):
|
||||||
|
"""Tweet. Needs authentication."""
|
||||||
|
|
||||||
|
tweet = match.group(1)
|
||||||
|
if not self.twit.verify_credentials():
|
||||||
|
return self.bot.reply(event, "The bot must be authenticated to tweet.")
|
||||||
|
if not has_permission(event.source, 'twitter.send_tweets'):
|
||||||
|
return self.bot.reply(event, "You do not have permission to send tweets.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.twit.update_status(status=tweet, display_coordinates=False) is not None:
|
||||||
|
return self.bot.reply(event, "'{0:s}' tweeted.".format(tweet))
|
||||||
|
else:
|
||||||
|
return self.bot.reply(event, "Unknown error sending tweet(s).")
|
||||||
|
except Exception as e:
|
||||||
|
log.error("couldn't tweet")
|
||||||
|
log.exception(e)
|
||||||
|
return self.bot.reply(event, "Couldn't tweet: {0:s}".format(str(e)))
|
||||||
|
|
||||||
|
def handle_replyto(self, connection, event, match):
|
||||||
|
"""Reply to a tweet, in the twitter in_reply_to_status_id sense. Needs authentication."""
|
||||||
|
|
||||||
|
status_id = match.group(1)
|
||||||
|
tweet = match.group(2)
|
||||||
|
|
||||||
|
if not self.twit.verify_credentials():
|
||||||
|
return self.bot.reply(event, "The bot must be authenticated to tweet.")
|
||||||
|
if not has_permission(event.source, 'twitter.send_tweets'):
|
||||||
|
return self.bot.reply(event, "you do not have permission to send tweets.")
|
||||||
|
|
||||||
|
replyee_tweet = self.twit.show_status(id=status_id)
|
||||||
|
target = replyee_tweet['user']['screen_name']
|
||||||
|
|
||||||
|
try:
|
||||||
|
reptweet = "@{0:s} {1:s}".format(target, tweet)
|
||||||
|
if self.twit.update_status(status=reptweet, display_coordinates=False, in_reply_to_status_id=status_id) is not None:
|
||||||
|
return self.bot.reply(event, "'{0:s}' tweeted.".format(tweet))
|
||||||
|
else:
|
||||||
|
return self.bot.reply(event, "Unknown error sending tweet.")
|
||||||
|
except Exception as e:
|
||||||
|
log.error("couldn't tweet")
|
||||||
|
log.exception(e)
|
||||||
|
return self.bot.reply(event, "Couldn't tweet: {0:s}".format(str(e)))
|
||||||
|
|
||||||
|
def handle_gettoken(self, connection, event, match):
|
||||||
|
"""Get an oauth token, so that the user may authenticate the bot."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.twit.verify_credentials():
|
||||||
|
self.authed = False
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
client_args=self.twit_args)
|
||||||
|
except twython.TwythonError:
|
||||||
|
self.authed = False
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
client_args=self.twit_args)
|
||||||
|
|
||||||
|
auth = self.twit.get_authentication_tokens()
|
||||||
|
self.temp_token = auth['oauth_token']
|
||||||
|
self.temp_token_secret = auth['oauth_token_secret']
|
||||||
|
return self.bot.reply(event, "Go to the following link in your browser: {0:s} and send me the pin."
|
||||||
|
"".format(auth['auth_url']))
|
||||||
|
|
||||||
|
def handle_auth(self, connection, event, match):
|
||||||
|
"""Authenticate, given a PIN (following gettoken)."""
|
||||||
|
|
||||||
|
oauth_verifier = match.group(1)
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
self.temp_token, self.temp_token_secret,
|
||||||
|
client_args=self.twit_args)
|
||||||
|
final_step = self.twit.get_authorized_tokens(oauth_verifier)
|
||||||
|
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
final_step['oauth_token'], final_step['oauth_token_secret'],
|
||||||
|
client_args=self.twit_args)
|
||||||
|
|
||||||
|
try:
|
||||||
|
twittersettings = TwitterClient.objects.get(pk=1)
|
||||||
|
twittersettings.oauth_token = final_step['oauth_token']
|
||||||
|
twittersettings.oauth_token_secret = final_step['oauth_token_secret']
|
||||||
|
twittersettings.clean()
|
||||||
|
twittersettings.save()
|
||||||
|
|
||||||
|
if self.twit.verify_credentials():
|
||||||
|
self.authed = True
|
||||||
|
# print timeline stuff. this will set up the appropriate timer
|
||||||
|
return self.bot.reply(event, "The bot is now logged in.")
|
||||||
|
else:
|
||||||
|
self.twit = twython.Twython(settings.TWITTER_CONSUMER_KEY, settings.TWITTER_CONSUMER_SECRET,
|
||||||
|
client_args=self.twit_args)
|
||||||
|
return self.bot.reply(event, "The bot was not able to authenticate.")
|
||||||
|
except TwitterClient.DoesNotExist:
|
||||||
|
log.error("twitter settings object does not exist")
|
||||||
|
return self.bot.reply(event, "twitter module not configured")
|
||||||
|
|
||||||
|
def thread_watch_mentions(self, sleep_time=60):
|
||||||
|
"""Poll mentions from Twitter every sleep_time seconds.
|
||||||
|
|
||||||
|
:param sleep_time: second to sleep between checks
|
||||||
|
:type sleep_time: int
|
||||||
|
"""
|
||||||
|
|
||||||
|
while self.poll_mentions:
|
||||||
|
twittersettings = TwitterClient.objects.get(pk=1)
|
||||||
|
out_chan = twittersettings.mentions_output_channel.name
|
||||||
|
since_id = twittersettings.mentions_since_id
|
||||||
|
|
||||||
|
if out_chan.server != self.server:
|
||||||
|
self.poll_mentions = False
|
||||||
|
return
|
||||||
|
|
||||||
|
mentions = self.twit.get_mentions_timeline(since_id=since_id)
|
||||||
|
mentions.reverse()
|
||||||
|
for mention in mentions:
|
||||||
|
reply = self._return_tweet_or_retweet_text(tweet=mention, print_source=True)
|
||||||
|
self.bot.reply(None, reply, explicit_target=out_chan)
|
||||||
|
since_id = mention['id'] if mention['id'] > since_id else since_id
|
||||||
|
|
||||||
|
twittersettings.mentions_since_id = since_id
|
||||||
|
twittersettings.save()
|
||||||
|
|
||||||
|
time.sleep(sleep_time)
|
||||||
|
|
||||||
|
def _reply_with_tweet_or_retweet_text(self, event, tweet, print_source=False, print_id=True):
|
||||||
|
"""Do a bot.reply() with the appropriate text representation of the given tweet.
|
||||||
|
|
||||||
|
See _return_tweet_or_retweet_text for details.
|
||||||
|
|
||||||
|
:param event: the irc event to use for the reply
|
||||||
|
:type event: Event
|
||||||
|
:param tweet: the tweet (from twython) to inspect and return a string for
|
||||||
|
:type tweet: dict
|
||||||
|
:param print_source: whether or not to print the tweet's author (default False)
|
||||||
|
:type print_source: bool
|
||||||
|
:param print_id: whether or not to print the tweet's ID (default True)
|
||||||
|
:type print_id: bool
|
||||||
|
:returns: tweet text suitable for printing
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.bot.reply(event, self._return_tweet_or_retweet_text(tweet, print_source, print_id))
|
||||||
|
|
||||||
|
def _return_tweet_or_retweet_text(self, tweet, print_source=False, print_id=True):
|
||||||
|
"""Return a string of the author and text body of a status, accounting for whether
|
||||||
|
or not the fetched status is a retweet.
|
||||||
|
|
||||||
|
:param tweet: the tweet (from twython) to inspect and return a string for
|
||||||
|
:type tweet: dict
|
||||||
|
:param print_source: whether or not to print the tweet's author (default False)
|
||||||
|
:type print_source: bool
|
||||||
|
:param print_id: whether or not to print the tweet's ID (default True)
|
||||||
|
:type print_id: bool
|
||||||
|
:returns: tweet text suitable for printing
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
|
||||||
|
retweet = getattr(tweet, 'retweeted_status', None)
|
||||||
|
if retweet:
|
||||||
|
if print_source:
|
||||||
|
reply = "@%s (RT @%s): %s" % (tweet['user']['screen_name'],
|
||||||
|
retweet['user']['screen_name'],
|
||||||
|
self._unencode_xml(retweet['text']))
|
||||||
|
else:
|
||||||
|
reply = "(RT @%s): %s" % (retweet['user']['screen_name'],
|
||||||
|
self._unencode_xml(retweet['text']))
|
||||||
|
else:
|
||||||
|
if print_source:
|
||||||
|
reply = "@%s: %s" % (tweet['user']['screen_name'],
|
||||||
|
self._unencode_xml(tweet['text']))
|
||||||
|
else:
|
||||||
|
reply = "%s" % (self._unencode_xml(tweet['text']))
|
||||||
|
|
||||||
|
if print_id:
|
||||||
|
reply = reply + " [{0:d}]".format(tweet['id'])
|
||||||
|
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
plugin = Twitter
|
22
twitter/migrations/0001_initial.py
Normal file
22
twitter/migrations/0001_initial.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TwitterClient',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||||
|
('since_id', models.PositiveIntegerField()),
|
||||||
|
('output_channel', models.CharField(default='', max_length=200)),
|
||||||
|
('oauth_token', models.CharField(default='', max_length=256)),
|
||||||
|
('oauth_token_secret', models.CharField(default='', max_length=256)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
28
twitter/migrations/0002_auto_20150616_2022.py
Normal file
28
twitter/migrations/0002_auto_20150616_2022.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('twitter', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='twitterclient',
|
||||||
|
name='oauth_token',
|
||||||
|
field=models.CharField(default='', max_length=256, blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='twitterclient',
|
||||||
|
name='oauth_token_secret',
|
||||||
|
field=models.CharField(default='', max_length=256, blank=True),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='twitterclient',
|
||||||
|
name='output_channel',
|
||||||
|
field=models.CharField(default='', max_length=200, blank=True),
|
||||||
|
),
|
||||||
|
]
|
17
twitter/migrations/0003_auto_20150620_0951.py
Normal file
17
twitter/migrations/0003_auto_20150620_0951.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('twitter', '0002_auto_20150616_2022'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='twitterclient',
|
||||||
|
options={'permissions': (('send_tweets', 'Can send tweets via IRC'),)},
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,22 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ircbot', '0014_auto_20160116_1955'),
|
||||||
|
('twitter', '0003_auto_20150620_0951'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='twitterclient',
|
||||||
|
name='output_channel',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='twitterclient',
|
||||||
|
name='mentions_output_channel',
|
||||||
|
field=models.ForeignKey(blank=True, related_name='mentions_twitter_client', null=True, to='ircbot.IrcChannel', default=None, on_delete=models.CASCADE),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('twitter', '0004_add_mentions_output_channel_to_config'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='twitterclient',
|
||||||
|
name='since_id',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='twitterclient',
|
||||||
|
name='mentions_since_id',
|
||||||
|
field=models.PositiveIntegerField(default=1, blank=True),
|
||||||
|
),
|
||||||
|
]
|
16
twitter/migrations/0006_add_manage_threads_permission.py
Normal file
16
twitter/migrations/0006_add_manage_threads_permission.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('twitter', '0005_replace_since_id_with_replies_specific_one'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='twitterclient',
|
||||||
|
options={'permissions': (('send_tweets', 'Can send tweets via IRC'), ('manage_threads', 'Can start/stop polling threads via IRC'))},
|
||||||
|
),
|
||||||
|
]
|
0
twitter/migrations/__init__.py
Normal file
0
twitter/migrations/__init__.py
Normal file
28
twitter/models.py
Normal file
28
twitter/models.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
"""Twitter settings models."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from ircbot.models import IrcChannel
|
||||||
|
|
||||||
|
|
||||||
|
log = logging.getLogger('twitter.models')
|
||||||
|
|
||||||
|
|
||||||
|
class TwitterClient(models.Model):
|
||||||
|
|
||||||
|
"""Track twitter settings and similar."""
|
||||||
|
|
||||||
|
oauth_token = models.CharField(max_length=256, default='', blank=True)
|
||||||
|
oauth_token_secret = models.CharField(max_length=256, default='', blank=True)
|
||||||
|
|
||||||
|
mentions_output_channel = models.ForeignKey(IrcChannel, related_name='mentions_twitter_client', default=None,
|
||||||
|
null=True, blank=True, on_delete=models.CASCADE)
|
||||||
|
mentions_since_id = models.PositiveIntegerField(default=1, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
permissions = (
|
||||||
|
('send_tweets', "Can send tweets via IRC"),
|
||||||
|
('manage_threads', "Can start/stop polling threads via IRC"),
|
||||||
|
)
|
Loading…
x
Reference in New Issue
Block a user