Initial commit

This commit is contained in:
Sijmen 2017-11-10 16:50:11 +01:00
commit 7427007520
7 changed files with 837 additions and 0 deletions

246
.gitignore vendored Normal file
View file

@ -0,0 +1,246 @@
# Created by https://www.gitignore.io/api/vim,macos,emacs,python,sublimetext
### Emacs ###
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/
dist/
# Flycheck
flycheck_*.el
# server auth directory
/server/
# projectiles files
.projectile
projectile-bookmarks.eld
# directory configuration
.dir-locals.el
# saveplace
places
# url cache
url/cache/
# cedet
ede-projects.el
# smex
smex-items
# company-statistics
company-statistics-cache.el
# anaconda-mode
anaconda-mode/
### macOS ###
*.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
### SublimeText ###
# cache files for sublime text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
# workspace files are user-specific
*.sublime-workspace
# project files should be checked into the repository, unless a significant
# proportion of contributors will probably not be using SublimeText
# *.sublime-project
# sftp configuration file
sftp-config.json
# Package control specific files
Package Control.last-run
Package Control.ca-list
Package Control.ca-bundle
Package Control.system-ca-bundle
Package Control.cache/
Package Control.ca-certs/
Package Control.merged-ca-bundle
Package Control.user-ca-bundle
oscrypto-ca-bundle.crt
bh_unicode_properties.cache
# Sublime-github package stores a github token in this file
# https://packagecontrol.io/packages/sublime-github
GitHub.sublime-settings
### Vim ###
# swap
[._]*.s[a-v][a-z]
[._]*.sw[a-p]
[._]s[a-v][a-z]
[._]sw[a-p]
# session
Session.vim
# temporary
.netrwhist
# auto-generated tag files
tags
config.py
# End of https://www.gitignore.io/api/vim,macos,emacs,python,sublimetext

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "telewalrus"]
path = telewalrus
url = git@github.com:SijmenSchoon/telewalrus.git

102
api.py Normal file
View file

@ -0,0 +1,102 @@
import urllib.parse
import aiohttp
SCHEME = 'http'
NETLOC = 'localhost:5000'
class ApiError(Exception): pass
class BadRequestError(ApiError): pass
class PermissionDeniedError(ApiError): pass
class NotFoundError(ApiError): pass
class InternalServerError(ApiError): pass
def build_url(path, query_args=None):
query = urllib.parse.urlencode(query_args if query_args else {})
parse_result = urllib.parse.ParseResult(
scheme=SCHEME, netloc=NETLOC, path=path,
params='', query=query, fragment='')
return urllib.parse.urlunparse(parse_result)
def check_status(status):
if status == 400:
raise BadRequestError
elif status == 403:
raise PermissionDeniedError
elif status == 404:
raise NotFoundError
elif status == 500:
raise InternalServerError
async def get_json(url, token=None):
headers = {'Authorization': f'Bearer {token}'} if token else {}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as resp:
check_status(resp.status)
return await resp.json()
async def post_json(url, obj, token=None):
headers = {'Authorization': f'Bearer {token}'} if token else {}
async with aiohttp.ClientSession() as session:
async with session.post(url, json=obj, headers=headers) as resp:
check_status(resp.status)
return await resp.json()
async def put_json(url, obj, token=None):
headers = {'Authorization': f'Bearer {token}'} if token else {}
async with aiohttp.ClientSession() as session:
async with session.put(url, json=obj, headers=headers) as resp:
check_status(resp.status)
return await resp.json()
async def get_tasks(token, group_id=None):
args = {'group_id': group_id} if group_id else {}
url = build_url('/pimpy/api/tasks/', args)
return await get_json(url, token=token)
async def get_group_tasks(token, group_id):
url = build_url(f'/pimpy/api/groups/{group_id:d}/tasks/')
return await get_json(url, token=token)
async def get_group_user_tasks(token, group_id, user_id='me'):
url = build_url(f'/pimpy/api/groups/{group_id:d}/users/{user_id}/tasks/')
return await get_json(url, token=token)
async def get_group_task(token, group_id, task_id):
url = build_url(f'/pimpy/api/groups/{group_id:d}/tasks/{task_id:d}/')
return await get_json(url, token=token)
async def add_group_task(token, group_id, owners, title):
url = build_url(f'/pimpy/api/groups/{group_id:d}/tasks/')
obj = {'owners': owners, 'title': title}
return await post_json(url, obj, token=token)
async def get_group_users(token, group_id):
url = build_url(f'/pimpy/api/groups/{group_id:d}/users/')
return await get_json(url, token=token)
async def get_task(token, task_id):
url = build_url(f'/pimpy/api/tasks/{task_id:d}/')
return await get_json(url, token=token)
async def set_group_task_status(token, group_id, task_id, status):
url = build_url(f'/pimpy/api/groups/{group_id:d}/tasks/{task_id:d}/status/')
return await put_json(url, {'status': status}, token=token)
async def set_task_status(token, task_id, status):
url = build_url(f'/pimpy/api/tasks/{task_id:d}/status/')
return await put_json(url, {'status': status}, token=token)

326
app.py Normal file
View file

@ -0,0 +1,326 @@
#!/usr/bin/env python3
import json
import re
from collections import defaultdict
from math import ceil
import baas32
import telewalrus.bot
import api
import messages
from config import VIA_USERS, VIA_GROUPS, TG_TOKEN, USER_TOKENS
BOT = telewalrus.bot.Bot(TG_TOKEN)
@BOT.command('start')
async def cmd_start(message):
name = message.from_user.first_name
if message.from_user.id not in VIA_USERS:
await message.chat.message(messages.stranger_message(name))
return
await message.chat.message(
f'Heya, {name}! Zie /tasks om te zien welke taken je open hebt staan.')
@BOT.command('chatinfo')
async def cmd_chatinfo(message):
chat = message.chat
if message.chat.type == 'private':
admins = []
else:
admins = [admin.user for admin in await chat.administrators()]
msg = f'''
id: {chat.id}
type: {chat.type}
title: {chat.title}
username: {chat.username}
first_name: {chat.first_name}
last_name: {chat.last_name}
admins: {admins}
'''
await message.chat.message(msg)
@BOT.command('tasks')
async def cmd_tasks(message):
token = USER_TOKENS.get(message.from_user.id)
if not token:
msg = messages.stranger_message(message.from_user.first_name)
await message.chat.message(msg)
return
is_group = message.chat.type != 'private'
if not is_group:
tasks = await api.get_tasks(token)
else:
group_id = VIA_GROUPS.get(message.chat.id)
if group_id is None:
await message.chat.message(
'pimpy is nog niet ingeschakeld voor deze groep :/')
return
tasks = await api.get_group_user_tasks(token, group_id)
print(json.dumps(tasks, indent=4))
msg = messages.tasks_message(tasks, is_group)
await message.chat.message(msg, parse_mode='HTML')
@BOT.command('grouptasks')
async def cmd_grouptasks(message):
token = USER_TOKENS.get(message.from_user.id)
if not token:
msg = messages.stranger_message(message.from_user.first_name)
await message.chat.message(msg)
return
if message.chat.type == 'private':
await message.chat.message(
'Dit commando werkt alleen in commissiechats.')
return
group_id = VIA_GROUPS.get(message.chat.id)
if not group_id:
await message.chat.message(
'pimpy is nog niet ingeschakeld voor deze groep :/')
return
user_tasks = await api.get_group_tasks(token, group_id)
msg = ''
for i, (name, tasks) in enumerate(user_tasks.items()):
status = defaultdict(int)
for task in tasks:
status[task['status']] += 1
msg += f'<b>{name}</b>:\n' \
f'{status["Niet begonnen"]}, ▶️ {status["Begonnen"]}, ' \
f'{status["Done"]}, ❌ {status["Niet Done"]}\n'
users = await api.get_group_users(token, group_id)
keyboard = []
for i, user in enumerate(users):
if i % 3 == 0:
keyboard.append([])
keyboard[-1].append({
'text': user['name'],
'callback_data': f'tasks {user["id"]} {user["name"]}'
})
msg += '\nKlik op een naam hieronder om zijn/haar taken weer te geven.'
reply_markup = {'inline_keyboard': keyboard}
await message.chat.message(msg, parse_mode='HTML',
disable_web_page_preview=True,
reply_markup=json.dumps(reply_markup))
async def get_task_from_args(token, message, group_id=None):
task_code = baas32.normalize(message.args)
if not task_code:
await message.chat.message(
'Welke taak? Protip: zet de taakcode achter het commando.')
return
try:
task_id = baas32.decode(task_code)
except ValueError:
await message.chat.message(f'{task_code} is geen geldige taakcode.')
return
try:
if group_id:
task = await api.get_group_task(token, group_id, task_id)
else:
task = await api.get_task(token, task_id)
except api.NotFoundError:
await message.chat.message(f'Kan taak {task_code} niet vinden :(')
return
except api.PermissionDeniedError:
await message.chat.message(
f'Je hebt geen rechten voor taak {task_code}.')
return
return task, task_code
@BOT.command('task')
async def cmd_task(message):
token = USER_TOKENS.get(message.from_user.id)
if not token:
msg = messages.stranger_message(message.from_user.first_name)
await message.chat.message(msg)
return
group_id = None
if message.chat.type != 'private':
group_id = VIA_GROUPS.get(message.chat.id)
if not group_id:
await message.chat.message(
'pimpy is nog niet ingeschakeld voor deze groep :/')
return
task, _ = await get_task_from_args(token, message, group_id)
if not task:
return
msg, reply_markup = messages.task_message(task, group_id is not None)
await message.chat.message(msg, parse_mode='HTML',
reply_markup=json.dumps(reply_markup),
disable_web_page_preview=True)
@BOT.command('done')
async def cmd_done(message):
user_id = VIA_USERS.get(message.from_user.id)
if not user_id:
msg = messages.stranger_message(message.from_user.first_name)
await message.chat.message(msg)
return
task, task_code = await get_task_from_args(message)
if not task:
return
if not user_id in (user['id'] for user in task['users']):
msg = f'Je bent geen eigenaar van de taak {task_code}!'
await message.chat.message(msg, parse_mode='HTML')
return
await api.set_task_status(task['id'], 'done')
msg = f'Taak {task_code} staat nu op done!'
await message.chat.message(msg)
@BOT.command('addtask')
async def cmd_addtask(message):
user_id = VIA_USERS.get(message.from_user.id)
if not user_id:
msg = messages.stranger_message(message.from_user.first_name)
await message.chat.message(msg)
return
match = re.match(r'^([^"\' ]+|["\'][^"\']+["\']) (.*)$', message.args)
group = match.group(1).strip('\'"')
title = match.group(2).strip('\'"')
await message.chat.message(f'Groep: {group}\nTitel: {title}')
@BOT.command('actie')
async def cmd_actie(message):
if message.chat.type == 'private':
await message.chat.message(
'Deze functie werkt alleen in commissiechats.')
return
group_id = VIA_GROUPS.get(message.chat.id)
if not group_id:
await message.chat.message(
'pimpy is nog niet ingeschakeld voor deze groep :/')
return
match = re.match(r'^([^:]+): (.*)$', message.args)
if not match:
match = re.match(r'^([^ ]+) (.*)$', message.args)
if match:
owner = match.group(1)
title = match.group(2)
await message.chat.message(
f'Incorrecte syntax. Misschien bedoelde je /actie {owner}: {title}?')
else:
await message.chat.message(
f'Incorrecte syntax. Probeer eens /actie [naam]: [titel].')
return
task = await api.add_group_task(group_id, match.group(1), match.group(2))
msg, reply_markup = messages.task_message(task, None, True)
task_code = baas32.encode(task['id'])
msg = f'Taak <code>[{task_code}]</code> aangemaakt!\n\n' + msg
await message.chat.message(msg, parse_mode='HTML',
reply_markup=json.dumps(reply_markup),
disable_web_page_preview=True)
async def callback_status(query, _, args):
token = USER_TOKENS.get(query.from_user.id)
if not token:
msg = messages.stranger_message(message.from_user.first_name)
await message.chat.message(msg)
return
status, task_id = args
task_id = int(task_id)
if query.message.chat.type == 'private':
await api.set_task_status(token, task_id, status)
task = await api.get_task(token, task_id)
else:
group_id = VIA_GROUPS.get(query.message.chat.id)
if not group_id:
return
await api.set_group_task_status(token, group_id, task_id, status)
task = await api.get_group_task(token, group_id, task_id)
msg, reply_markup = messages.task_message(task, False)
await query.message.edit(msg, parse_mode='HTML',
reply_markup=json.dumps(reply_markup),
disable_web_page_preview=True)
await query.answer()
async def callback_tasks(query, _, args):
token = USER_TOKENS.get(query.from_user.id)
if not token:
msg = messages.stranger_message(message.from_user.first_name)
await message.chat.message(msg)
return
group_id = VIA_GROUPS.get(query.message.chat.id)
if not group_id:
await query.answer()
return
user_id = int(args[0])
user_name = ' '.join(args[1:])
tasks = await api.get_group_user_tasks(token, group_id, user_id)
print(json.dumps(tasks, indent=4))
msg = messages.tasks_message(tasks, True, user_name)
await query.message.chat.message(msg, parse_mode='HTML')
CALLBACK_HANDLERS = {
'status': callback_status,
'tasks': callback_tasks
}
@BOT.callback
async def callback(query):
command, *args = query.data.split(' ')
handler = CALLBACK_HANDLERS.get(command)
if handler:
await handler(query, command, args)
while True:
try:
BOT.run()
except KeyboardInterrupt:
print('\nhee doei hè')
break
except:
pass

153
messages.py Normal file
View file

@ -0,0 +1,153 @@
from datetime import datetime
from collections import defaultdict
import random
import locale
import baas32
locale.setlocale(locale.LC_TIME, 'nl_NL')
STATUS_EMOJI = {
'Niet begonnen': '',
'Begonnen': '▶️',
'Done': '',
'Niet Done': ''
}
def stranger_message(name):
return f'''
Heya, {name}! Cool dat je even komt kijken!
Voor nu is deze bot nog even afgesloten voor het publiek,
maar kom later vooral een keertje terug.
Joe!'''
def me_message(user_id):
return f'''
Dit weet ik over je:
svia.nl user_id: {user_id}
'''
def tasks_message(tasks, is_group=False, user_name=None):
groups = defaultdict(list)
for task in tasks:
groups[task['group']['id']].append(task)
if not user_name:
user_name = 'Je'
else:
user_name += '\'s'
if is_group:
msg = f'<strong>{user_name} taken voor deze groep:</strong>\n\n'
else:
msg = f'<strong>{user_name} taken:</strong>\n\n'
for _, group_tasks in groups.items():
if not group_tasks:
continue
group_name = group_tasks[0]['group']['name'] if not is_group else None
msg += taskset_message(group_name, group_tasks)
random_task = baas32.encode(random.choice(tasks)['id'])
msg += f'Gebruik /task &lt;task_id&gt; voor meer informatie. ' \
f'Bijvoorbeeld: /task {random_task}'
return msg
def taskset_message(name, tasks):
msg = f'<strong>{name}:</strong>\n' if name else ''
for task in tasks:
task_code = baas32.encode(task['id'])
emoji = STATUS_EMOJI[task['status']]
if len(task['users']) == 2:
emoji += ' 👨‍👦'
elif len(task['users']) == 3:
emoji += ' 👨‍👧‍👦'
elif len(task['users']) > 3:
emoji += ' 👨‍👩‍👧‍👧'
msg += f'• <code>[{task_code}]</code> ' \
f'{emoji} {task["title"].strip()}'
msg += '\n'
msg += '\n'
return msg
def task_message(task, is_group):
task_code = baas32.encode(task['id'])
msg = f'<code>[{task_code}]</code> <strong>{task["title"]}</strong>\n'
timestamp = datetime.strptime(task["timestamp"], "%Y-%m-%dT%H:%M:%S")
msg += f'<em>{timestamp.strftime("%d %B %Y, %H:%M")}</em>\n\n'
# Print the task group
msg += f'<strong>Groep:</strong> {task["group"]["name"]}\n'
# Print the task state
msg += f'<strong>Status:</strong> {task["status"]}\n'
# Print task owner(s)
users = task['users']
if not users:
msg += f'<em>Geen eigenaren</em>\n'
elif len(users) == 1:
msg += f'<strong>Eigenaar:</strong> {users[0]["name"]}\n'
elif 1 < len(users) <= 2:
msg += f'<strong>Eigenaren:</strong> ' \
f'{users[0]["name"]} en {users[1]["name"]}\n'
else:
msg += '\n<strong>Eigenaren:</strong>\n'
for user in task['users']:
msg += f'{user["name"]}\n'
msg += '\n'
# Print the description, if available
try:
msg += f'<strong>Beschrijving:</strong>\n{task["content"]}\n\n'
except KeyError:
pass
# Print the minute URL, if available
try:
minute = task['minute']
minute_url = f'http://svia.nl/pimpy/minutes/single/{minute["id"]}/'
minute_url += str(minute['line']) if 'line' in minute else ''
msg += f'<a href="{minute_url}">Bijbehorende notulen</a>\n'
except KeyError:
msg += f'<em>Geen bijbehorende notulen</em>\n'
keyboard = []
if task['status'] != 'Niet begonnen':
keyboard.append({
'text': '⏸ Niet begonnen',
'callback_data': f'status unstarted {task["id"]}'
})
if task['status'] != 'Begonnen':
keyboard.append({
'text': '▶️ Begonnen',
'callback_data': f'status started {task["id"]}'
})
if task['status'] != 'Done':
keyboard.append({
'text': '✅ Done',
'callback_data': f'status done {task["id"]}'
})
if task['status'] != 'Niet Done':
keyboard.append({
'text': '❌ Niet Done',
'callback_data': f'status notdone {task["id"]}'
})
reply_markup = {'inline_keyboard': [keyboard]}
return msg, reply_markup

6
requirements.txt Normal file
View file

@ -0,0 +1,6 @@
aiohttp==2.3.2
async-timeout==2.0.0
baas32==0.3.2
chardet==3.0.4
multidict==3.3.2
yarl==0.13.0

1
telewalrus Submodule

@ -0,0 +1 @@
Subproject commit 244c7ed6e6658dfb09dbae45446e132f9bc144c5