Compare commits
11 Commits
2f9203cac2
...
9312586f18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9312586f18 | ||
|
|
0d54243e9d | ||
|
|
019a2d4ede | ||
|
|
9f50f201df | ||
|
|
7d5403b4af | ||
|
|
a58e6d10a6 | ||
|
|
96fa1f58f5 | ||
|
|
6a72809817 | ||
|
|
ed669f8561 | ||
|
|
9ab90fef5a | ||
|
|
1de5bcd369 |
255
.gitignore
vendored
Normal file
255
.gitignore
vendored
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the executable, and should not be committed.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# Node.js
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
|
||||||
|
# Google Service Account credentials
|
||||||
|
service-account.json
|
||||||
|
token.pickle
|
||||||
|
credentials.json
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
public
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Invoke
|
||||||
|
.inv
|
||||||
|
|
||||||
|
# NPM cache
|
||||||
|
.npm-cache
|
||||||
92
GOOGLE_SETUP_README.md
Normal file
92
GOOGLE_SETUP_README.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Google Service Account Setup für Advoware Calendar Sync
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
Dieser Calendar Sync verwendet **ausschließlich Google Service Accounts** für die Authentifizierung. Kein OAuth, kein Browser-Interaktion - perfekt für Server-Umgebungen!
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
- Google Cloud Console Zugang
|
||||||
|
- Berechtigung zum Erstellen von Service Accounts
|
||||||
|
- (Optional) Google Workspace Admin Zugang für Domain-wide Delegation
|
||||||
|
|
||||||
|
## Schritt 1: Google Cloud Console aufrufen
|
||||||
|
1. Gehen Sie zu: https://console.cloud.google.com/
|
||||||
|
2. Melden Sie sich mit Ihrem Google-Konto an
|
||||||
|
3. Wählen Sie ein bestehendes Projekt aus oder erstellen Sie ein neues
|
||||||
|
|
||||||
|
## Schritt 2: Google Calendar API aktivieren
|
||||||
|
1. Klicken Sie auf "APIs & Dienste" → "Bibliothek"
|
||||||
|
2. Suchen Sie nach "Google Calendar API"
|
||||||
|
3. Klicken Sie auf "Google Calendar API" → "Aktivieren"
|
||||||
|
|
||||||
|
## Schritt 3: Service Account erstellen
|
||||||
|
1. Gehen Sie zu "IAM & Verwaltung" → "Service-Konten"
|
||||||
|
2. Klicken Sie auf "+ Service-Konto erstellen"
|
||||||
|
3. Grundlegende Informationen:
|
||||||
|
- **Service-Kontoname**: `advoware-calendar-sync`
|
||||||
|
- **Beschreibung**: `Service Account für Advoware-Google Calendar Synchronisation`
|
||||||
|
- **E-Mail**: wird automatisch generiert
|
||||||
|
4. Klicken Sie auf "Erstellen und fortfahren"
|
||||||
|
|
||||||
|
## Schritt 4: Berechtigungen zuweisen
|
||||||
|
1. **Rolle zuweisen**: Wählen Sie eine der folgenden Optionen:
|
||||||
|
- Für volle Zugriffe: `Editor`
|
||||||
|
- Für eingeschränkte Zugriffe: `Calendar API Admin` (falls verfügbar)
|
||||||
|
2. Klicken Sie auf "Fertig"
|
||||||
|
|
||||||
|
## Schritt 5: JSON-Schlüssel erstellen und installieren
|
||||||
|
1. Klicken Sie auf das neu erstellte Service-Konto
|
||||||
|
2. Gehen Sie zum Tab "Schlüssel"
|
||||||
|
3. Klicken Sie auf "Schlüssel hinzufügen" → "Neuen Schlüssel erstellen"
|
||||||
|
4. Wählen Sie "JSON" als Schlüsseltyp
|
||||||
|
5. Klicken Sie auf "Erstellen"
|
||||||
|
6. Die JSON-Datei wird automatisch heruntergeladen
|
||||||
|
7. **Benennen Sie die Datei um zu: `service-account.json`**
|
||||||
|
8. **Kopieren Sie die Datei nach: `/opt/motia-app/service-account.json`**
|
||||||
|
9. **Stellen Sie sichere Berechtigungen ein:**
|
||||||
|
```bash
|
||||||
|
chmod 600 /opt/motia-app/service-account.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schritt 6: Domain-wide Delegation (nur für Google Workspace)
|
||||||
|
Falls Sie Google Workspace verwenden und auf Kalender anderer Benutzer zugreifen möchten:
|
||||||
|
|
||||||
|
1. Gehen Sie zurück zum Service-Konto
|
||||||
|
2. Aktivieren Sie "Google Workspace Domain-wide Delegation"
|
||||||
|
3. Notieren Sie sich die "Unique ID" des Service-Kontos
|
||||||
|
4. Gehen Sie zu Google Admin Console: https://admin.google.com/
|
||||||
|
5. "Sicherheit" → "API-Berechtigungen"
|
||||||
|
6. "Domain-wide Delegation" → "API-Clienten verwalten"
|
||||||
|
7. Fügen Sie die Unique ID hinzu
|
||||||
|
8. Berechtigungen: `https://www.googleapis.com/auth/calendar`
|
||||||
|
|
||||||
|
## Schritt 7: Testen
|
||||||
|
Nach dem Setup können Sie den Calendar Sync testen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Vollständige Termindetails synchronisieren
|
||||||
|
curl -X POST http://localhost:3000/api/flows/advoware_cal_sync \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"full_content": true}'
|
||||||
|
|
||||||
|
# Nur "blocked" Termine synchronisieren (weniger Details)
|
||||||
|
curl -X POST http://localhost:3000/api/flows/advoware_cal_sync \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"full_content": false}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Hinweise
|
||||||
|
- ✅ **Kein Browser nötig** - läuft komplett server-seitig
|
||||||
|
- ✅ **Automatisch** - einmal setup, läuft für immer
|
||||||
|
- ✅ **Sicher** - Service Accounts haben granulare Berechtigungen
|
||||||
|
- ✅ **Skalierbar** - perfekt für Produktionsumgebungen
|
||||||
|
|
||||||
|
## Fehlerbehebung
|
||||||
|
- **"service-account.json nicht gefunden"**: Überprüfen Sie den Pfad `/opt/motia-app/service-account.json`
|
||||||
|
- **"Access denied"**: Überprüfen Sie die Berechtigungen des Service Accounts
|
||||||
|
- **"API not enabled"**: Stellen Sie sicher, dass Calendar API aktiviert ist
|
||||||
|
- **"Invalid credentials"**: Überprüfen Sie die service-account.json Datei
|
||||||
|
|
||||||
|
## Sicherheit
|
||||||
|
- Halten Sie die `service-account.json` Datei sicher und versionieren Sie sie nicht
|
||||||
|
- Verwenden Sie IAM-Rollen in GCP-Umgebungen statt JSON-Keys
|
||||||
|
- Rotiere Service Account Keys regelmäßig
|
||||||
1
bitbylaw/__init__.py
Normal file
1
bitbylaw/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Bitbylaw project
|
||||||
19
bitbylaw/check_db.py
Normal file
19
bitbylaw/check_db.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import asyncio
|
||||||
|
import asyncpg
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
async def check_db():
|
||||||
|
conn = await asyncpg.connect(
|
||||||
|
host=Config.POSTGRES_HOST or 'localhost',
|
||||||
|
user=Config.POSTGRES_USER,
|
||||||
|
password=Config.POSTGRES_PASSWORD,
|
||||||
|
database=Config.POSTGRES_DB_NAME,
|
||||||
|
timeout=10
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
row = await conn.fetchrow('SELECT * FROM calendar_sync WHERE sync_id = $1', '1329fa1f-9de5-49dc-95c6-a13525f315c5')
|
||||||
|
print('DB Row:', dict(row) if row else 'No row found')
|
||||||
|
finally:
|
||||||
|
await conn.close()
|
||||||
|
|
||||||
|
asyncio.run(check_db())
|
||||||
@@ -9,6 +9,7 @@ class Config:
|
|||||||
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
REDIS_HOST = os.getenv('REDIS_HOST', 'localhost')
|
||||||
REDIS_PORT = int(os.getenv('REDIS_PORT', '6379'))
|
REDIS_PORT = int(os.getenv('REDIS_PORT', '6379'))
|
||||||
REDIS_DB_ADVOWARE_CACHE = int(os.getenv('REDIS_DB_ADVOWARE_CACHE', '1'))
|
REDIS_DB_ADVOWARE_CACHE = int(os.getenv('REDIS_DB_ADVOWARE_CACHE', '1'))
|
||||||
|
REDIS_DB_CALENDAR_SYNC = int(os.getenv('REDIS_DB_CALENDAR_SYNC', '2'))
|
||||||
REDIS_TIMEOUT_SECONDS = int(os.getenv('REDIS_TIMEOUT_SECONDS', '5'))
|
REDIS_TIMEOUT_SECONDS = int(os.getenv('REDIS_TIMEOUT_SECONDS', '5'))
|
||||||
|
|
||||||
# Advoware API settings
|
# Advoware API settings
|
||||||
@@ -22,4 +23,17 @@ class Config:
|
|||||||
ADVOWARE_ROLE = int(os.getenv('ADVOWARE_ROLE', '2'))
|
ADVOWARE_ROLE = int(os.getenv('ADVOWARE_ROLE', '2'))
|
||||||
ADVOWARE_PASSWORD = os.getenv('ADVOWARE_PASSWORD', 'your_password')
|
ADVOWARE_PASSWORD = os.getenv('ADVOWARE_PASSWORD', 'your_password')
|
||||||
ADVOWARE_TOKEN_LIFETIME_MINUTES = int(os.getenv('ADVOWARE_TOKEN_LIFETIME_MINUTES', '55'))
|
ADVOWARE_TOKEN_LIFETIME_MINUTES = int(os.getenv('ADVOWARE_TOKEN_LIFETIME_MINUTES', '55'))
|
||||||
ADVOWARE_API_TIMEOUT_SECONDS = int(os.getenv('ADVOWARE_API_TIMEOUT_SECONDS', '30'))
|
ADVOWARE_API_TIMEOUT_SECONDS = int(os.getenv('ADVOWARE_API_TIMEOUT_SECONDS', '30'))
|
||||||
|
|
||||||
|
# Google Calendar API settings (Service Account only)
|
||||||
|
GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH = os.getenv('GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH', 'service-account.json')
|
||||||
|
GOOGLE_CALENDAR_SCOPES = ['https://www.googleapis.com/auth/calendar']
|
||||||
|
|
||||||
|
# PostgreSQL settings for Calendar Sync Hub
|
||||||
|
POSTGRES_HOST = os.getenv('POSTGRES_HOST', 'localhost')
|
||||||
|
POSTGRES_USER = os.getenv('POSTGRES_USER', 'calendar_sync_user')
|
||||||
|
POSTGRES_PASSWORD = os.getenv('POSTGRES_PASSWORD', 'default_password')
|
||||||
|
POSTGRES_DB_NAME = os.getenv('POSTGRES_DB_NAME', 'calendar_sync_db')
|
||||||
|
|
||||||
|
# Calendar Sync settings
|
||||||
|
CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS = os.getenv('CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS', 'true').lower() == 'true'
|
||||||
@@ -57,12 +57,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "advoware_cal_sync",
|
||||||
|
"config": {
|
||||||
|
"steps/advoware_cal_sync/calendar_sync_api_step.py": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"steps/advoware_cal_sync/calendar_sync_cron_step.py": {
|
||||||
|
"x": 200,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "advoware",
|
"id": "advoware",
|
||||||
"config": {
|
"config": {
|
||||||
"steps/advoware_proxy/advoware_api_proxy_put_step.py": {
|
"steps/advoware_proxy/advoware_api_proxy_put_step.py": {
|
||||||
"x": 400,
|
"x": 168,
|
||||||
"y": 0
|
"y": -54
|
||||||
},
|
},
|
||||||
"steps/advoware_proxy/advoware_api_proxy_post_step.py": {
|
"steps/advoware_proxy/advoware_api_proxy_post_step.py": {
|
||||||
"x": -340,
|
"x": -340,
|
||||||
@@ -77,14 +90,5 @@
|
|||||||
"y": 0
|
"y": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "advoware_cal_sync",
|
|
||||||
"config": {
|
|
||||||
"steps/advoware_cal_sync/advoware_calendar_sync_step.py": {
|
|
||||||
"x": 0,
|
|
||||||
"y": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "motia install",
|
"postinstall": "motia install",
|
||||||
"dev": "motia dev",
|
"dev": "motia dev",
|
||||||
"start": ". python_modules/bin/activate && motia start --host 0.0.0.0",
|
"start": "PYTHONPATH=/opt/motia-app/bitbylaw . python_modules/bin/activate && motia start --host 0.0.0.0",
|
||||||
"generate-types": "motia generate-types",
|
"generate-types": "motia generate-types",
|
||||||
"build": "motia build",
|
"build": "motia build",
|
||||||
"clean": "rm -rf dist node_modules python_modules .motia .mermaid"
|
"clean": "rm -rf dist node_modules python_modules .motia .mermaid"
|
||||||
|
|||||||
@@ -5,6 +5,4 @@ requests
|
|||||||
redis
|
redis
|
||||||
python-dotenv
|
python-dotenv
|
||||||
google-api-python-client
|
google-api-python-client
|
||||||
google-auth
|
google-auth
|
||||||
google-auth-oauthlib
|
|
||||||
google-auth-httplib2
|
|
||||||
@@ -26,7 +26,7 @@ class AdvowareAPI:
|
|||||||
|
|
||||||
def __init__(self, context=None):
|
def __init__(self, context=None):
|
||||||
self.context = context # Für Workbench-Logging
|
self.context = context # Für Workbench-Logging
|
||||||
self._log("AdvowareAPI __init__ started")
|
self._log("AdvowareAPI __init__ started", level='debug')
|
||||||
self.API_BASE_URL = Config.ADVOWARE_API_BASE_URL
|
self.API_BASE_URL = Config.ADVOWARE_API_BASE_URL
|
||||||
try:
|
try:
|
||||||
self.redis_client = redis.Redis(
|
self.redis_client = redis.Redis(
|
||||||
@@ -131,9 +131,12 @@ class AdvowareAPI:
|
|||||||
|
|
||||||
async with aiohttp.ClientSession(timeout=effective_timeout) as session:
|
async with aiohttp.ClientSession(timeout=effective_timeout) as session:
|
||||||
try:
|
try:
|
||||||
self._log(f"Making API call: {method} {url}")
|
if self.context:
|
||||||
|
self.context.logger.debug(f"Making API call: {method} {url}")
|
||||||
|
else:
|
||||||
|
logger.debug(f"Making API call: {method} {url}")
|
||||||
async with session.request(method, url, headers=effective_headers, params=params, json=json_data) as response:
|
async with session.request(method, url, headers=effective_headers, params=params, json=json_data) as response:
|
||||||
self._log(f"API response status: {response.status}")
|
response.raise_for_status()
|
||||||
if response.status == 401:
|
if response.status == 401:
|
||||||
self._log("401 Unauthorized, refreshing token")
|
self._log("401 Unauthorized, refreshing token")
|
||||||
token = self.get_access_token(force_refresh=True)
|
token = self.get_access_token(force_refresh=True)
|
||||||
@@ -142,13 +145,27 @@ class AdvowareAPI:
|
|||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return await response.json() if response.content_type == 'application/json' else None
|
return await response.json() if response.content_type == 'application/json' else None
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return await response.json() if response.content_type == 'application/json' else None
|
if response.content_type == 'application/json':
|
||||||
|
try:
|
||||||
|
return await response.json()
|
||||||
|
except Exception as e:
|
||||||
|
self._log(f"JSON parse error: {e}")
|
||||||
|
# For methods like DELETE that may return 200 with no body, return None
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
||||||
except aiohttp.ClientError as e:
|
except aiohttp.ClientError as e:
|
||||||
self._log(f"API call failed: {e}")
|
self._log(f"API call failed: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def _log(self, message):
|
def _log(self, message, level='info'):
|
||||||
if self.context:
|
if self.context:
|
||||||
self.context.logger.info(message)
|
if level == 'debug':
|
||||||
|
self.context.logger.debug(message)
|
||||||
|
else:
|
||||||
|
self.context.logger.info(message)
|
||||||
else:
|
else:
|
||||||
logger.info(message)
|
if level == 'debug':
|
||||||
|
logger.debug(message)
|
||||||
|
else:
|
||||||
|
logger.info(message)
|
||||||
1
bitbylaw/steps/__init__.py
Normal file
1
bitbylaw/steps/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Steps package
|
||||||
@@ -1,65 +1,160 @@
|
|||||||
# Advoware Calendar Sync
|
# Advoware Calendar Sync - Hub-Based Design
|
||||||
|
|
||||||
Dieser Abschnitt implementiert die bidirektionale Synchronisation zwischen Advoware-Terminen und Google Calendar. Für jeden Mitarbeiter in Advoware wird automatisch ein entsprechender Google Calendar erstellt und gepflegt.
|
# Advoware Calendar Sync - Hub-Based Design
|
||||||
|
|
||||||
|
Dieser Abschnitt implementiert die bidirektionale Synchronisation zwischen Advoware-Terminen und Google Calendar unter Verwendung von PostgreSQL als zentralem Hub (Single Source of Truth). Das System stellt sicher, dass Termine konsistent gehalten werden, mit konfigurierbaren Konfliktauflösungsstrategien, Schreibberechtigungen und Datenschutzfeatures wie Anonymisierung. Der Sync läuft in vier strikten Phasen, um maximale Robustheit und Atomarität zu gewährleisten.
|
||||||
|
|
||||||
## Übersicht
|
## Übersicht
|
||||||
|
|
||||||
Das System synchronisiert Termine zwischen:
|
Das System synchronisiert Termine zwischen:
|
||||||
- **Advoware**: Zentrale Terminverwaltung mit detaillierten Informationen
|
- **Advoware**: Zentrale Terminverwaltung mit detaillierten Informationen (aber vielen API-Bugs).
|
||||||
- **Google Calendar**: Benutzerfreundliche Kalenderansicht für jeden Mitarbeiter
|
- **Google Calendar**: Benutzerfreundliche Kalenderansicht für jeden Mitarbeiter.
|
||||||
|
- **PostgreSQL Hub**: Zentraler Datenspeicher für State, Policies und Audit-Logs.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
|
### Hub-Design
|
||||||
|
- **Single Source of Truth**: Alle Sync-Informationen werden in PostgreSQL gespeichert.
|
||||||
|
- **Policies**: Enums für Sync-Strategien (`source_system_wins`, `last_change_wins`) und Flags für Schreibberechtigung (`advoware_write_allowed`).
|
||||||
|
- **Status-Tracking**: `sync_status` ('pending', 'synced', 'failed') für Monitoring und Retries.
|
||||||
|
- **Transaktionen**: Jede DB-Operation läuft in separaten Transaktionen; Fehler beeinflussen nur den aktuellen Eintrag.
|
||||||
|
- **Soft Deletes**: Gelöschte Termine werden markiert, nicht entfernt.
|
||||||
|
- **Phasen-basierte Verarbeitung**: Sync in 4 Phasen, um Neue, Deletes und Updates zu trennen.
|
||||||
|
- **Timestamp-basierte Updates**: Updates werden ausschließlich auf Basis von `last_sync` (gesetzt auf den API-Timestamp der Quelle) getriggert, nicht auf Datenvergleichen, um Race-Conditions zu vermeiden.
|
||||||
|
- **Anonymisierung**: Optionale Anonymisierung sensibler Daten (Text, Notiz, Ort) bei Advoware → Google Sync, um Datenschutz zu wahren.
|
||||||
|
|
||||||
|
### Sync-Phasen
|
||||||
|
1. **Phase 1: Neue Einträge Advoware → Google** - Erstelle Google-Events für neue Advoware-Termine, dann DB-Insert.
|
||||||
|
2. **Phase 2: Neue Einträge Google → Advoware** - Erstelle Advoware-Termine für neue Google-Events, dann DB-Insert.
|
||||||
|
3. **Phase 3: Gelöschte Einträge identifizieren** - Handle Deletes/Recreates basierend auf Strategie.
|
||||||
|
4. **Phase 4: Bestehende Einträge updaten** - Update bei Änderungen, basierend auf Timestamps (API-Timestamp > `last_sync`).
|
||||||
|
|
||||||
|
### Datenbank-Schema
|
||||||
|
```sql
|
||||||
|
-- Haupt-Tabelle
|
||||||
|
CREATE TABLE calendar_sync (
|
||||||
|
sync_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
employee_kuerzel VARCHAR(10) NOT NULL,
|
||||||
|
advoware_frnr INTEGER,
|
||||||
|
google_event_id VARCHAR(255),
|
||||||
|
source_system source_system_enum NOT NULL,
|
||||||
|
sync_strategy sync_strategy_enum NOT NULL DEFAULT 'source_system_wins',
|
||||||
|
sync_status sync_status_enum NOT NULL DEFAULT 'synced',
|
||||||
|
advoware_write_allowed BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
deleted BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
|
last_sync TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Enums
|
||||||
|
CREATE TYPE source_system_enum AS ENUM ('advoware', 'google');
|
||||||
|
CREATE TYPE sync_strategy_enum AS ENUM ('source_system_wins', 'last_change_wins');
|
||||||
|
CREATE TYPE sync_status_enum AS ENUM ('pending', 'synced', 'failed');
|
||||||
|
|
||||||
|
-- Audit-Tabelle
|
||||||
|
CREATE TABLE calendar_sync_audit (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
sync_id UUID NOT NULL,
|
||||||
|
action VARCHAR(10) NOT NULL, -- INSERT, UPDATE, DELETE
|
||||||
|
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indizes (angepasst für Soft Deletes)
|
||||||
|
CREATE UNIQUE INDEX idx_calendar_sync_advoware ON calendar_sync (employee_kuerzel, advoware_frnr) WHERE advoware_frnr IS NOT NULL AND deleted = FALSE;
|
||||||
|
CREATE UNIQUE INDEX idx_calendar_sync_google ON calendar_sync (employee_kuerzel, google_event_id) WHERE google_event_id IS NOT NULL AND deleted = FALSE;
|
||||||
|
```
|
||||||
|
|
||||||
## Funktionalität
|
## Funktionalität
|
||||||
|
|
||||||
### Automatische Kalender-Erstellung
|
### Automatische Kalender-Erstellung
|
||||||
- Für jeden Advoware-Mitarbeiter wird ein Google Calendar mit dem Namen `AW-{Kuerzel}` erstellt
|
- Für jeden Advoware-Mitarbeiter wird ein Google Calendar mit dem Namen `AW-{Kuerzel}` erstellt.
|
||||||
- Beispiel: Mitarbeiter mit Kürzel "SB" → Calendar "AW-SB"
|
- Beispiel: Mitarbeiter mit Kürzel "SB" → Calendar "AW-SB".
|
||||||
- Kalender wird mit dem Haupt-Google-Account (`lehmannundpartner@gmail.com`) als Owner geteilt
|
- Kalender wird mit dem Haupt-Google-Account (`lehmannundpartner@gmail.com`) als Owner geteilt.
|
||||||
|
|
||||||
### Bidirektionale Synchronisation mit Token-Validierung
|
### Phasen-Details
|
||||||
|
|
||||||
#### Advoware → Google Calendar
|
#### Phase 1: Neue Einträge Advoware → Google
|
||||||
- Alle Termine eines Mitarbeiters werden aus Advoware abgerufen (Zeitraum: aktuelles Jahr + 2 Jahre)
|
- Fetch Advoware-Termine.
|
||||||
- Neue Termine werden in den entsprechenden Google Calendar eingetragen
|
- Für jede frNr, die nicht in DB (deleted=FALSE) existiert: Standardisiere Daten (mit Anonymisierung falls aktiviert), erstelle Google-Event, dann INSERT in DB mit `sync_status = 'synced'`, `last_sync` auf Advoware-Timestamp.
|
||||||
- Die Advoware-Termin-ID (`frNr`) wird als Metadaten gespeichert
|
- Bei Fehlern: Warnung loggen, weitermachen (nicht abbrechen).
|
||||||
- Bestehende Termine werden aktualisiert, wenn Änderungen erkannt werden
|
|
||||||
|
|
||||||
#### Google Calendar → Advoware
|
#### Phase 2: Neue Einträge Google → Advoware
|
||||||
- Termine aus Google Calendar ohne `frNr` werden als neue Termine in Advoware erstellt
|
- Fetch Google-Events.
|
||||||
- Die generierte `frNr` wird zurück in den Google Calendar geschrieben
|
- Für jeden event_id, der nicht in DB existiert: Standardisiere Daten, erstelle Advoware-Termin, dann INSERT in DB mit `sync_status = 'synced'`, `last_sync` auf Google-Timestamp.
|
||||||
- Token-basierte Validierung verhindert unbefugte Änderungen
|
- Bei frNr None (API-Bug): Skippen mit Warnung.
|
||||||
- Sync-Info wird in der Description/Notiz gespeichert
|
- Bei Fehlern: Warnung loggen, weitermachen.
|
||||||
|
|
||||||
### Token-Sicherheit
|
#### Phase 3: Gelöschte Einträge identifizieren
|
||||||
- MD5-Hash mit Salt (aus Umgebungsvariable `CALENDAR_SYNC_SALT`) für Änderungsvalidierung
|
- Für jeden DB-Eintrag: Prüfe, ob Termin in API fehlt.
|
||||||
- Sync-Info Format: `## no change below this line ##\nfrNr: {frNr}\nsync-token: {token}`
|
- Bei beiden fehlend: Soft Delete.
|
||||||
- Token wird bei jeder Änderung neu berechnet und validiert
|
- Bei einem fehlend: Recreate oder propagate Delete basierend auf Strategie.
|
||||||
|
- Bei Fehlern: `sync_status = 'failed'`, Warnung.
|
||||||
|
|
||||||
### Löschungen
|
#### Phase 4: Bestehende Einträge updaten
|
||||||
- Google-initiale Termine, die in Google gelöscht werden, werden auch in Advoware gelöscht
|
- Für bestehende Einträge: Prüfe API-Timestamp > `last_sync`.
|
||||||
- Tracking von gelöschten `frNr` um Re-Sync zu verhindern
|
- Bei `source_system_wins`: Update basierend auf `source_system`, setze `last_sync` auf den API-Timestamp der Quelle.
|
||||||
|
- Bei `last_change_wins`: Vergleiche Timestamps, update das System mit dem neueren, setze `last_sync` auf den neueren Timestamp.
|
||||||
|
- Anonymisierung: Bei Advoware → Google wird Text/Notiz/Ort anonymisiert, wenn `CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS = True`.
|
||||||
|
- Bei Fehlern: `sync_status = 'failed'`, Warnung.
|
||||||
|
|
||||||
## API-Endpunkte
|
### Datenmapping und Standardisierung
|
||||||
|
Beide Systeme werden auf gemeinsames Format normalisiert (Berlin TZ):
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
'start': datetime, # Berlin TZ
|
||||||
|
'end': datetime,
|
||||||
|
'text': str,
|
||||||
|
'notiz': str,
|
||||||
|
'ort': str,
|
||||||
|
'dauertermin': int, # 0/1
|
||||||
|
'turnus': int, # 0/1
|
||||||
|
'turnusArt': int,
|
||||||
|
'recurrence': str # RRULE oder None
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Advoware API
|
#### Advoware → Standard
|
||||||
- `GET /api/v1/advonet/Mitarbeiter?aktiv=true` - Liste aktiver Mitarbeiter
|
- Start: `datum` + `uhrzeitVon` (Fallback 09:00), oder `datum` als datetime.
|
||||||
- `GET /api/v1/advonet/Termine?frnr={frnr}` - Einzelner Termin
|
- End: `datumBis` + `uhrzeitBis` (Fallback 10:00), oder `datum` + 1h.
|
||||||
- `GET /api/v1/advonet/Termine?kuerzel={kuerzel}&from={date}&to={date}` - Termine eines Mitarbeiters
|
- All-Day: `dauertermin=1` oder Dauer >1 Tag.
|
||||||
- `POST /api/v1/advonet/Termine` - Neuen Termin erstellen
|
- Recurring: `turnus`/`turnusArt` (vereinfacht, keine RRULE).
|
||||||
- `PUT /api/v1/advonet/Termine` - Termin aktualisieren
|
- Anonymisierung: Wenn `CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS`, setze `text='Advoware blocked'`, `notiz=''`, `ort=''`.
|
||||||
- `DELETE /api/v1/advonet/Termine?frnr={frnr}` - Termin löschen
|
|
||||||
|
|
||||||
**API-Schwächen und Seltsamkeiten:**
|
#### Google → Standard
|
||||||
- Parameter müssen als Strings übergeben werden (z.B. `aktiv='true'`, nicht `True`)
|
- Start/End: `dateTime` oder `date` (All-Day).
|
||||||
- Response kann manchmal HTML statt JSON zurückgeben, auch bei 200 Status
|
- All-Day: `dauertermin=1` wenn All-Day oder Dauer >1 Tag.
|
||||||
- Content-Type Header ist nicht immer korrekt gesetzt
|
- Recurring: RRULE aus `recurrence`.
|
||||||
- Token-Authentifizierung mit HMAC-Signature erforderlich
|
|
||||||
- Keine Paginierung für große Resultate
|
|
||||||
- Zeitformate: `datum` als `YYYY-MM-DDTHH:MM:SS`, aber manchmal ohne T
|
|
||||||
|
|
||||||
### Google Calendar API
|
#### Standard → Advoware
|
||||||
- Kalender-Management (erstellen, auflisten, ACL setzen)
|
- POST/PUT: `datum`/`uhrzeitBis`/`datumBis` aus start/end.
|
||||||
- Event-Management (erstellen, aktualisieren, löschen)
|
- Defaults: `vorbereitungsDauer='00:00:00'`, `sb`/`anwalt`=employee_kuerzel.
|
||||||
- Service Account Authentifizierung mit Backoff bei Rate Limits
|
|
||||||
|
#### Standard → Google
|
||||||
|
- All-Day: `date` statt `dateTime`, end +1 Tag.
|
||||||
|
- Recurring: RRULE aus `recurrence`.
|
||||||
|
|
||||||
|
## API-Schwächen und Fuckups
|
||||||
|
|
||||||
|
### Advoware API (Buggy und Inkonsistent)
|
||||||
|
- **Case Sensitivity in Responses**: Feldnamen variieren – manchmal `'frNr'`, manchmal `'frnr'` (z.B. POST-Response: `{'frnr': 123}`). Code prüft beide (`result.get('frNr') or result.get('frnr')`), um None zu vermeiden.
|
||||||
|
- **Zeitformate**: `datum`/`datumBis` als `'YYYY-MM-DD'` oder `'YYYY-MM-DDTHH:MM:SS'`. `uhrzeitVon`/`uhrzeitBis` separat (z.B. `'09:00:00'`). Fehlt `uhrzeitVon`, Fallback 09:00; fehlt `uhrzeitBis`, 10:00. Parsing muss beide Formate handhaben.
|
||||||
|
- **Defaults und Fehlende Felder**: Viele Felder optional; Code setzt Fallbacks (z.B. `uhrzeitVon='09:00:00'`).
|
||||||
|
- **Recurring-Unterstützung**: Keine RRULE; nur `turnus` (0/1) und `turnusArt` (0-?). Mapping zu Google RRULE ist vereinfacht und unvollständig.
|
||||||
|
- **API-Zuverlässigkeit**: Manchmal erfolgreicher POST, aber `frNr: None` (trotz gültiger Response). 500-Fehler bei Bad Requests. Keine Timestamp-Details in Responses.
|
||||||
|
- **Zeitzonen**: Alles implizit Berlin; Code konvertiert explizit.
|
||||||
|
- **Andere Bugs**: `zuletztGeaendertAm` für Timestamps, aber Format unzuverlässig.
|
||||||
|
- **DELETE Responses**: DELETE-Anfragen geben manchmal einen leeren Body zurück, was zu `JSONDecodeError` führt. Code fängt dies mit try/except ab und gibt `None` zurück, um den Sync nicht zu brechen.
|
||||||
|
- **frNr Wiederverwendung**: frNr sind sequentiell und werden nicht wiederverwendet. Getestet durch Erstellen/Löschen/Erstellen: z.B. 85861, 85862, delete 85861, nächstes Create 85863. Kein Risiko für DB-Konflikte durch ID-Reuse.
|
||||||
|
- **Timestamp-basierte Updates**: Um Race-Conditions und redundante Syncs zu vermeiden, werden Updates in Phase 4 nur durchgeführt, wenn der API-Timestamp der Quelle > `last_sync` (gesetzt auf den API-Timestamp nach erfolgreichem Write).
|
||||||
|
- **Soft Deletes und Partielle Unique Indexes**: Gelöschte Termine werden mit `deleted = TRUE` markiert, nicht entfernt. Partielle Unique Indexes (z.B. `WHERE deleted = FALSE`) verhindern Duplikate für aktive Einträge.
|
||||||
|
- **Anonymisierung**: Optionale Anonymisierung sensibler Daten (Text, Notiz, Ort) bei Advoware → Google Sync, um Datenschutz zu wahren (z.B. `text='Advoware blocked'`).
|
||||||
|
|
||||||
|
### Google Calendar API (Zuverlässig)
|
||||||
|
- **Zeitformate**: `dateTime` als ISO mit TZ (z.B. `'2025-01-01T10:00:00+01:00'`), `date` für All-Day. Code parst mit `fromisoformat` und `.rstrip('Z')`.
|
||||||
|
- **Zeitzonen**: Explizit (z.B. `'Europe/Berlin'`); Code konvertiert zu Berlin TZ.
|
||||||
|
- **Recurring**: RRULE in `recurrence`; vollständig unterstützt.
|
||||||
|
- **Updates**: `updated` Timestamp für last-change.
|
||||||
|
- **Keine bekannten Bugs**: Zuverlässig, aber Rate-Limits möglich.
|
||||||
|
|
||||||
## Step-Konfiguration
|
## Step-Konfiguration
|
||||||
|
|
||||||
@@ -71,41 +166,38 @@ Das System synchronisiert Termine zwischen:
|
|||||||
**Event Data:**
|
**Event Data:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"full_content": true // oder false für nur "blocked"
|
"data": {
|
||||||
|
"body": {
|
||||||
|
"employee_kuerzel": "SB" // Optional, default "AI"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### calendar_sync_api_step.py
|
|
||||||
- **Type:** api
|
|
||||||
- **Path:** `/advoware/calendar/sync`
|
|
||||||
- **Method:** POST
|
|
||||||
- **Flows:** advoware
|
|
||||||
|
|
||||||
**Request Body:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"full_content": true // oder false für nur "blocked"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### calendar_sync_cron_step.py
|
|
||||||
- **Type:** cron
|
|
||||||
- **Schedule:** Täglich um 2:00 Uhr
|
|
||||||
- **Flows:** advoware
|
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
1. PostgreSQL 17 installieren und starten (localhost-only).
|
||||||
|
2. Datenbank erstellen: `sudo -u postgres psql -f /tmp/create_db.sql`
|
||||||
|
3. User und Berechtigungen setzen.
|
||||||
|
|
||||||
### Google API Credentials
|
### Google API Credentials
|
||||||
1. Google Cloud Console Projekt erstellen
|
1. Google Cloud Console Projekt erstellen.
|
||||||
2. Google Calendar API aktivieren
|
2. Google Calendar API aktivieren.
|
||||||
3. Service Account erstellen (kein OAuth)
|
3. Service Account erstellen.
|
||||||
4. `service-account.json` Datei im Projektverzeichnis bereitstellen
|
4. `service-account.json` im Projekt bereitstellen.
|
||||||
|
|
||||||
### Advoware API Credentials
|
### Advoware API Credentials
|
||||||
OAuth-ähnliche Authentifizierung mit HMAC-Signature.
|
OAuth-ähnliche Authentifizierung.
|
||||||
|
|
||||||
### Umgebungsvariablen
|
### Umgebungsvariablen
|
||||||
```env
|
```env
|
||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_HOST=localhost
|
||||||
|
POSTGRES_USER=calendar_sync_user
|
||||||
|
POSTGRES_PASSWORD=your_password
|
||||||
|
POSTGRES_DB_NAME=calendar_sync_db
|
||||||
|
|
||||||
# Google Calendar
|
# Google Calendar
|
||||||
GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH=service-account.json
|
GOOGLE_CALENDAR_SERVICE_ACCOUNT_PATH=service-account.json
|
||||||
|
|
||||||
@@ -122,14 +214,14 @@ ADVOWARE_PASSWORD=your_password
|
|||||||
ADVOWARE_TOKEN_LIFETIME_MINUTES=55
|
ADVOWARE_TOKEN_LIFETIME_MINUTES=55
|
||||||
ADVOWARE_API_TIMEOUT_SECONDS=30
|
ADVOWARE_API_TIMEOUT_SECONDS=30
|
||||||
|
|
||||||
# Redis für Token Caching
|
# Redis
|
||||||
REDIS_HOST=localhost
|
REDIS_HOST=localhost
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
REDIS_DB_ADVOWARE_CACHE=1
|
REDIS_DB_CALENDAR_SYNC=1
|
||||||
REDIS_TIMEOUT_SECONDS=5
|
REDIS_TIMEOUT_SECONDS=5
|
||||||
|
|
||||||
# Calendar Sync
|
# Anonymisierung
|
||||||
CALENDAR_SYNC_SALT=your_secret_salt
|
CALENDAR_SYNC_ANONYMIZE_GOOGLE_EVENTS=true # Optional, default false
|
||||||
```
|
```
|
||||||
|
|
||||||
## Verwendung
|
## Verwendung
|
||||||
@@ -141,121 +233,56 @@ curl -X POST "http://localhost:3000/advoware/calendar/sync" \
|
|||||||
-d '{"full_content": true}'
|
-d '{"full_content": true}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Event-basierter Sync
|
|
||||||
```bash
|
|
||||||
curl -X POST "http://localhost:3000/events" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"type": "calendar.sync.triggered", "data": {"full_content": true}}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Automatischer Sync
|
### Automatischer Sync
|
||||||
Der Cron-Step führt täglich um 2:00 Uhr einen vollständigen Sync aus.
|
Cron-Step für regelmäßige Ausführung.
|
||||||
|
|
||||||
## Datenmapping
|
## Fehlerbehandlung und Logging
|
||||||
|
|
||||||
### Advoware → Google Calendar
|
- **Transaktionen**: Pro Operation separat; Rollback nur für diese.
|
||||||
```javascript
|
- **Logging**: Detailliert (Info/Debug für API, Warnung für Fehler).
|
||||||
{
|
- **API-Fehler**: Retry mit Backoff für Google; robust gegen Advoware-Bugs.
|
||||||
"summary": "Advoware blocked", // Immer "blocked" für Privacy
|
- **Datenfehler**: Fallbacks bei Parsing-Fehlern.
|
||||||
"description": `Advoware Termin ID: ${appointment.frNr}`,
|
|
||||||
"location": "",
|
|
||||||
"start": {
|
|
||||||
"dateTime": start_datetime, // Berechnet aus datum/uhrzeitVon
|
|
||||||
"timeZone": "Europe/Berlin"
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"dateTime": end_datetime, // Berechnet aus datumBis/uhrzeitBis
|
|
||||||
"timeZone": "Europe/Berlin"
|
|
||||||
},
|
|
||||||
"extendedProperties": {
|
|
||||||
"private": {
|
|
||||||
"advoware_frnr": appointment.frNr.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Google Calendar → Advoware
|
## Sicherheit und Datenschutz
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
"frNr": int(frnr),
|
|
||||||
"text": event.summary,
|
|
||||||
"notiz": updated_description, // Mit sync-info
|
|
||||||
"ort": event.location,
|
|
||||||
"datum": start.split('+')[0] if '+' in start else start,
|
|
||||||
"uhrzeitBis": end.split('T')[1].split('+')[0] if 'T' in end else '09:00:00',
|
|
||||||
"datumBis": end.split('+')[0] if '+' in end else end,
|
|
||||||
"sb": employee_kuerzel,
|
|
||||||
"anwalt": employee_kuerzel,
|
|
||||||
"vorbereitungsDauer": "00:00:00"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fehlerbehandlung
|
- DB-User mit minimalen Berechtigungen.
|
||||||
|
- Schreibberechtigung-Flags verhindern unbefugte Änderungen.
|
||||||
|
- Anonymisierung: Verhindert Leakage sensibler Daten in Google Calendar.
|
||||||
|
- Audit-Logs für Compliance.
|
||||||
|
|
||||||
- **Google API Fehler:** Exponentieller Backoff bei 403/429, automatische Wiederholung
|
## Bekannte Probleme
|
||||||
- **Advoware API Fehler:** Token-Refresh bei 401, detaillierte Logging
|
|
||||||
- **JSON Parse Fehler:** Response-Text logging für Debugging
|
|
||||||
- **Netzwerkfehler:** Timeout-Handling, konfigurierbare Timeouts
|
|
||||||
- **Dateninkonsistenzen:** Token-Validierung, Änderungsvergleich vor Update
|
|
||||||
|
|
||||||
## Monitoring
|
- Recurring-Events: Begrenzte Unterstützung; Advoware hat keine RRULE.
|
||||||
|
- Timestamps: Fehlende in Google können zu Fallback führen.
|
||||||
|
- Performance: Bei vielen Terminen könnte Paginierung helfen.
|
||||||
|
|
||||||
### Logs
|
## Korrekter Umgang mit Advoware-Timestamps
|
||||||
- Erfolgreiche Synchronisationen mit Anzahl
|
|
||||||
- Fehlerhafte API-Calls mit Details
|
|
||||||
- Kalender-Erstellungen und ACL-Setups
|
|
||||||
- Performance-Metriken (Sync-Dauer)
|
|
||||||
- Debug-Logs für Änderungsvergleiche
|
|
||||||
|
|
||||||
### Metriken
|
### Problemstellung
|
||||||
- Anzahl synchronisierter Termine
|
Advoware-Timestamps (z.B. `'zuletztGeaendertAm'`) werden in Berlin-Zeit geliefert, aber das Parsing mit `datetime.datetime.fromisoformat(...).replace(tzinfo=BERLIN_TZ)` führte zu falschen Offsets (z.B. 53 Minuten Unterschied), da `replace(tzinfo=...)` auf naive datetime nicht korrekt mit pytz-TZ-Objekten funktioniert. Dies verursachte Endlosschleifen in Phase 4, da `adv_ts` falsch hochgesetzt wurde.
|
||||||
- Verarbeitete Mitarbeiter
|
|
||||||
- Fehlerquoten pro API
|
|
||||||
- Gelöschte Termine
|
|
||||||
- Token-Validierungsfehler
|
|
||||||
|
|
||||||
## Sicherheit
|
### Lösung
|
||||||
|
Verwende `BERLIN_TZ.localize(naive_datetime)` statt `.replace(tzinfo=BERLIN_TZ)`:
|
||||||
|
- `localize()` setzt die TZ korrekt auf pytz-TZ-Objekte.
|
||||||
|
- Beispiel:
|
||||||
|
```python
|
||||||
|
naive = datetime.datetime.fromisoformat('2025-10-23T14:18:36.245')
|
||||||
|
adv_ts = BERLIN_TZ.localize(naive) # Ergebnis: 2025-10-23 14:18:36.245+02:00
|
||||||
|
```
|
||||||
|
- Dies stellt sicher, dass Timestamps korrekt in UTC konvertiert werden (z.B. 12:18 UTC) und Vergleiche in Phase 4 funktionieren.
|
||||||
|
|
||||||
- Service Account für Google API (kein User-Consent)
|
### Implementierung
|
||||||
- HMAC-SHA512 für Advoware Token-Generierung
|
- In `calendar_sync_event_step.py`, Phase 4:
|
||||||
- Token-Caching in Redis mit TTL
|
```python
|
||||||
- MD5-Token für Sync-Validierung mit Salt
|
adv_ts = BERLIN_TZ.localize(datetime.datetime.fromisoformat(adv_data['zuletztGeaendertAm']))
|
||||||
- Scoped Permissions (nur Calendar-Zugriff)
|
```
|
||||||
- Audit-Logs für alle Änderungen
|
- Für Google-Timestamps: `.astimezone(BERLIN_TZ)` bleibt korrekt.
|
||||||
|
- Alle Timestamps werden zu UTC normalisiert für DB-Speicherung und Vergleiche.
|
||||||
|
|
||||||
## Bekannte Probleme und Workarounds
|
### Vermeidung von Fehlern
|
||||||
|
- Niemals `.replace(tzinfo=pytz_tz)` verwenden – immer `tz.localize(naive)`.
|
||||||
### Advoware API
|
- Teste Parsing: `BERLIN_TZ.localize(datetime.datetime.fromisoformat(ts)).astimezone(pytz.utc)` sollte korrekte UTC ergeben.
|
||||||
- **Parameter-Typen:** Alle Query-Parameter müssen Strings sein (z.B. `'true'` statt `True`)
|
- Bei anderen TZ: Gleiche Regel anwenden.
|
||||||
- **Response-Formate:** Manchmal HTML statt JSON, auch bei 200 Status
|
|
||||||
- **Content-Type:** Nicht immer korrekt gesetzt, führt zu Parse-Fehlern
|
|
||||||
- **Zeitformate:** Inkonsistente Formate, manuelle Parsing erforderlich
|
|
||||||
- **Rate Limits:** Keine dokumentierten Limits, aber praktische Limits vorhanden
|
|
||||||
|
|
||||||
### Google Calendar API
|
|
||||||
- **Rate Limits:** 403/429 mit Backoff-Handling
|
|
||||||
- **ACL-Sharing:** Owner-Rolle für Hauptaccount erforderlich
|
|
||||||
- **Event-Updates:** Vollständige Event-Daten bei Updates senden
|
|
||||||
|
|
||||||
### Sync-Logik
|
|
||||||
- **Token-Mismatch:** Verhindert unbefugte Änderungen
|
|
||||||
- **Deleted Tracking:** Verhindert Re-Sync gelöschter Termine
|
|
||||||
- **Änderungsvergleich:** Nur tatsächliche Änderungen syncen
|
|
||||||
|
|
||||||
## Erweiterungen
|
## Erweiterungen
|
||||||
|
|
||||||
### Geplante Features
|
|
||||||
- Inkrementelle Syncs (nur geänderte Termine seit letztem Sync)
|
|
||||||
- Konfliktlösungsstrategien (Advoware gewinnt, Google gewinnt, Manuell)
|
|
||||||
- Batch-Verarbeitung für bessere Performance
|
|
||||||
- Webhook-Integration für Echtzeit-Syncs
|
|
||||||
- Mehrere Google-Accounts unterstützen
|
|
||||||
- Outlook/Apple Calendar Integration
|
|
||||||
|
|
||||||
### Code-Verbesserungen
|
|
||||||
- Unit-Tests für API-Calls
|
|
||||||
- Mock-Server für Testing
|
|
||||||
- Config-Validation beim Startup
|
|
||||||
- Health-Checks für beide APIs
|
|
||||||
- Metrics-Export (Prometheus)
|
|
||||||
1
bitbylaw/steps/advoware_cal_sync/__init__.py
Normal file
1
bitbylaw/steps/advoware_cal_sync/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Advoware Calendar Sync package
|
||||||
@@ -1,308 +0,0 @@
|
|||||||
from services.advoware import AdvowareAPI
|
|
||||||
from config import Config
|
|
||||||
from googleapiclient.discovery import build
|
|
||||||
from google.oauth2.credentials import Credentials
|
|
||||||
from google.auth.transport.requests import Request
|
|
||||||
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
||||||
import json
|
|
||||||
import datetime
|
|
||||||
import pickle
|
|
||||||
import os.path
|
|
||||||
import redis
|
|
||||||
|
|
||||||
config = {
|
|
||||||
'type': 'api',
|
|
||||||
'name': 'Advoware Calendar Sync',
|
|
||||||
'description': 'Synchronisiert Advoware Termine mit Google Calendar für alle Mitarbeiter',
|
|
||||||
'path': '/advoware/calendar/sync',
|
|
||||||
'method': 'POST',
|
|
||||||
'flows': ['advoware'],
|
|
||||||
'emits': []
|
|
||||||
}
|
|
||||||
|
|
||||||
SCOPES = ['https://www.googleapis.com/auth/calendar']
|
|
||||||
|
|
||||||
async def get_google_service(context):
|
|
||||||
"""Initialisiert Google Calendar API Service"""
|
|
||||||
creds = None
|
|
||||||
|
|
||||||
# Token aus Datei laden falls vorhanden
|
|
||||||
if os.path.exists('token.pickle'):
|
|
||||||
with open('token.pickle', 'rb') as token:
|
|
||||||
creds = pickle.load(token)
|
|
||||||
|
|
||||||
# Wenn keine validen Credentials, neu authentifizieren
|
|
||||||
if not creds or not creds.valid:
|
|
||||||
if creds and creds.expired and creds.refresh_token:
|
|
||||||
creds.refresh(Request())
|
|
||||||
else:
|
|
||||||
# Hier würde normalerweise der OAuth Flow laufen
|
|
||||||
# Für Server-Umgebung brauchen wir Service Account oder gespeicherte Credentials
|
|
||||||
print("WARNING: Google OAuth Credentials nicht gefunden. Bitte token.pickle bereitstellen oder Google Calendar Sync überspringen.")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Token speichern
|
|
||||||
with open('token.pickle', 'wb') as token:
|
|
||||||
pickle.dump(creds, token)
|
|
||||||
|
|
||||||
return build('calendar', 'v3', credentials=creds)
|
|
||||||
|
|
||||||
async def get_advoware_employees(context):
|
|
||||||
"""Ruft alle Mitarbeiter von Advoware ab"""
|
|
||||||
advoware = AdvowareAPI(context)
|
|
||||||
try:
|
|
||||||
# Annahme: Mitarbeiter-Endpoint existiert ähnlich wie andere
|
|
||||||
result = await advoware.api_call('Mitarbeiter')
|
|
||||||
print(f"Advoware Mitarbeiter abgerufen: {len(result) if isinstance(result, list) else 'unbekannt'}")
|
|
||||||
return result if isinstance(result, list) else []
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fehler beim Abrufen der Mitarbeiter: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def ensure_google_calendar(service, employee_kuerzel, context):
|
|
||||||
"""Stellt sicher, dass ein Google Calendar für den Mitarbeiter existiert"""
|
|
||||||
calendar_name = f"AW-{employee_kuerzel}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Bestehende Kalender prüfen
|
|
||||||
calendar_list = service.calendarList().list().execute()
|
|
||||||
for calendar in calendar_list.get('items', []):
|
|
||||||
if calendar['summary'] == calendar_name:
|
|
||||||
print(f"Google Calendar '{calendar_name}' existiert bereits")
|
|
||||||
return calendar['id']
|
|
||||||
|
|
||||||
# Neuen Kalender erstellen
|
|
||||||
calendar_body = {
|
|
||||||
'summary': calendar_name,
|
|
||||||
'description': f'Advoware Termine für Mitarbeiter {employee_kuerzel}',
|
|
||||||
'timeZone': 'Europe/Berlin'
|
|
||||||
}
|
|
||||||
|
|
||||||
created_calendar = service.calendars().insert(body=calendar_body).execute()
|
|
||||||
calendar_id = created_calendar['id']
|
|
||||||
print(f"Google Calendar '{calendar_name}' erstellt mit ID: {calendar_id}")
|
|
||||||
|
|
||||||
return calendar_id
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fehler bei Google Calendar für {employee_kuerzel}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def get_advoware_appointments(employee_kuerzel, context):
|
|
||||||
"""Ruft Termine eines Mitarbeiters aus Advoware ab"""
|
|
||||||
advoware = AdvowareAPI(context)
|
|
||||||
|
|
||||||
# Zeitraum: aktuelles Jahr + 2 Jahre
|
|
||||||
from_date = datetime.datetime.now().strftime('%Y-01-01T00:00:00Z')
|
|
||||||
to_date = (datetime.datetime.now() + datetime.timedelta(days=730)).strftime('%Y-12-31T23:59:59Z')
|
|
||||||
|
|
||||||
try:
|
|
||||||
params = {
|
|
||||||
'kuerzel': employee_kuerzel,
|
|
||||||
'from': from_date,
|
|
||||||
'to': to_date
|
|
||||||
}
|
|
||||||
result = await advoware.api_call('Termine', method='GET', params=params)
|
|
||||||
appointments = result if isinstance(result, list) else []
|
|
||||||
print(f"Advoware Termine für {employee_kuerzel}: {len(appointments)} gefunden")
|
|
||||||
return appointments
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fehler beim Abrufen der Termine für {employee_kuerzel}: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def get_google_events(service, calendar_id, context):
|
|
||||||
"""Ruft Events aus Google Calendar ab"""
|
|
||||||
try:
|
|
||||||
now = datetime.datetime.utcnow()
|
|
||||||
from_date = now.strftime('%Y-01-01T00:00:00Z')
|
|
||||||
to_date = (now + datetime.timedelta(days=730)).strftime('%Y-12-31T23:59:59Z')
|
|
||||||
|
|
||||||
events_result = service.events().list(
|
|
||||||
calendarId=calendar_id,
|
|
||||||
timeMin=from_date,
|
|
||||||
timeMax=to_date,
|
|
||||||
singleEvents=True,
|
|
||||||
orderBy='startTime'
|
|
||||||
).execute()
|
|
||||||
|
|
||||||
events = events_result.get('items', [])
|
|
||||||
print(f"Google Calendar Events: {len(events)} gefunden")
|
|
||||||
return events
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fehler beim Abrufen der Google Events: {e}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
async def sync_appointment_to_google(service, calendar_id, appointment, full_content, context):
|
|
||||||
"""Synchronisiert einen Advoware-Termin zu Google Calendar"""
|
|
||||||
try:
|
|
||||||
# Start- und Endzeit aus Advoware-Daten
|
|
||||||
start_date = appointment.get('datum')
|
|
||||||
end_date = appointment.get('datumBis') or start_date
|
|
||||||
start_time = appointment.get('uhrzeitBis', '00:00:00') # Advoware hat uhrzeitBis als Endzeit?
|
|
||||||
end_time = appointment.get('uhrzeitBis', '23:59:59')
|
|
||||||
|
|
||||||
# Vollständiges Event oder nur "blocked"
|
|
||||||
if full_content:
|
|
||||||
summary = appointment.get('text', 'Advoware Termin')
|
|
||||||
description = f"Advoware Termin\nNotiz: {appointment.get('notiz', '')}\nOrt: {appointment.get('ort', '')}\nRaum: {appointment.get('raum', '')}"
|
|
||||||
location = appointment.get('ort', '')
|
|
||||||
else:
|
|
||||||
summary = "Blocked (Advoware)"
|
|
||||||
description = "Termin aus Advoware"
|
|
||||||
location = ""
|
|
||||||
|
|
||||||
event_body = {
|
|
||||||
'summary': summary,
|
|
||||||
'description': description,
|
|
||||||
'location': location,
|
|
||||||
'start': {
|
|
||||||
'dateTime': f"{start_date}T{start_time}",
|
|
||||||
'timeZone': 'Europe/Berlin',
|
|
||||||
},
|
|
||||||
'end': {
|
|
||||||
'dateTime': f"{end_date}T{end_time}",
|
|
||||||
'timeZone': 'Europe/Berlin',
|
|
||||||
},
|
|
||||||
'extendedProperties': {
|
|
||||||
'private': {
|
|
||||||
'advoware_frnr': str(appointment.get('frNr'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Event erstellen
|
|
||||||
created_event = service.events().insert(calendarId=calendar_id, body=event_body).execute()
|
|
||||||
print(f"Termin {appointment.get('frNr')} zu Google Calendar hinzugefügt")
|
|
||||||
return created_event
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fehler beim Sync zu Google für Termin {appointment.get('frNr')}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def sync_event_to_advoware(service, calendar_id, event, employee_kuerzel, context):
|
|
||||||
"""Synchronisiert ein Google Event zu Advoware (falls keine frNr vorhanden)"""
|
|
||||||
try:
|
|
||||||
# Prüfen ob bereits eine frNr vorhanden
|
|
||||||
extended_props = event.get('extendedProperties', {}).get('private', {})
|
|
||||||
frnr = extended_props.get('advoware_frnr')
|
|
||||||
|
|
||||||
if frnr:
|
|
||||||
# Bereits synchronisiert
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Neuen Termin in Advoware erstellen
|
|
||||||
advoware = AdvowareAPI(context)
|
|
||||||
|
|
||||||
# Start/End aus Google Event extrahieren
|
|
||||||
start = event.get('start', {}).get('dateTime', '')
|
|
||||||
end = event.get('end', {}).get('dateTime', '')
|
|
||||||
|
|
||||||
# Advoware-Termin erstellen
|
|
||||||
appointment_data = {
|
|
||||||
'text': event.get('summary', 'Google Calendar Termin'),
|
|
||||||
'notiz': event.get('description', ''),
|
|
||||||
'ort': event.get('location', ''),
|
|
||||||
'datum': start[:10] if start else datetime.datetime.now().strftime('%Y-%m-%d'),
|
|
||||||
'uhrzeitBis': start[11:19] if start else '09:00:00',
|
|
||||||
'datumBis': end[:10] if end else start[:10] if start else datetime.datetime.now().strftime('%Y-%m-%d'),
|
|
||||||
'sb': employee_kuerzel,
|
|
||||||
'anwalt': employee_kuerzel
|
|
||||||
}
|
|
||||||
|
|
||||||
result = await advoware.api_call('Termine', method='POST', json_data=appointment_data)
|
|
||||||
|
|
||||||
if result and isinstance(result, dict):
|
|
||||||
new_frnr = result.get('frNr')
|
|
||||||
if new_frnr:
|
|
||||||
# frNr zurück in Google Event schreiben
|
|
||||||
event['extendedProperties'] = event.get('extendedProperties', {})
|
|
||||||
event['extendedProperties']['private'] = event['extendedProperties'].get('private', {})
|
|
||||||
event['extendedProperties']['private']['advoware_frnr'] = str(new_frnr)
|
|
||||||
|
|
||||||
service.events().update(calendarId=calendar_id, eventId=event['id'], body=event).execute()
|
|
||||||
print(f"Neuer Advoware Termin erstellt: {new_frnr}, frNr in Google aktualisiert")
|
|
||||||
return new_frnr
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fehler beim Sync zu Advoware für Google Event {event.get('id')}: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def handler(req, context):
|
|
||||||
try:
|
|
||||||
# Konfiguration aus Request-Body
|
|
||||||
body = req.get('body', {})
|
|
||||||
full_content = body.get('full_content', True) # Default: volle Termindetails
|
|
||||||
|
|
||||||
print(f"Starte Advoware Calendar Sync, full_content: {full_content}")
|
|
||||||
|
|
||||||
# Google Calendar Service initialisieren
|
|
||||||
service = await get_google_service(context)
|
|
||||||
if not service:
|
|
||||||
print("Google Calendar Service nicht verfügbar. Sync wird übersprungen.")
|
|
||||||
return {
|
|
||||||
'status': 200,
|
|
||||||
'body': {
|
|
||||||
'status': 'skipped',
|
|
||||||
'reason': 'Google Calendar credentials not configured',
|
|
||||||
'total_synced': 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Alle Mitarbeiter abrufen
|
|
||||||
employees = await get_advoware_employees(context)
|
|
||||||
|
|
||||||
if not employees:
|
|
||||||
return {'status': 500, 'body': {'error': 'Keine Mitarbeiter gefunden'}}
|
|
||||||
|
|
||||||
total_synced = 0
|
|
||||||
|
|
||||||
for employee in employees:
|
|
||||||
kuerzel = employee.get('kuerzel') or employee.get('anwalt')
|
|
||||||
if not kuerzel:
|
|
||||||
print(f"Mitarbeiter ohne Kürzel übersprungen: {employee}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
print(f"Verarbeite Mitarbeiter: {kuerzel}")
|
|
||||||
|
|
||||||
# Google Calendar sicherstellen
|
|
||||||
calendar_id = await ensure_google_calendar(service, kuerzel, context)
|
|
||||||
if not calendar_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Termine aus beiden Systemen abrufen
|
|
||||||
advoware_appointments = await get_advoware_appointments(kuerzel, context)
|
|
||||||
google_events = await get_google_events(service, calendar_id, context)
|
|
||||||
|
|
||||||
# Advoware → Google syncen
|
|
||||||
google_frnrs = {event.get('extendedProperties', {}).get('private', {}).get('advoware_frnr') for event in google_events}
|
|
||||||
|
|
||||||
for appointment in advoware_appointments:
|
|
||||||
frnr = str(appointment.get('frNr'))
|
|
||||||
if frnr not in google_frnrs:
|
|
||||||
await sync_appointment_to_google(service, calendar_id, appointment, full_content, context)
|
|
||||||
total_synced += 1
|
|
||||||
|
|
||||||
# Google → Advoware syncen
|
|
||||||
for event in google_events:
|
|
||||||
await sync_event_to_advoware(service, calendar_id, event, kuerzel, context)
|
|
||||||
|
|
||||||
print(f"Advoware Calendar Sync abgeschlossen. {total_synced} Termine synchronisiert.")
|
|
||||||
|
|
||||||
return {
|
|
||||||
'status': 200,
|
|
||||||
'body': {
|
|
||||||
'status': 'completed',
|
|
||||||
'total_synced': total_synced,
|
|
||||||
'employees_processed': len([e for e in employees if e.get('kuerzel') or e.get('anwalt')])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Fehler beim Advoware Calendar Sync: {e}")
|
|
||||||
return {
|
|
||||||
'status': 500,
|
|
||||||
'body': {
|
|
||||||
'error': 'Internal server error',
|
|
||||||
'details': str(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
|
import redis
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
CALENDAR_SYNC_LOCK_KEY = 'calendar_sync_lock'
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
'type': 'api',
|
'type': 'api',
|
||||||
@@ -11,11 +15,34 @@ config = {
|
|||||||
|
|
||||||
async def handler(req, context):
|
async def handler(req, context):
|
||||||
try:
|
try:
|
||||||
|
# Prüfe ob bereits ein Sync läuft
|
||||||
|
redis_client = redis.Redis(
|
||||||
|
host=Config.REDIS_HOST,
|
||||||
|
port=int(Config.REDIS_PORT),
|
||||||
|
db=int(Config.REDIS_DB_CALENDAR_SYNC),
|
||||||
|
socket_timeout=Config.REDIS_TIMEOUT_SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
|
if redis_client.get(CALENDAR_SYNC_LOCK_KEY):
|
||||||
|
context.logger.info("Calendar Sync API: Sync bereits aktiv, überspringe")
|
||||||
|
return {
|
||||||
|
'status': 409,
|
||||||
|
'body': {
|
||||||
|
'status': 'conflict',
|
||||||
|
'message': 'Calendar sync bereits aktiv',
|
||||||
|
'triggered_by': 'api'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Konfiguration aus Request-Body
|
# Konfiguration aus Request-Body
|
||||||
body = req.get('body', {})
|
body = req.get('body', {})
|
||||||
full_content = body.get('full_content', True)
|
full_content = body.get('full_content', True)
|
||||||
|
|
||||||
print(f"Calendar Sync API aufgerufen, full_content: {full_content}")
|
context.logger.info(f"Calendar Sync API aufgerufen, full_content: {full_content}")
|
||||||
|
|
||||||
|
# Setze Lock für 30 Minuten (Sync sollte max 30 Minuten dauern)
|
||||||
|
redis_client.set(CALENDAR_SYNC_LOCK_KEY, 'api', ex=1800)
|
||||||
|
context.logger.info("Calendar Sync API: Lock gesetzt")
|
||||||
|
|
||||||
# Emit Event für den Sync
|
# Emit Event für den Sync
|
||||||
await context.emit({
|
await context.emit({
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import json
|
import json
|
||||||
|
import redis
|
||||||
|
from config import Config
|
||||||
|
|
||||||
|
CALENDAR_SYNC_LOCK_KEY = 'calendar_sync_lock'
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
'type': 'cron',
|
'type': 'cron',
|
||||||
@@ -10,7 +14,25 @@ config = {
|
|||||||
|
|
||||||
async def handler(event, context):
|
async def handler(event, context):
|
||||||
try:
|
try:
|
||||||
context.logger.info("Calendar Sync Cron: Starte automatische Synchronisation alle 15 Minuten")
|
# Prüfe ob bereits ein Sync läuft
|
||||||
|
redis_client = redis.Redis(
|
||||||
|
host=Config.REDIS_HOST,
|
||||||
|
port=int(Config.REDIS_PORT),
|
||||||
|
db=int(Config.REDIS_DB_CALENDAR_SYNC),
|
||||||
|
socket_timeout=Config.REDIS_TIMEOUT_SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
|
if redis_client.get(CALENDAR_SYNC_LOCK_KEY):
|
||||||
|
context.logger.info("Calendar Sync Cron: Sync bereits aktiv, überspringe")
|
||||||
|
return {
|
||||||
|
'status': 'skipped',
|
||||||
|
'reason': 'sync_already_running',
|
||||||
|
'triggered_by': 'cron'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Setze Lock für 30 Minuten (Sync sollte max 30 Minuten dauern)
|
||||||
|
redis_client.set(CALENDAR_SYNC_LOCK_KEY, 'cron', ex=1800)
|
||||||
|
context.logger.info("Calendar Sync Cron: Lock gesetzt, starte automatische Synchronisation alle 15 Minuten")
|
||||||
|
|
||||||
# Emit Event für den Sync
|
# Emit Event für den Sync
|
||||||
await context.emit({
|
await context.emit({
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ config = {
|
|||||||
'path': '/advoware/proxy',
|
'path': '/advoware/proxy',
|
||||||
'method': 'DELETE',
|
'method': 'DELETE',
|
||||||
'emits': [],
|
'emits': [],
|
||||||
'flows': ['basic-tutorial', 'advoware']
|
'flows': ['advoware']
|
||||||
}
|
}
|
||||||
|
|
||||||
async def handler(req, context):
|
async def handler(req, context):
|
||||||
@@ -23,10 +23,7 @@ async def handler(req, context):
|
|||||||
json_data = None
|
json_data = None
|
||||||
|
|
||||||
context.logger.info(f"Proxying request to Advoware: {method} {endpoint}")
|
context.logger.info(f"Proxying request to Advoware: {method} {endpoint}")
|
||||||
context.logger.info(f"Query params: {params}")
|
|
||||||
result = await advoware.api_call(endpoint, method=method, params=params, json_data=json_data)
|
result = await advoware.api_call(endpoint, method=method, params=params, json_data=json_data)
|
||||||
context.logger.info(f"Advoware API response received, length: {len(str(result)) if result else 0}")
|
|
||||||
context.logger.info(f"Response preview: {str(result)[:500] if result else 'None'}")
|
|
||||||
|
|
||||||
return {'status': 200, 'body': {'result': result}}
|
return {'status': 200, 'body': {'result': result}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ config = {
|
|||||||
'path': '/advoware/proxy',
|
'path': '/advoware/proxy',
|
||||||
'method': 'GET',
|
'method': 'GET',
|
||||||
'emits': [],
|
'emits': [],
|
||||||
'flows': ['basic-tutorial', 'advoware']
|
'flows': ['advoware']
|
||||||
}
|
}
|
||||||
|
|
||||||
async def handler(req, context):
|
async def handler(req, context):
|
||||||
@@ -23,10 +23,7 @@ async def handler(req, context):
|
|||||||
json_data = None
|
json_data = None
|
||||||
|
|
||||||
context.logger.info(f"Proxying request to Advoware: {method} {endpoint}")
|
context.logger.info(f"Proxying request to Advoware: {method} {endpoint}")
|
||||||
context.logger.info(f"Query params: {params}")
|
|
||||||
result = await advoware.api_call(endpoint, method=method, params=params, json_data=json_data)
|
result = await advoware.api_call(endpoint, method=method, params=params, json_data=json_data)
|
||||||
context.logger.info(f"Advoware API response received, length: {len(str(result)) if result else 0}")
|
|
||||||
context.logger.info(f"Response preview: {str(result)[:500] if result else 'None'}")
|
|
||||||
|
|
||||||
return {'status': 200, 'body': {'result': result}}
|
return {'status': 200, 'body': {'result': result}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ config = {
|
|||||||
'path': '/advoware/proxy',
|
'path': '/advoware/proxy',
|
||||||
'method': 'PUT',
|
'method': 'PUT',
|
||||||
'emits': [],
|
'emits': [],
|
||||||
'flows': ['basic-tutorial', 'advoware']
|
'flows': ['advoware']
|
||||||
}
|
}
|
||||||
|
|
||||||
async def handler(req, context):
|
async def handler(req, context):
|
||||||
@@ -23,11 +23,7 @@ async def handler(req, context):
|
|||||||
json_data = req.get('body')
|
json_data = req.get('body')
|
||||||
|
|
||||||
context.logger.info(f"Proxying request to Advoware: {method} {endpoint}")
|
context.logger.info(f"Proxying request to Advoware: {method} {endpoint}")
|
||||||
context.logger.info(f"Query params: {params}")
|
|
||||||
context.logger.info(f"Request body: {json_data}")
|
|
||||||
result = await advoware.api_call(endpoint, method=method, params=params, json_data=json_data)
|
result = await advoware.api_call(endpoint, method=method, params=params, json_data=json_data)
|
||||||
context.logger.info(f"Advoware API response received, length: {len(str(result)) if result else 0}")
|
|
||||||
context.logger.info(f"Response preview: {str(result)[:500] if result else 'None'}")
|
|
||||||
|
|
||||||
return {'status': 200, 'body': {'result': result}}
|
return {'status': 200, 'body': {'result': result}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
4
bitbylaw/types.d.ts
vendored
4
bitbylaw/types.d.ts
vendored
@@ -24,6 +24,8 @@ declare module 'motia' {
|
|||||||
'Advoware Proxy POST': ApiRouteHandler<Record<string, unknown>, unknown, never>
|
'Advoware Proxy POST': ApiRouteHandler<Record<string, unknown>, unknown, never>
|
||||||
'Advoware Proxy GET': ApiRouteHandler<Record<string, unknown>, unknown, never>
|
'Advoware Proxy GET': ApiRouteHandler<Record<string, unknown>, unknown, never>
|
||||||
'Advoware Proxy DELETE': ApiRouteHandler<Record<string, unknown>, unknown, never>
|
'Advoware Proxy DELETE': ApiRouteHandler<Record<string, unknown>, unknown, never>
|
||||||
'Advoware Calendar Sync': ApiRouteHandler<Record<string, unknown>, unknown, never>
|
'Calendar Sync Event Step': EventHandler<never, never>
|
||||||
|
'Calendar Sync Cron Job': CronHandler<{ topic: 'calendar.sync.triggered'; data: never }>
|
||||||
|
'Calendar Sync API Trigger': ApiRouteHandler<Record<string, unknown>, unknown, { topic: 'calendar.sync.triggered'; data: never }>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user