Initial commit.
This commit is contained in:
commit
3b422450ac
6 changed files with 294 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
11
adapters/__init__.py
Normal file
11
adapters/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
|
||||||
|
|
||||||
|
from .google import Google
|
||||||
|
from .wordpress import Wordpress
|
||||||
|
from .wordpress import CalendarMetadata
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'Google',
|
||||||
|
'Wordpress',
|
||||||
|
'CalendarMetadata',
|
||||||
|
]
|
63
adapters/google.py
Normal file
63
adapters/google.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import tempfile
|
||||||
|
import datetime
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
import json
|
||||||
|
from google.auth.transport.requests import Request
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from googleapiclient.errors import HttpError
|
||||||
|
|
||||||
|
# If modifying these scopes, delete the file token.json.
|
||||||
|
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
||||||
|
|
||||||
|
|
||||||
|
class Google():
|
||||||
|
|
||||||
|
def __init__(self, calendar_id, *, credentials, token_file):
|
||||||
|
_, credentials_file = tempfile.mkstemp()
|
||||||
|
with open(credentials_file, 'w') as fp:
|
||||||
|
json.dump(credentials, fp)
|
||||||
|
|
||||||
|
self.calendar_id = calendar_id
|
||||||
|
self.credentials_file = credentials_file
|
||||||
|
self.token_file = token_file
|
||||||
|
self.credentials = None
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
if os.path.exists(self.token_file):
|
||||||
|
self.credentials = Credentials.from_authorized_user_file(self.token_file, SCOPES)
|
||||||
|
if self.credentials is None or not self.credentials.valid:
|
||||||
|
if self.credentials is not None \
|
||||||
|
and self.credentials.expired \
|
||||||
|
and self.credentials.refresh_token:
|
||||||
|
self.credentials.refresh(Request())
|
||||||
|
else:
|
||||||
|
flow = InstalledAppFlow.from_client_secrets_file(
|
||||||
|
self.credentials_file,
|
||||||
|
SCOPES,
|
||||||
|
)
|
||||||
|
self.credentials = flow.run_local_server(port=0)
|
||||||
|
with open(self.token_file, 'w') as token:
|
||||||
|
token.write(self.credentials.to_json())
|
||||||
|
|
||||||
|
def get_events(self, start: datetime.datetime | None=None, until: datetime.timedelta | None=None, limit=None):
|
||||||
|
until = until if until else datetime.timedelta(days=365)
|
||||||
|
now = start if start else datetime.datetime.utcnow().astimezone()
|
||||||
|
now_365 = now + until
|
||||||
|
|
||||||
|
try:
|
||||||
|
service = build('calendar', 'v3', credentials=self.credentials)
|
||||||
|
events = service.events().list(
|
||||||
|
calendarId=self.calendar_id,
|
||||||
|
timeMin=now.isoformat(),
|
||||||
|
timeMax=now_365.isoformat(),
|
||||||
|
maxResults=limit,
|
||||||
|
).execute()
|
||||||
|
except HttpError:
|
||||||
|
print('an error occured')
|
||||||
|
raise
|
||||||
|
events = events.get('items', [])
|
||||||
|
return events
|
||||||
|
|
9
adapters/utils.py
Normal file
9
adapters/utils.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import collections
|
||||||
|
|
||||||
|
|
||||||
|
def dict_merge(dct, merge_dct):
|
||||||
|
for k, v in merge_dct.items():
|
||||||
|
if (k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], dict)): #noqa
|
||||||
|
dict_merge(dct[k], merge_dct[k])
|
||||||
|
else:
|
||||||
|
dct[k] = merge_dct[k]
|
144
adapters/wordpress.py
Normal file
144
adapters/wordpress.py
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import requests
|
||||||
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from . import utils
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarMetadata():
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
id: int,
|
||||||
|
translations: dict,
|
||||||
|
):
|
||||||
|
self.name = name
|
||||||
|
self.id = id
|
||||||
|
self.translations = translations
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
translations = {
|
||||||
|
f'calendar_name_translation_{k}': v for k, v in self.translations.items()
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'calendar_name': self.name,
|
||||||
|
'calendar_id': self.id,
|
||||||
|
**translations,
|
||||||
|
}
|
||||||
|
|
||||||
|
class Wordpress():
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
*,
|
||||||
|
calendar_metadata: CalendarMetadata,
|
||||||
|
credentials: dict,
|
||||||
|
):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.credentials = credentials
|
||||||
|
self.calendar_meadata = calendar_metadata
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
login_request = self.session.post(
|
||||||
|
f'{self.base_url}/wp-login.php',
|
||||||
|
data={
|
||||||
|
'log': self.credentials['user'],
|
||||||
|
'pwd': self.credentials['password'],
|
||||||
|
'wp-submit': 'Anmelden',
|
||||||
|
'redirect_to': f'{self.base_url}/wp-admin/',
|
||||||
|
'testcookie': 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return login_request
|
||||||
|
|
||||||
|
def _datestr_to_date(self, s, tz=None):
|
||||||
|
if s.endswith('Z'):
|
||||||
|
s = s[:-1]
|
||||||
|
return datetime.datetime.fromisoformat(s).astimezone().date()
|
||||||
|
|
||||||
|
def _generate_data_single(self, event, item_id=2):
|
||||||
|
start = event['start']
|
||||||
|
end = event['end']
|
||||||
|
summary = event['summary']
|
||||||
|
|
||||||
|
day_increment = datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
if 'date' in start:
|
||||||
|
start = self._datestr_to_date(start['date'])
|
||||||
|
elif 'dateTime' in start:
|
||||||
|
start = self._datestr_to_date(start['dateTime'], tz=start['timeZone'])
|
||||||
|
else:
|
||||||
|
raise ValueError('Cannot process event')
|
||||||
|
|
||||||
|
if 'date' in end:
|
||||||
|
end = self._datestr_to_date(end['date'])
|
||||||
|
elif 'dateTime' in end:
|
||||||
|
end = self._datestr_to_date(end['dateTime'], tz=end['timeZone'])
|
||||||
|
end += day_increment # if its a time on a day, we want to add one, in order to also include it in the update
|
||||||
|
else:
|
||||||
|
raise ValueError('Cannot process event')
|
||||||
|
|
||||||
|
flow = start
|
||||||
|
dictionary = {}
|
||||||
|
while flow < end:
|
||||||
|
dictionary.setdefault(flow.year, {})
|
||||||
|
dictionary[flow.year].setdefault(flow.month, {})
|
||||||
|
dictionary[flow.year][flow.month].setdefault(flow.day, {})
|
||||||
|
dictionary[flow.year][flow.month][flow.day] = {
|
||||||
|
'description': summary,
|
||||||
|
'legend_item_id': item_id,
|
||||||
|
}
|
||||||
|
flow += day_increment
|
||||||
|
return dictionary
|
||||||
|
|
||||||
|
def _fill_empty(self, d, *, start: datetime.datetime, until: datetime.timedelta):
|
||||||
|
day_increment = datetime.timedelta(days=1)
|
||||||
|
flow = start
|
||||||
|
while flow < start + until:
|
||||||
|
d.setdefault(flow.year, {})
|
||||||
|
d[flow.year].setdefault(flow.month, {})
|
||||||
|
d[flow.year][flow.month].setdefault(flow.day, {})
|
||||||
|
d[flow.year][flow.month][flow.day].setdefault('legend_item_id', 1)
|
||||||
|
d[flow.year][flow.month][flow.day].setdefault('description', '')
|
||||||
|
flow += day_increment
|
||||||
|
return d
|
||||||
|
|
||||||
|
def _generate_data(
|
||||||
|
self,
|
||||||
|
events,
|
||||||
|
start: datetime.datetime | None=None,
|
||||||
|
until: datetime.timedelta | None=None
|
||||||
|
):
|
||||||
|
until = until if until else datetime.timedelta(days=365)
|
||||||
|
start = start if start else datetime.datetime.utcnow().astimezone()
|
||||||
|
|
||||||
|
final_dict = {}
|
||||||
|
for event in events:
|
||||||
|
data = self._generate_data_single(event)
|
||||||
|
utils.dict_merge(final_dict, data)
|
||||||
|
final_dict = self._fill_empty(
|
||||||
|
final_dict,
|
||||||
|
start=start,
|
||||||
|
until=until,
|
||||||
|
)
|
||||||
|
return final_dict
|
||||||
|
|
||||||
|
def post_events(self, events):
|
||||||
|
metadata = self.calendar_meadata.to_dict()
|
||||||
|
data = self._generate_data(events)
|
||||||
|
update_request = self.session.post(
|
||||||
|
f'{self.base_url}/wp-admin/admin-ajax.php',
|
||||||
|
auth=(self.credentials['user'], self.credentials['password']),
|
||||||
|
data={
|
||||||
|
'action': 'wpbs_save_calendar_data',
|
||||||
|
'form_data': urllib.parse.urlencode(metadata),
|
||||||
|
'calendar_data': json.dumps(data),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return update_request
|
||||||
|
|
66
main.py
Executable file
66
main.py
Executable file
|
@ -0,0 +1,66 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
try:
|
||||||
|
from yaml import CLoader as Loader
|
||||||
|
except ImportError:
|
||||||
|
from yaml import Loader
|
||||||
|
|
||||||
|
from adapters import *
|
||||||
|
|
||||||
|
class Config():
|
||||||
|
|
||||||
|
"""
|
||||||
|
The default configuration.
|
||||||
|
|
||||||
|
|
||||||
|
Keys:
|
||||||
|
.google.calendar_id: the id of the calendar to sync
|
||||||
|
.google.credentials: the json of the obtained credentials file
|
||||||
|
.google.token_file: where to save the login token
|
||||||
|
.wordpress.url: the base wordpress url
|
||||||
|
.wordpress.calendar.id: the id of the (wp-booking-system) calendar
|
||||||
|
.wordpress.calendar.name: the name of the calendar
|
||||||
|
.wordpress.calendar.translations: a dictionary of language <-> translation pairs (example: {"en": "Reservations"})
|
||||||
|
.wordpress.credentials.user: the user as which to log into wordpress
|
||||||
|
.wordpress.credentials.password: the users password
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, file):
|
||||||
|
self.file = file
|
||||||
|
self.config: dict | None = None
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
if self.file == '-':
|
||||||
|
config = yaml.load(sys.stdin, Loader=Loader)
|
||||||
|
else:
|
||||||
|
with open(self.file) as fp:
|
||||||
|
config = yaml.load(fp, Loader=Loader)
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def __getitem__(self, name):
|
||||||
|
assert self.config is not None
|
||||||
|
return self.config[name]
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
config = Config('-')
|
||||||
|
config.load()
|
||||||
|
g = Google(
|
||||||
|
config['google']['calendar_id'],
|
||||||
|
credentials=config['google']['credentials'],
|
||||||
|
token_file=config['google'].get('token_file', '~/.wp-cal-integration-google-token.json')
|
||||||
|
)
|
||||||
|
w = Wordpress(
|
||||||
|
config['wordpress']['url'],
|
||||||
|
calendar_metadata=CalendarMetadata(
|
||||||
|
id=config['wordpress']['calendar']['id'],
|
||||||
|
name=config['wordpress']['calendar']['name'],
|
||||||
|
translations=config['wordpress']['calendar']['translations'],
|
||||||
|
),
|
||||||
|
credentials=config['wordpress']['credentials'],
|
||||||
|
)
|
||||||
|
g.login()
|
||||||
|
events = g.get_events()
|
||||||
|
w.login()
|
||||||
|
w.post_events(events)
|
Loading…
Reference in a new issue