From 63de2d2dcf7c1161fa688dbd7624db910fc0e8c1 Mon Sep 17 00:00:00 2001 From: Floke Date: Mon, 9 Feb 2026 11:37:32 +0000 Subject: [PATCH] Dateien nach "docs/Zierl" hochladen --- docs/Zierl/Ziel_1.txt | 109 ++++++++++++ docs/Zierl/Ziel_2.txt | 21 +++ docs/Zierl/Ziel_3.txt | 112 ++++++++++++ docs/Zierl/Ziel_4.txt | 226 +++++++++++++++++++++++++ docs/Zierl/transkript_Manuel_Zierl.txt | 161 ++++++++++++++++++ 5 files changed, 629 insertions(+) create mode 100644 docs/Zierl/Ziel_1.txt create mode 100644 docs/Zierl/Ziel_2.txt create mode 100644 docs/Zierl/Ziel_3.txt create mode 100644 docs/Zierl/Ziel_4.txt create mode 100644 docs/Zierl/transkript_Manuel_Zierl.txt diff --git a/docs/Zierl/Ziel_1.txt b/docs/Zierl/Ziel_1.txt new file mode 100644 index 00000000..daad4d0a --- /dev/null +++ b/docs/Zierl/Ziel_1.txt @@ -0,0 +1,109 @@ +envelope = f""" + + + {APPLICATION_TOKEN} + {CONTEXT_IDENTIFIER} + + + + {SIGNED_SYSTEM_TOKEN} + {RETURN_TOKEN_TYPE} + + + +""".strip() + +headers = { + "Content-Type": "text/xml; charset=utf-8", + "SOAPAction": "http://www.superoffice.com/superid/partnersystemuser/0.1/IPartnerSystemUserService/Authenticate", +} + +resp = requests.post( + SOAP_URL, data=envelope.encode("utf-8"), headers=headers, timeout=30 +) +print(resp) +# --- Useful diagnostics if server responds with HTML or error --- +ct = resp.headers.get("Content-Type", "") +if "xml" not in ct.lower(): + print("Unexpected response (not XML). Status:", resp.status_code) + print("Content-Type:", ct) + print(resp.text[:1200]) + raise SystemExit("Check URL, SOAPAction, or required SOAP headers/values.") + +# --- Parse SOAP response per WSDL --- +root = ET.fromstring(resp.text) +ns = { + "s": "http://schemas.xmlsoap.org/soap/envelope/", + "tns": "http://www.superoffice.com/superid/partnersystemuser/0.1", +} + +# Fault? +fault = root.find(".//s:Fault", ns) +if fault is not None: + print(resp.text) + raise SystemExit("SOAP Fault returned. See XML above.") + +# Extract AuthenticationResponse +is_ok_el = root.find(".//tns:AuthenticationResponse/tns:IsSuccessful", ns) +token_el = root.find(".//tns:AuthenticationResponse/tns:Token", ns) +err_el = root.find(".//tns:AuthenticationResponse/tns:ErrorMessage", ns) + +is_ok = is_ok_el is not None and (is_ok_el.text or "").strip().lower() == "true" +token = token_el.text.strip() if token_el is not None and token_el.text else None +error_msg = (err_el.text or "").strip() if err_el is not None else "" + +if not is_ok: + print("Authenticate returned IsSuccessful = false") + print("ErrorMessage:", error_msg) + print(resp.text) + raise SystemExit("Authentication failed.") + +print("Authenticate succeeded.") +print("Token (truncated):", (token[:50] + "...") if token else None) + +# If ReturnTokenType=Jwt, you can inspect claims to find the SOTicket claim key +if RETURN_TOKEN_TYPE.lower() == "jwt" and token and "." in token: + + def b64url_decode(seg: str) -> bytes: + seg += "=" * ((4 - len(seg) % 4) % 4) + return base64.urlsafe_b64decode(seg.encode("ascii")) + + header_b64, payload_b64, sig_b64 = token.split(".", 2) + payload = json.loads(b64url_decode(payload_b64)) + print("JWT payload keys:", list(payload.keys())) + + # Try to locate a ticket-like claim (key name may vary by env) + ticket = None + for k, v in payload.items(): + if isinstance(v, str) and ( + "ticket" in k.lower() or v.startswith(("7T:", "8A:", "8C:")) + ): + ticket = v + break + + if ticket: + print("Extracted system-user ticket:", ticket) + # Example REST call using SOTicket + SO-AppToken headers: + TENANT_BASE = ( + "https://online.superoffice.com/Cust26703" # use your actual sodN host! + ) + rest_headers = { + "Authorization": f"SOTicket {ticket}", + "SO-AppToken": APPLICATION_TOKEN, # same value as ApplicationToken + "Accept": "application/json", + } + test = requests.get( + f"{TENANT_BASE}/api/v1/contact/1", headers=rest_headers, timeout=30 + ) + print("REST test:", test.status_code, test.text) + else: + print( + "No obvious 'ticket' claim found in JWT. Inspect payload above and pick the correct claim manually." + ) +else: + print( + "Returned token is not a JWT (or you requested SAML). Use the token as intended by your flow." + ) + \ No newline at end of file diff --git a/docs/Zierl/Ziel_2.txt b/docs/Zierl/Ziel_2.txt new file mode 100644 index 00000000..5d49e2a0 --- /dev/null +++ b/docs/Zierl/Ziel_2.txt @@ -0,0 +1,21 @@ +def make_signed_system_token(system_user_token: str, private_key_pem: str) -> str: + # 1) stamp in UTC like yyyyMMddHHmm + ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M") + to_sign = f"{system_user_token}.{ts}".encode("utf-8") + + # 2) load your RSA private key (PEM, PKCS#1 or PKCS#8) + key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), password=None + ) + + # 3) RSA-SHA256, PKCS#1 v1.5 padding, then Base64 (standard, not URL-safe) + signature = key.sign(to_sign, padding.PKCS1v15(), hashes.SHA256()) + sig_b64 = base64.b64encode(signature).decode("ascii") + + # 4) final SignedSystemToken + return f"{system_user_token}.{ts}.{sig_b64}" + + +# print(load_rsa_private_key_pem(PEM_STR)) # test loading key +print(make_signed_system_token(SYSTEM_USER_TOKEN, PEM_STR)) + \ No newline at end of file diff --git a/docs/Zierl/Ziel_3.txt b/docs/Zierl/Ziel_3.txt new file mode 100644 index 00000000..453c662f --- /dev/null +++ b/docs/Zierl/Ziel_3.txt @@ -0,0 +1,112 @@ +from typing import Dict, Optional, final + +import requests +import xmltodict +from rest_framework import status + + +class BaseAPI: + __api_name__ = "BaseAPI" + + def __init__(self): + pass + + def get_host(self) -> str: + raise NotImplementedError("Subclasses must implement _get_host() method") + + def build_headers(self) -> Dict[str, str]: + return {} + + def build_params(self) -> Dict[str, str]: + return {} + + def repair_authentication(self) -> bool: + """Callback to repair authentication, e.g. refresh token or re-login.""" + return False + + @final + def request( + self, + method: str, + path: str, + data: Optional[Dict | str | bytes] = None, + json: Optional[Dict] = None, + headers: Optional[Dict[str, str]] = None, + params: Optional[Dict[str, str]] = None, + custom_host: Optional[str] = None, + timeout: int = 30, + use_build_headers: bool = True, + ) -> Dict: + host = custom_host or self.get_host() + url = f"{host}{path}" + combined_headers: Optional[Dict[str, str]] = { + **(headers or {}), + } + if use_build_headers: + combined_headers = { + **self.build_headers(), + **(headers or {}), + } + combined_params: Optional[Dict[str, str]] = { + **self.build_params(), + **(params or {}), + } + + if combined_headers == {}: + combined_headers = None + if combined_params == {}: + combined_params = None + + try: + response = requests.request( + method=method, + url=url, + headers=combined_headers, + params=combined_params, + data=data, + json=json, + timeout=timeout, + ) + if ( + response.status_code == status.HTTP_401_UNAUTHORIZED + or response.status_code == status.HTTP_403_FORBIDDEN + ): + if self.repair_authentication(): + host = custom_host or self.get_host() + url = f"{host}{path}" + combined_headers: Optional[Dict[str, str]] = { + **self.build_headers(), + **(headers or {}), + } + combined_params: Optional[Dict[str, str]] = { + **self.build_params(), + **(params or {}), + } + response = requests.request( + method=method, + url=url, + headers=combined_headers, + params=combined_params, + data=data, + timeout=timeout, + ) + response.raise_for_status() + if response.status_code == status.HTTP_204_NO_CONTENT: + return {} + + content_type = response.headers.get("Content-Type", "") + + if "xml" in content_type: + return xmltodict.parse(response.text) + if "application/json" in content_type: + return response.json() + else: + raise ValueError( + "Unsupported Content-Type: {content_type} must be application/json" + ) + + except requests.exceptions.RequestException as e: + raise e + except ValueError as e: + raise e + \ No newline at end of file diff --git a/docs/Zierl/Ziel_4.txt b/docs/Zierl/Ziel_4.txt new file mode 100644 index 00000000..e73805bb --- /dev/null +++ b/docs/Zierl/Ziel_4.txt @@ -0,0 +1,226 @@ +import base64 +import time +from datetime import datetime, timezone +from pathlib import Path +from typing import Dict + +import jwt +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding +from django.conf import settings + +from core.plugins.base_api import BaseAPI +from core.plugins.super_office.ticket_store import RedisTicketStore, TicketRecord +from core.plugins.super_office.utils import to_iso_z_datetime + + +class SuperOfficeAPI(BaseAPI): + __api_name__ = "SuperOfficeAPI" + + def __init__(self): + super().__init__() + template_path = Path(__file__).with_name( + "partnersystemuser_envelope_template.xml" + ) + self.envelope_template = template_path.read_text(encoding="utf-8").strip() + self.webapi_base = None + self.ticket = None + self.redis_store = RedisTicketStore() + + def get_host(self) -> str: + if self.webapi_base is None: + self.set_ticket_and_webapi_base() + return self.webapi_base + + def build_headers(self) -> Dict[str, str]: + if self.ticket is None: + self.set_ticket_and_webapi_base() + return { + "Authorization": f"SOTicket {self.ticket}", + "SO-AppToken": settings.SUPER_OFFICE_APPLICATION_TOKEN, + "Accept": "application/json", + } + + def repair_authentication(self) -> bool: + new_ticket_data = self.get_ticket() + if ( + not new_ticket_data + or not new_ticket_data.get("ticket") + or not new_ticket_data.get("webapi_base") + ): + return False + self.redis_store.set( + TicketRecord( + ticket=new_ticket_data["ticket"], + webapi_base=new_ticket_data["webapi_base"], + issued_at=datetime.now(timezone.utc).timestamp(), + ) + ) + self.ticket = new_ticket_data["ticket"] + self.webapi_base = new_ticket_data["webapi_base"] + return True + + @staticmethod + def generate_signed_system_token(): + """ + # todo: doc + """ + ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M") + to_sign = f"{settings.SUPER_OFFICE_SYSTEM_USER_TOKEN}.{ts}".encode("utf-8") + key = serialization.load_pem_private_key( + settings.SUPER_OFFICE_PRIVATE_KEY_PEM.encode("utf-8"), password=None + ) + signature = key.sign(to_sign, padding.PKCS1v15(), hashes.SHA256()) + sig_b64 = base64.b64encode(signature).decode("ascii") + return f"{settings.SUPER_OFFICE_SYSTEM_USER_TOKEN}.{ts}.{sig_b64}" + + def get_ticket(self): + """ + todo: doc + todo: test + :return: + """ + signed_system_token = self.generate_signed_system_token() + envelope = self.envelope_template.format( + application_token=settings.SUPER_OFFICE_APPLICATION_TOKEN, + context_identifier=settings.SUPER_OFFICE_CONTEXT_IDENTIFIER, + signed_system_token=signed_system_token, + return_token_type=settings.SUPER_OFFICE_RETURN_TOKEN_TYPE, + ) + headers = { + "Content-Type": "text/xml; charset=utf-8", + "SOAPAction": "http://www.superoffice.com/superid/partnersystemuser/0.1/IPartnerSystemUserService/Authenticate", + } + + resp = self.request( + method="POST", + path="", + data=envelope.encode("utf-8"), + headers=headers, + custom_host=settings.SUPER_OFFICE_SOAP_URL, + use_build_headers=False, + ) + token = resp["Envelope"]["Body"]["AuthenticationResponse"]["Token"] + is_successful = resp["Envelope"]["Body"]["AuthenticationResponse"][ + "IsSuccessful" + ] + if not str(is_successful).lower() == "true": + return None + + claims = jwt.decode(token, options={"verify_signature": False}) + ticket = claims.get("http://schemes.superoffice.net/identity/ticket") + webapi_base = claims.get("http://schemes.superoffice.net/identity/webapi_url") + return {"ticket": ticket, "webapi_base": webapi_base} + + def set_ticket_and_webapi_base(self): + ticket_data = self.redis_store.get() + if ticket_data: + self.ticket = ticket_data.ticket + self.webapi_base = ticket_data.webapi_base + return + + lock_token = self.redis_store.acquire_refresh_lock() + try: + if lock_token is None: + # someone else is refreshing the ticket, wait and try to get it again + import time + + for _ in range(10): # ~2s total + time.sleep(0.2) + ticket_data = self.redis_store.get() + if ticket_data: + self.ticket = ticket_data.ticket + self.webapi_base = ticket_data.webapi_base + return + raise RuntimeError("Failed to get ticket after waiting") + # we have the lock, refresh the ticket + ok = self.repair_authentication() + if not ok: + raise RuntimeError("SuperOffice Authenticate failed") + finally: + if lock_token: + self.redis_store.release_refresh_lock(lock_token) + + @staticmethod + def _filter_since(since: datetime | None) -> str: + if since: + since = since.astimezone(timezone.utc).replace(microsecond=0) + since = since.strftime("%Y-%m-%dT%H:%M:%S") + return f"&$filter=(registeredDate+afterTime+'{since}' or updatedDate+afterTime+'{since}')" + return "" + + def _query_with_paging(self, base_query: str, since: datetime | None = None): + if since: + query = base_query + self._filter_since(since) + else: + query = base_query + values = [] + while True: + resp = self.request(method="GET", path=query) + values += resp.get("value", []) + if "odata.nextLink" in resp and resp["odata.nextLink"]: + query = resp["odata.nextLink"].split("Cust26703/api/")[-1] + time.sleep(1) # be nice to the API + continue + else: + break + return { + "value": values, + } + + def get_all_projects(self, since: datetime | None = None): + return self._query_with_paging( + base_query="v1/Project?$select=PrimaryKey,name", + since=since, + ) + + def get_all_contacts(self, since: datetime | None = None): + return self._query_with_paging( + base_query="v1/Contact?$select=PrimaryKey,name", + since=since, + ) + + def get_all_persons(self, since: datetime | None = None): + return self.request( + method="GET", + path="v1/MDOList/Associate", + ) + + def get_all_sale_types(self, since: datetime | None = None): + return self.request( + method="GET", + path="v1/List/SaleType/Items", + ) + + def get_all_sources(self, since: datetime | None = None): + return { + "value": self.request( + method="GET", + path="v1/List/Source/Items", + ) + } + + def get_contact_projects(self, contact_id: str): + return self.request( + method="GET", + path=f"v1/Contact/{contact_id}/Projects", + ) + + def add_sale(self, **kwargs): + for key in ["Contact", "Project", "Person", "Associate"]: + if key in kwargs: + kwargs[key] = {f"{key}Id": kwargs[key]} + + for key in ["SaleType", "Source"]: + if key in kwargs: + kwargs[key] = {"Id": kwargs[key]} + + if "Saledate" in kwargs: + kwargs["Saledate"] = to_iso_z_datetime(kwargs["Saledate"]) + + return self.request( + method="POST", + path="v1/Sale", + json=kwargs, + ) + \ No newline at end of file diff --git a/docs/Zierl/transkript_Manuel_Zierl.txt b/docs/Zierl/transkript_Manuel_Zierl.txt new file mode 100644 index 00000000..aa767c86 --- /dev/null +++ b/docs/Zierl/transkript_Manuel_Zierl.txt @@ -0,0 +1,161 @@ +Hier ist die wortgetreue Transkription des Gesprächs zwischen Manuel Zierl und Christian Godelmann: + +Manuel Zierl: ... gebaut, wenn die das brauchen. Und die wollten über dieses – ich weiß nicht, ob du das kennst – über dieses TED-Portal... Da stehen so Ausschreibungen auf dem Portal und die von Wackler aus dem, wie heißt das? Irgendwas Center... Proposal Center, genau. Die haben hier quasi so eine Suchvorlage, mit der sie das quasi jeden Tag durchsuchen. Und dann müssen sie sich da durchklicken. Und die wollten eine einfachere Möglichkeit haben, um das in ihr SuperOffice rüberzubekommen. Weil sie müssen es halt alles von Hand quasi übertragen. Und die Idee war auch, dass man möglicherweise dann vielleicht noch irgendwie eine KI dazwischenschalten könnte, die halt quasi schon mal vorfiltert. Aber das haben wir jetzt noch nicht gemacht. Aber das war so quasi die Idee. Und was ich dann gebaut habe für Wackler, also was die jetzt auch schon benutzen, ist so ein Tool, was... Es ist ein bisschen komplizierter, aber ich meine... Das verbindet quasi so verschiedene Steps miteinander. Kriegt am Anfang quasi die Sachen, die von dieser TED-Schnittstelle reinkommen, rein und gibt am Ende dann hier in SuperOffice die Sachen aus. + +Christian Godelmann: Also TED hat schon eine Schnittstelle von sich aus? + +Manuel Zierl: Genau, die haben sogar eine... die haben sogar eine öffentlich verfügbare... ja, egal... die haben sogar eine öffentlich verfügbare Schnittstelle, die du einfach so abgreifen kannst. Kannst auch hier alles suchen, aber wie gesagt, kannst du auch öffentlich suchen. Da haben sie nur gewisse Limits halt drauf. Darfst nur so und so viel pro Minute und sowas, aber ist ja wurscht. Genau, und dann, was die halt dann benutzen, ist quasi dieses Ding hier. Da ist halt... in dem, was man jetzt halt hier gerade gesehen hat, sind halt alle möglichen Filterungen drin, die die halt brauchen. Da ist halt irgendwie sowas zum Beispiel drin wie: Wie hoch ist das Auftragsvolumen? Dann haben sie irgendwelche Bestandskunden, die sie bevorzugen möchten, oder was weiß ich. Oder es gibt irgendwelche Kunden, die auf einer Blacklist stehen, die sie nicht annehmen, und sowas halt. Und wenn dann diese Filterung durch ist, kriegen die hier quasi ihre Ergebnisse und können die dann in SuperOffice übertragen, indem sie sie halt hier entweder mit Häkchen oder mit Kreuzchen versehen. Und das Ganze funktioniert dann noch so, dass die dann eben möglichst wenig machen müssen, dass halt hier... Quasi das sind alle Sachen, die die halt normalerweise bei SuperOffice eintragen. Und davon sind halt viele schon vorgefertigt. Also was jetzt zum Beispiel das Aktenzeichen, das Verkaufsdatum ist klar, dann der Besitzer ist eigentlich immer der Michael Melzer in dem SuperOffice dann. Öffentliche Ausschreibung, Proposal Center, das ist auch immer gleich. Und da haben wir halt dann versucht, das noch quasi so heuristisch zu matchen aus der Ausschreibung, ob es möglicherweise halt diese Firma schon gibt. Ich weiß jetzt nicht, ob es in dem Fall stimmt, keine Ahnung. Manchmal gibt es die, manchmal nicht. Max-Planck-Institut für Mathematik und Naturwissenschaften... Also wenn die quasi schon drin ist in der in SuperOffice, dann findet er die auch und kann die halt matchen. Und dann kann der das quasi einfach automatisch in SuperOffice übertragen von dem. Und das ist das, was ich gemacht habe. + +Christian Godelmann: Das ist in Python geschrieben? + +Manuel Zierl: Ich hab das in Python geschrieben, ja. Ich kann dir auch mal den Code bezüglich SuperOffice schreiben, weil es leider nicht ganz so einfach war und auch ein bisschen komplizierter war. Aber ich meine... Also nur zur Einordnung: Ich bin kein Entwickler, leider. Ja, ja, alles gut. Ich kann nur mal... Ich weiß jetzt auch schon ein bisschen her, dass ich es gemacht habe. Muss ich selbst mal reinschauen. Also ich habe... Es gibt quasi zwei unterschiedliche Schnittstellen, die ich hier jetzt in diesem Fall verwenden musste. Das ist einmal halt ein Exporter, das heißt quasi: Ich schicke was zu SuperOffice rüber und speichere das rein. Und ich brauche aber auch quasi Daten von denen, um halt hier diese Suche durchzuführen, wo ich dann zum Beispiel gucke: Gibt es diesen Kontakt schon? Ne? Und die SuperOffice API, die ist... die ist ja konfigurierbar über dieses dev.irgendwas... + +Christian Godelmann: Ja, den Account habe ich schon, beziehungsweise habe ich mir angelegt. + +Manuel Zierl: Genau, so sieht das aus. Und da kannst du dann ja quasi dieses Ding konfigurieren. Und es ist aber ein bisschen komplizierter, weil du musst quasi... Jetzt lass mich kurz hier reinschauen, das ist jetzt meine Testapplikation... genau, du hast hier eine Konfiguration. Du brauchst dann quasi... Also so wie das bei uns funktioniert hat: Wir konnten das am Anfang nur mit der Staging-Environment machen. Da kriegst du dann, also zumindest hier bei uns, kriegst du dann sozusagen dein eigenes SuperOffice, wo halt nichts drinsteht, was blank ist. Genau, wo du das quasi testen kannst. Und um das dann für Production freizuschalten, mussten wir dann das tatsächlich halt wirklich manuell anfragen bei SuperOffice. Und da gingen dann auch ein paar E-Mails hin und her, weil die überprüfen dann auch sozusagen, welche API-Calls du machst und geben dir dann auch noch mal Vorschläge, was du irgendwie vielleicht besser machen kannst, effizienter machen kannst und so. Und deswegen quasi zuerst dieser Schritt über die Staging-Environment. + +Christian Godelmann: Also hast du es dann als Customer Application eingestellt oder quasi mit deren Standard-Applikation? + +Manuel Zierl: Warte mal, lass mal kurz gucken, ich müsste das noch mal gucken... Also unser Ding war so: Als ich angefangen habe das zu implementieren, hatte ich noch gar keinen Zugriff auf das Wackler-SuperOffice. Ich habe mir quasi einfach nur einen eigenen Account bei SuperOffice gemacht und halt eine Staging-App erstellt. Und dann hatte ich quasi mein komplett separates SuperOffice, mit dem ich halt testen konnte. Und dann, als das funktioniert hatte, haben wir dann eine... beziehungsweise als ich dann von Wackler halt den Zugriff bekommen hatte, haben wir das Ganze auch erst mal auf einer Staging gemacht, die halt mit dem Wackler verbunden war. Dann, als das funktioniert hat, haben wir eben noch mal mit den Leuten von SuperOffice geschrieben und die haben dann noch ein paar Anpassungen gewollt. Und die haben wir dann implementiert und dann konnten wir das quasi in Production rüberschieben. Beziehungsweise gab es noch einen Schritt dazwischen, weil du kriegst dann auch noch mal so eine Pre-Production Environment, wo du quasi ein kopiertes SuperOffice von dem Wackler-SuperOffice bekommst. Also dass du quasi mit Livedaten noch mal testest. + +Christian Godelmann: Über welchen Zeitraum sprechen wir hier ungefähr? Also wie lange hast du gebraucht von...? + +Manuel Zierl: Lass mich überlegen. Ich habe angefangen, das zu implementieren, vermutlich so im November oder so, würde ich jetzt schätzen. Oktober... ja, vielleicht Oktober, ja. Aber also sehr viele Probleme waren tatsächlich am Ende dieses... Also ich meine, es geht ja hier um diese ganze Applikation, wo ja auch noch mehr dabei ist und so, ne? Also auch Frontend und so Sachen, habe ich alles ich alleine gebaut. Aber der größte Teil am Ende war tatsächlich diese SuperOffice-Integration, weil das eben ewig gedauert hat, bis das halt mit den E-Mails hin und her ging. Und dann musste halt irgendwie auch viel über den Dieter... den Dieter Tonch, unser Admin, laufen, weil halt ich ja natürlich keinen Admin-Zugriff auf das Wackler-SuperOffice habe und aber diese API-Geschichten halt über ihn dann freigeschaltet werden mussten und so. Und bis da in der Kommunikation alles hin und her ging, das hat ewig gedauert. Aber... + +Christian Godelmann: Die Frage ist... Also ich kann mal kurz skizzieren, was ich so vorhabe. Ich will mir auch quasi so ein External Enrichment Interface bauen, also so eine Art Schatten-CRM-System, wo ich im Prinzip – genau wie du das auch so ein bisschen vorhattest – durch ein Sprachmodell Accounts anreichern möchte. Also ich möchte, sag mal ganz plump gefragt, zuerst den Account einer Branche zuordnen. Und im Weiteren möchte ich mir die Webseite holen und diesen Webseiteninhalt eben sehr spezifische Fragen stellen zum Einsatz von Robo Planet. Also welcher Roboter geeignet wäre, welche Fläche die haben, also beziehungsweise die Reinigungsfläche möchte ich mir über verschiedene Proxies berechnen können. + +Manuel Zierl: Aber möchtest du das quasi für die Kunden machen, die ihr schon im SuperOffice drin habt? Oder möchtest du das für neue Kunden machen? + +Christian Godelmann: Für Accounts, die schon im CRM-System drin sind, beziehungsweise auch solche, die dann zukünftig hineinpurzeln. Dass die im Prinzip... Weil ich in SuperOffice jetzt selbst kein Sprachmodell zur Verfügung habe. Also ich komme ursprünglich aus der Dynamics-Welt, Dynamics 365, da habe ich ja zumindest die... da gibt es diese... + +Manuel Zierl: Also du möchtest quasi, wenn ich das richtig verstehe: Ihr habt euer Robo Planet SuperOffice, ihr habt da einfach eine Menge an Kunden und du möchtest irgendwie so eine KI, die da durchgehen kann und diese Kunden für dich analysieren kann? + +Christian Godelmann: Das würde ich auf einer... Also ich habe mir schon ein eigenes CRM-System im Prinzip nachgebaut. Das heißt, die beiden Systeme möchte ich über API miteinander verbinden. Das heißt, wenn eben die Daten in SuperOffice noch nicht angereichert sind, sollen die zu mir in meine Enrichment-Engine rüberwandern. Dort werden sie angereichert und die Daten werden dann wieder zurückgespielt nach SuperOffice. + +Manuel Zierl: Ja, das kann man machen, ja. Und das geht auch. Also ich kann dir natürlich dein Szenario nicht eins zu eins anwenden. Zum einen, weil grundsätzlich ihr wahrscheinlich jetzt einen anderen Mandanten habt und ich... + +Christian Godelmann: Ja, und diese SuperOffice, die sind auch irgendwie unterschiedlich konfiguriert. Also ich hatte dann auch Probleme, als ich dann von meinem quasi Developer-SuperOffice in das andere rüber bin, dass Sachen anders waren. Weil irgendwie viel auch damit zusammenhängt, wie euer SuperOffice überhaupt konfiguriert ist. Und ich wusste ja vorher gar nicht, wie das Wackler-SuperOffice aussieht. + +Manuel Zierl: Genau da bin ich jetzt auch. Also ich weiß auch von null, ne? Also ich habe am letzten Montag erst angefangen, das zur Einordnung... Ich meine, ich kann dir... Wo ich dir auf jeden Fall helfen kann, ist – weil da habe ich mich auch ein bisschen damit rumgeschlagen – ist diese SOAP-Token-Geschichte. Also diese API ist keine normale API, also keine schöne irgendwie REST... Also ich glaube, man könnte schon sagen, dass es eine REST-API ist, keine Ahnung. Aber es ist nicht so eine schöne, die dir so JSONs zurückgibt, sondern es ist ein bisschen komplizierter. Du hast hier... Warte mal, ich kann mal in meine Konfiguration reinschauen. Schön, dass du das alles hart reincodest, das ist mir sehr sympathisch. Ja, ja, das ist meine... Developer-Daten... die... also das ist dann nichts, was irgendwie online landet, natürlich. Aber das sieht dann quasi hier so aus. Warte mal, meine ganzen SuperOffice-Variablen sind hier. Genau, du hast einen Super... also okay, Redis egal, da werden bei mir die Sachen lokal gespeichert. Aber du brauchst quasi diesen Context-Identifier und der ist auch wieder kompliziert, weil den machst du über die Dev-Seite. Was ich Freitag halt schon probiert habe, ist – also da habe ich nicht das Server-to-Server genommen, sondern ich habe erst mal Type "None" angegeben bei dieser... bei dem Start. Aber das war wahrscheinlich auch schon nicht der richtige Start. Bis ich da draufgekommen bin, das hat ewig gedauert. Weil du musst diese Seite hier verwenden. Das ist total bescheuert und ich checke das auch nicht, warum die das so kompliziert machen. Aber... die kann ich dir auch einmal schicken. Denn diese Seite... Wenn ich jetzt hier in meine Testapplikation reingehe... ich brauche nämlich dieses... diesen SuperOffice System User Token. Und ich habe ewig nicht gecheckt, was das für ein Ding ist. Und ich bin dann draufgekommen, du musst Folgendes machen: Du hast hier – also hier generierst du deine Client ID, hast die für drei Stages. Und dann hast du hier ein Secret. Das speicherst du einmal ab, speicherst dir das irgendwo zwischen. Genau, und dann musst du auf diese Seite hier gehen, gibst hier deine Environment ein, gibst hier deine Client ID ein... ich kann das auch einmal machen für dich. Client ID... und dann gibst du da dein Secret ein... das habe ich wahrscheinlich irgendwo hier... SuperOffice Sandbox... das ist das hier... Die Environment oben muss auch stimmen. Dann drückst du auf Sign In. Dann wirst du jetzt weitergeleitet an deinen SuperOffice Account. Musst dich da möglicherweise... das habe ich schon gemacht... Genau, dann musst du dich da noch mal einloggen. Beziehungsweise hab ich das dann über Localhost habe ich das gemacht, weil... man braucht ja nur diese ID am Ende, oder? Genau. Ich habe dann jetzt hier zwei Accounts, weil ich halt diesen Wackler und den eigenen habe. Ich glaube, das ist der hier... wenn nicht, dann... ja, was auch immer. Auf jeden Fall wirst du dann weitergeleitet und dann kriegst du diesen SuperOffice System User Token. Das ist wahrscheinlich der, den ich habe. Mit dem man sich dann diese Tokens zur Laufzeit generieren kann? Genau. Wie sieht denn der aus? Weil der sah bei mir immer... der sah bei mir immer irgendwie so aus. Der hat immer dieses... Genau, also das hier ist jetzt der aus der Sandbox, so sah der aus. Der hat immer so diesen Namen von der Applikation vorne, dann minus S2S bei mir, weil es halt Server-to-Server ist. Ja, genau so sieht er aus. Nee, das ist der Refresh Token. Das ist der Refresh Token, das ist er nicht. Nee. Gut, dann war das was Falsches. Aber es mag sein, dass... Also der hat immer vorne so diesen Namen stehen von dem Ding. Und es gibt auch einen Refresh Token, den brauchst du aber nicht unbedingt. Das kannst du dann selber machen. Und dann habe ich das... Es läuft über XML, nicht über JSON, deswegen ist es extrem hässlich. Habe ich das einmal hier so nachgebaut gehabt, wie dieses Ding aussehen muss. Und da musst du so ein paar Sachen beachten, dass halt du hier das Richtige signierst, die richtigen Kontexte setzt, was auch immer. Und dann hier... ja, alles Mögliche an Python-Code. Ich kann dir den theoretisch auch einfach mal schicken. + +Christian Godelmann: Das würde mir wahrscheinlich ungefähr zwei Monate sparen. + +Manuel Zierl: Ja, das kann gut sein. Also da musst du natürlich noch mal durchgucken, weil da steht jetzt halt sowas drin wie hier dieser Customer 2... das ist natürlich meiner. Zwei, aber was haben wir denn? Ich schicke dir mal die... ich schicke dir mal die oben nicht mit, sonst kriege ich Ärger. Nee, also natürlich darfst du mir nichts schicken... Aber die kannst du dir ja dann denken, was die bedeuten. Also die sind schon halt nach dem benannt, was quasi was das ist. Und du musst dann nur einmal... ach so genau, warte mal... genau, dieses Ding zeige ich dir auch gleich noch mal, weil diesen Token musst du nämlich selber signieren dann. Das ist auch noch mal ganz eklig. Also das ist quasi dieser Code hier, mit dem du dieses XML an die schickst. Und ich weiß jetzt nicht, was macht denn dieses Beispiel hier... + +Christian Godelmann: Also mir geht es erst mal darum, einfach einen Durchstich zu bekommen und einen Get-Request zu machen. + +Manuel Zierl: Ja, genau. Ich gucke gerade mal. Genau, also der hier ruft quasi den Kontakt mit der ID 1 auf. Das ist das, was zurückkommen soll. Also wenn das schon mal funktioniert, dann weißt du, du kannst mit der API sprechen quasi, ne? Und dieses Token-Signieren, dafür brauchst du wiederum einen... ich weiß nicht, du weißt wahrscheinlich, was ein Private Key ist, ein Public Key und so. Die musst du ja bei dir abspeichern. Die musst du dann ja auch einmal hier eintragen, dass hier dein Private-Public-Key ist. Und also nicht wundern, dass das alles hier drinsteht, weil wie gesagt, hier habe ich das alles nur getestet in diesen Files. Und damit kannst du dir dann quasi diesen... über diesen System User Token, den du brauchst, und mit deinem Private Key kannst du dir quasi hier dann dieses Ding hier generieren. Das was dann hier steht, was ich hier dann eingetragen habe: Sign System User Token. + +Christian Godelmann: Okay. + +Manuel Zierl: Und das Ding kann ich dir auch noch mal schicken. Das wäre schon mal sehr hilfreich. Also nochmal, wie gesagt, ich bin kein Entwickler, das ist auch nur alles Web-Coding. + +Manuel Zierl: Ja, ich glaube, da kommst du aber auch ganz gut weiter, wenn du diesen Code quasi der ChatGPT oder Copilot, was auch immer gibst. Dann wird er sich da schon einigermaßen zurechtfinden, genau, glaube ich auch. Also ich habe auch die KI zur Hilfe verwendet. Das Problem war nur eher herauszufinden, was die überhaupt wollen, weil die Dokumentation auch nicht ganz so klar an manchen Stellen ist, zumindest meiner Meinung nach. Genau, aber das ist quasi der Schritt. Also ich habe hier mein... ich habe das halt hier so eingetragen, das war halt leichter, wenn du das dann theoretisch wirklich implementieren würdest, würdest du es natürlich anders machen. Aber wenn du es eh nur lokal bei dir laufen hast, dann kannst du es theoretisch auch ins Skript mit eingeben. Also ist auch kein Problem. Wenn du das natürlich irgendwo auf einen Server laden würdest, würdest du das natürlich nicht machen. Aber genau... Jetzt lass mich überlegen, ob ich da sonst noch was habe... ich hatte das auch mal versucht mit irgendwie schöner zu machen, aber das hat nicht funktioniert. Ich kann nur mal gucken, wie das in meiner tatsächlichen Applikation jetzt aussieht. Weil eine Doku hast du dafür nicht, oder? Also es gibt halt die SuperOffice Dokumentation. Die kenne ich ja. Aber ich meine jetzt für deine... Ich habe für mich selbst nicht so viel Dokumentation geschrieben, nee, da ich halt der Einzige war, der das implementiert hat. Ich meine, was ich dir geben könnte... warte mal, ich könnte dir quasi diese... das ist quasi mein... was brauchst denn du? Du brauchst Import und Export eigentlich auch beide, ne? Also ich kann dir diesen Code auch geben. Der ist halt aber ein bisschen spezifischer auf quasi das, was ich gebaut habe. Also da ist halt quasi... Du hast halt hier irgendwie so ein... also musst halt dann dich mit der KI durchfragen. Weil das hier quasi... warte mal, ach so, das ist viel Code. Weil sehr viel von diesem Code, der ist halt spezifisch für das, was ich gebaut habe. Und da gibt es halt dann irgendwie auch so Sachen, die halt irgendwelche Sachen rausfiltern, die für Wackler spezifisch wichtig waren, beziehungsweise halt auch Sachen, die mir das ermöglichen, hier in meinem... in meiner grafischen Oberfläche die Sachen halt einfach für mich zu konfigurieren, aber halt auch angepasst auf das, was Wackler möchte. + +Christian Godelmann: Der Flow hast du dann... ist dann auch Teil deiner App oder...? + +Manuel Zierl: Ja, ja, also das hat Wackler auch. Die benutzen das natürlich nicht, weil ich das hier für die konfiguriere. Aber das macht es halt dann auch für mich einfacher, das für die zu konfigurieren, weil ich halt dann zum Beispiel jetzt hier... die wollen die Bestandskunden getaggt haben. Dann habe ich quasi hier einen Filter, der mir rausfiltert: Was sind Bestandskunden? Und das kann ich halt dann hier quasi konfigurieren über irgendwelche Felder, wo ich sage: Wie heißt der? Hier zum Beispiel... das kann man dann auch alles eintragen und so. Dann taggt der das und gibt das halt an den Nächsten weiter. Und das ist halt so ein bisschen die Idee und so funktio-... also das sind alles nur irgendwelche Filter hier, die das dann irgendwie anders taggen. Und am Ende läuft es dann in dieses Ding hier rein und das ist dann das, was die hier oben in dieser Liste sehen. Und genau, hier kommen noch mal die SuperOffice-Daten rein, die er braucht, um das zu matchen. Und dieses Ding hier quasi, das brauche ich dann nicht mehr wirklich zu konfigurieren. Also ein paar Sachen sind schon noch drin, aber da geht es halt dann noch mal darum, was für Felder die überhaupt eintragen möchten in das SuperOffice und woher sie diese Werte kriegen. Und das ist dann noch mal so konfiguriert, dass es mit dem zusammenpasst, was die quasi hier eingegeben haben. Deswegen ist sehr viel von dem Code wahrscheinlich für dich nicht besonders nützlich, aber es ist trotzdem halt... warte mal, vielleicht kann ich auch noch mal... es gibt hier... der Exporter... Add Sale müsste das irgendwie heißen oder so... Sale... Build Sale Payload, genau. API Add Sale. Ach so, beziehungsweise vielleicht ist das, weil mein Exporter und Importer sehr spezifisch ist, aber das Ding genau... nee, ich schicke dir das Ding, das ist wesentlich besser. Da das habe ich quasi nur um die API herumgebaut. Es kann natürlich auch nur die Sachen, die ich gebraucht habe. Also das sind quasi diese alle hier: Also Get All Projects, Context, Person, Sale Type, Source... + +Christian Godelmann: Ja, mehr gibt es ja nicht, ja. + +Manuel Zierl: Es gibt Tausende mehr. Aber davon brauchte ich halt nichts. Und ich weiß halt nicht, aber wenn du jetzt auch quasi nur einen Sale einfügen willst und vielleicht irgendwie noch ein paar Kontaktdaten haben willst... + +Christian Godelmann: Kontakt ist ein Account in dem Sinne, das ist... + +Manuel Zierl: Kontakt ist ein... ist das, was du bei SuperOffice quasi Firma nennst, genau. Ja, genau. Das hat mich auch total verwirrt, weil genau bei Get Persons ist nämlich dann dieses tatsächliche Person im SuperOffice. Muss man auch erst mal draufkommen, genau. Und hier ist tatsächlich auch schon das Meiste drin, was du brauchst, weil der macht dir auch schon... also der... ja gut, der erbt halt wieder von der hier, ne... Ich schicke dir mal... ich weiß nicht, Vererbung sagt dir was? Schon, ja. Okay. Also es gibt quasi eine Base-Klasse, die ich mir geschrieben habe, weil ich benutze ja mehrere APIs, die halt so ein paar Grundfunktionalitäten drin hatte. Und... + +Christian Godelmann: Schreibst du mir dazu, welche Klasse das jeweils ist, weil oder welche Funktion...? + +Manuel Zierl: Ach so, das steht oben ja, also immer Class Base API. Das siehst... kannst du dann auch die KI fragen, die merkt das dann. Und hier diese API erbt quasi von der, übernimmt quasi ein paar Funktionen von der, baut halt auch auf der auf. Und die kann tatsächlich auch so ein paar Tricks eben schon, dass sie hier diese Tokens generiert und sowas und die auch richtig einpackt und alles, den Header richtig setzt, alles Mögliche. Wo allerdings du möglicherweise... ja genau, JWT-Tokens und so, ganz kompliziert. Wo du aber aufpassen musst möglicherweise, ich weiß nicht, ob ich das Redis hier auch drin habe... ja, genau. Ich verwende – weil meine Applikation ja auf einem Server läuft – muss ich quasi diese Tokens, die ich habe, irgendwo zwischenspeichern. Und das macht man normalerweise in einem Redis-Store. Das ist so was wie eine Art Datenbank, die aber halt wesentlich schneller ist, weil du aber keine irgendwie relationalen Geschichten hast und so was. Also quasi um einfach schnell irgendwelche Strings reinzuspeichern und wieder rauszuholen. Das heißt quasi, das wirst du vermutlich nicht brauchen beziehungsweise halt irgendwie überschreiben müssen. Aber das ist halt rein server-spezifischer Code quasi, den ich gebraucht habe, aber den du vermutlich nicht brauchen wirst. Aber sonst glaube ich, kannst du ziemlich viel von hier wahrscheinlich wiederverwenden. Genau. Ja. Sonst... ja... es gibt auch einen ganz... also was ganz okay ist, ist die... ist der Support von denen. Man kann denen schreiben und die antworten auch, zumindest. Also ich muss erst mal so die ersten Grundlagen sicherstellen, wie gesagt. Es ist halt, wie gesagt, ziemlich nervig, weil sie sehr kompliziert ist, aber dafür haben sie auch einen Support, der das dann meistens schon irgendwie erklärt. Und du schlägst dir dann meistens danach den Kopf und denkst so: Ja, okay, so wie du es erklärst, macht es ja schon irgendwie Sinn, aber da wäre ich jetzt alleine nicht draufgekommen. Aber genau. Und was ich hier tatsächlich auch implementiert hatte, ich weiß nicht, wie wichtig das für dich ist... und zwar ist bei mir quasi das Problem gewesen, dass ich ja quasi alle Firmen, die im SuperOffice von Wackler drin sind, quasi hier ja durchsuchbar machen möchte. Und die haben keine richtig guten Suchendpunkte. Das heißt, ich lade die quasi alle zu mir auf den Server und durchsuche sie. Dieses Laden von allen Daten, das dauert aber so zehn Minuten und ist auch relativ belastend für den SuperOffice-Server. Und das waren dann auch ein paar von den API-Calls, die die moniert haben, wo sie gesagt haben, das wollen sie nicht, dass irgendwie alle jeden Tag alle Kontakte abgefragt werden und so. Aber es gibt da auch Endpunkte, die... mit denen du quasi filtern kannst, welche... dass du nur nach den Neuesten filterst und so was. Also das gibt es alles schon so, dass du das relativ gut machen kannst. Ich weiß nicht, wie viele Kunden ihr jetzt bei Robo Planet drin habt... + +Christian Godelmann: Leider viel zu viele, also das sind über 60.000. + +Manuel Zierl: Wackler hat, glaube ich, auch irgendwie über 100.000 oder so drin gehabt, deswegen also ist das dann relativ langsam. Aber okay, ja, dann wirst du so was vielleicht auch... + +Christian Godelmann: Ja, aber das Initial-Sync wird halt ein bisschen länger dauern, aber danach dann über ein Diff oder so was... + +Manuel Zierl: Genau, das ist eben die Idee, dass du halt dann immer sagst: Okay... Unserer, der ruft das, glaube ich, alle sechs Stunden oder so auf, nee, alle 12 Stunden, genau. Und fragt aber quasi immer nur, was die in den letzten 12 Stunden reingekommen ist. Und das sind dann natürlich relativ wenige. An neuen Firmen oder auch an Details an den neuen Firmen oder...? Auf alles bezogen jetzt hier. Wobei bei manchen habe ich es, glaube ich, tatsächlich nicht implementiert. Zum Beispiel bei den Sources, die verändern sich aber auch nicht. Also da quasi... und das sind irgendwie nur 10, 20 Stück oder so, deswegen ist das da wurscht. Da habe ich mir die Mühe nicht gemacht. Aber vor allem eben bei den Kontakten und bei den... ich glaube nicht mal bei den Personen war es ein Problem, weil das sind halt auch irgendwie 200, 300, 400, 500 Stück oder so was, aber das juckt den nicht. Das Problem sind dann die Kontakte, wenn du wirklich 100.000 hast und das dann halt wirklich... Und die vor allem auch relativ groß sind, weil irgendwie an einer Person hängt meistens auch nicht ganz so viel Daten dran wie dann an einem Projekt oder an einer Firma dran hängt. Weil da ja wieder... die Firmen ja wieder Projekte haben und so was und dann wird das Ding halt riesig, was da rauskommt. + +Christian Godelmann: Aber das arbeitest du jetzt über einen Diff, also du schaust, wann du das das letzte Mal geholt hast und welche Accounts seitdem dann aktualisiert wurden? Auch wenn jetzt wie gesagt ein Feld geändert wurde, also wenn die Straße oder so was angepasst wurde? + +Manuel Zierl: Ja, das mache ich schon. Das mache ich schon. Also beziehungsweise halt nur auf die Daten bezogen, die ich auch brauche. Weil das sind bei mir nicht so viele. Also so was wie jetzt zum Beispiel die Straße oder so was, das brauche ich nicht. Aber den Namen zum Beispiel, den Namen bräuchte ich halt zum Beispiel, weil der Name ja dann das ist, was hier angezeigt wird, wenn du dann hier was eingibst. + +Christian Godelmann: Jetzt mal für den Namen gefragt, weil du das Max-Planck-Institut hast. Hast du da auch eine Art Fuzzy-Lookup oder intelligenten Lookup? Weil dass der Name eins zu eins übereinstimmt, ist ja... + +Manuel Zierl: Ja, das habe ich... aber das wird für dich wahrscheinlich nicht sinnvoll sein. Ich habe das gemacht mit... muss ich mal überlegen, wo müsste das stehen... das müsste wahrscheinlich vielleicht hier stehen. Also ich, was ich mache ist: Ich lade die Daten quasi einmal zu mir und speichere sie in eine Postgres-Datenbank. Und in der Postgres-Datenbank kannst du eine... so eine Fuzzy-Suche machen, dafür gibt es ganz gute Libraries. Fuzzy-Search-Strings... Aber wie gesagt, dann müsstest du jetzt bei dir Postgres und so was installieren. Ich weiß nicht, wie sinnvoll das jetzt für dich ist. Es ist halt relativ effizient für das, was der Server dann... + +Christian Godelmann: Also ich habe das Thema... es ist häufiger, also zum einen muss ich erst mal gucken, was wir intern an Duplikaten in SuperOffice haben, weil da gibt es auch tonnenweise Duplikate schon, die muss ich erst mal sinnvoll bereinigen. Aber zukünftig ist auch so ein Ongoing-Thema. Weißt du, wenn du auf eine Messe gehst und dann fragen die: Wer von unseren Interessenten ist denn dort? Und so weiter. Da hast du ja ständig dieses Thema, dass du zwei Listen miteinander vergleichen musst. Hatte ich in der Vergangenheit auch schon mal ein Tool für entwickelt. + +Manuel Zierl: Ja, also das... da ist es, wie gesagt, natürlich schneller, wenn du das in einer Datenbank hast, in einer sinnvollen Datenbank wie Postgres. Aber ich weiß nicht, wenn du sagst, du bist kein Entwickler, ob dir das jetzt so viel Spaß machen wird, deine eigene Postgres-Instanz aufzusetzen, weiß ich nicht. Aber theoretisch sinnvoll ist es, keine Frage. + +Christian Godelmann: Ich hatte SQLite-Datenbanken, das reicht für meinen Kram meistens schon. + +Manuel Zierl: Kann auch schon gut reichen. Die haben halt dann meistens nicht so effiziente und smarte Algorithmen wie die hier drin, weil der schon... da kannst du wirklich sehr viel machen. Also da kannst du... ich weiß dann gar nicht mehr, was ich... ich müsste dann... ich müsste noch mal gucken, was ich da genau verwendet habe für einen Algorithmus, der dann sehr gut funktioniert hat. Der quasi über die Heuristik suchen kann, ob dieser String, den du hast, so ähnlich in diesem String drin ist. Weil du ja auch nicht willst, dass der dieselbe Länge haben muss. Es gibt zum Beispiel die Levenshtein-Distance, die halt aber quasi dann auch größer wird, wenn halt die Strings unterschiedliche Längen haben und so. + +Christian Godelmann: Ja, da sind wir genau bei der Thematik. Aber das habe ich... da habe ich in der Vergangenheit auch schon zwei, drei Wochen dran rumgedocktert, bis ich da ein System hatte, was dann... + +Manuel Zierl: Ja, das kann ich mir gut vorstellen. + +Christian Godelmann: Das klingt so trivial, aber da bist du dann schnell mal eine Woche am Testen und gucken und dann findest du den einen und den anderen wieder nicht. + +Manuel Zierl: Hmm... muss mal kurz... ja... doch, doch, doch, das ist es genau. Der Trigram-Match... in Postgres heißt der pg_trgm, Trigram. Das ist irgend so eine Heuristik, die quasi... die quasi eben darauf basiert, dass das nicht von der Länge abhängig ist. Und deswegen hat das jetzt in dem Fall sehr gut funktioniert, weil ich habe da ein paar durchprobiert und für den Use-Case war es auf jeden Fall sehr gut. + +Christian Godelmann: Ja, ich habe mich da auch mit Stopwords und so weiter, oder wenn... ja, ach Gott. + +Manuel Zierl: Genau, aber sonst ja... + +Christian Godelmann: Ja cool. Ich glaube, du hast mir schon super geholfen. Ich werde sicherlich noch mal auf dich zurückkommen. Das wird wahrscheinlich noch ein bisschen dauern, bis ich so weit bin und erst mal gucke, ob die Grundlage, wie gesagt, ob wir die richtige Lizenz haben oder nicht für die Robo Planet Instanz. + +Manuel Zierl: Ja, da musst du erst mal gucken. Genau. Wie gesagt, kann ich dir nicht viel helfen. Ich weiß nur, dass wir von Conclimate beziehungsweise Subtain, wir haben schon irgendwie... Also unser SuperOffice ist irgendwie so eine Unterinstanz vom Wackler-SuperOffice, soweit ich weiß. Und das wird dann irgendwie auch darüber geregelt. Also ich habe dann... Also quasi ich als Entwickler von Subtain stehe auch irgendwo im SuperOffice von Wackler drin. Aber ich kenne mich da jetzt auch nicht so aus. Also ich habe sowieso dann mit dem... mit unserem Conclimate Subtain SuperOffice habe ich sowieso nichts zu tun. Das machen ja eh alle die Sales-Leute oder die Controller, keine Ahnung. + +Christian Godelmann: Wer ist denn eigentlich euer Admin-Kontakt dann für die Lizenz zum Beispiel? Das ist die Frau Grillenberger, oder? + +Manuel Zierl: Also... für SuperOffice? Ja. Also wen ich mir vorstellen könnte, wäre auch der Dieter, weil der quasi halt der Superadmin von Wackler ist. Dass der das von euch auch macht, das weiß ich nicht. Und ansonsten haben wir halt... habe ich halt mit unserem... also mit unserem Subtain Conclimate SuperOffice habe ich halt gar nichts zu tun. Ich habe halt jetzt mehr mit dem Wackler-Office SuperOffice zu tun, weil ich halt für die das gebaut habe. Aber in Subtain gebe ich da... habe ich da keine Schnittstelle dazu. Deswegen weiß ich das nicht. Da müsstest du wahrscheinlich halt einfach mal bei dir im Unternehmen... ich kenne mich ja mit eurer Struktur bei Robo Planet nicht aus. + +Christian Godelmann: Die Webcom sagt dir aber nichts oder...? Weil die hat mir jetzt zumindest meinen Account eingerichtet, die Frau Grillenberger. + +Manuel Zierl: Ja, doch, die sagt mir schon was. Die könnte da auf jeden Fall was drüber wissen. Aber die ist... ist die Grillenberger, die ist ja auch von Wackler, nicht von jetzt Robo Planet, ne? Ist das so? Weiß ich nicht, ich glaube... Was steht denn da in ihrer E-Mail-Adresse? Webcom... Also das ist eine Externe. Ach so, das ist noch mal ein externer Dienstleister, okay, nee, dann weiß ich es wirklich nicht. Keine Ahnung. + +Christian Godelmann: Ja, vielleicht ist das ein Dienstleister von Wackler. + +Manuel Zierl: Nee, da würde ich einfach mal jemanden aus dem Sales bei euch vielleicht fragen oder so, weil die, glaube ich, arbeiten viel mit SuperOffice und die wissen das bestimmt. Also würde mich wundern, wenn nicht, keine Ahnung. Zu dem Dieter habe ich auch einen ganz guten Draht. Wir haben mal früher haben wir zwei Jahre zusammengearbeitet. + +Christian Godelmann: Ach witzig, okay. Wo? + +Manuel Zierl: Mobile X, das war diese Fleetmanagement-Geschichte. + +Manuel Zierl: Ach okay, witzig. Von daher habe ich einen ganz guten Draht. Der hat mir auch so ein bisschen... Ja, der ist auch nett, den mag ich auch ganz gerne. Das ist immer cool drauf und mit dem habe ich auch ab und zu immer mal was gehabt, weil ich bin ja der einzige Entwickler aus Subtain. Und er hat aber ja überall diese Admin-Geschichten und so. Da muss man manchmal dann, wenn es irgendwie über irgendwelche DNS-Geschichten oder so was geht... unsere DNS-Server von Wackler eigentlich Wackler gehören irgendwie oder so. + +Christian Godelmann: Auf welchem Server läuft eigentlich deine Geschichte jetzt hier? + +Manuel Zierl: Das ist ein Heroku-Server. Kennst du Heroku? Ist so... kannst du dir so ein bisschen vorstellen wie halt AWS, aber in ein bisschen einfacher. Ein bisschen teurer theoretisch, wenn du nicht so Credits bekommst, weil du irgendwie ein Unternehmen bist, keine Ahnung, und die dir dann Credits schenken. Aber theoretisch eigentlich ein bisschen teurer, dafür aber sehr viele Sachen gemanagt, die du, wenn du dich mit AWS auseinandersetzt, einfach komplizierter sind. Weil bei AWS brauchst du ja eigentlich fast wieder irgendwie eine eigene Person, musst du einstellen, die sich nur darum kümmert. + +Christian Godelmann: Nee, weil ich hab nämlich jetzt auch... also meine Entwicklungen sind momentan alle Container-basiert und es läuft jetzt bei mir zu Hause auf der DiskStation. + +Manuel Zierl: Ja, das ist auch okay, also... + +Christian Godelmann: Das soll aber perspektivisch dann hier irgendwo in eine Linux-Umgebung... + +Manuel Zierl: Ja, und also wir haben auch tatsächlich hier noch keine schöne URL, deswegen sieht die auch immer noch so aus. Also das ist halt die Heroku, die er verpasst. Aber die haben sowieso so ein bisschen mehr noch vor da, dass die... Also sie sollen dafür Wackler, glaube ich, auch noch weiter dran entwickeln, weil die haben da jetzt schon wieder ein paar Vorstellungen, was sie anders, besser haben wollen und so. Und dann wird das vielleicht auch so eine eigene Wackler-interne Tool-Webseite oder so, keine Ahnung, was die da genau vorhaben. Genau. + +Christian Godelmann: Okay, nee, das hilft mir schon mal sehr, sehr weiter. Habt ihr dann bei... ihr habt bei Robo Planet müsstet ihr aber auch Entwickler haben, ne? Gar nicht? Okay... Also ich bin ja auch kein Entwickler. Schön wär's, aber... Interessant. Na ja, aber irgendwer muss die Roboter programmieren, oder? Oder macht das irgendwie ein externer Dienstleister oder so? Das ist aber natürlich auch was anderes, ist ja Hardware. + +Christian Godelmann: Das ist was anderes, ja. Also ich glaube da... Da macht, glaube ich, auch der Support macht viel mit, aber das ist dann nichts in Richtung Programmierung. Das ist die... ja, quasi Setup-Programm und dann läuft er einmal rum und dann hat er seinen Floor-Plan. + +Manuel Zierl: Okay, ja. Cool. + +Christian Godelmann: Aber vielleicht weiß ich auch noch zu wenig, was die Kollegen so alles machen. Erst die Woche angefangen, von daher... das macht Sinn. + +Manuel Zierl: Ja, das macht Sinn. + +Christian Godelmann: Nee cool. Ich werde mir erst mal anschauen, was du mir geschickt hast. Ich werde es mal durch meine KI jagen, mal gucken, ob sie mir hilft, das irgendwo in die Wege zu leiten, die ich brauche. + +Manuel Zierl: Ja. Genau. Kannst mich auch... kannst mir auch schreiben. Ich bin nicht jeden Tag im Büro, also ich mache auch Homeoffice. Ich habe einen echt weiten Weg hierher, aber so ein, zwei Mal die Woche bin ich schon da. Am Freitag arbeite ich nicht, aber... alles gut. Genau, aber sonst kannst du dich melden, ja. + +Christian Godelmann: Super. Ich hoffe, dass ich nicht zu viel Zeit am Anfang geklaut habe. + +Manuel Zierl: Ach, alles gut. Geholfen? + +Christian Godelmann: Das werde ich beurteilen. Ich hoffe es. Super, Manuel. Dann vielen Dank schon mal. + +Manuel Zierl: Ja, kein Problem. Ich sitze übrigens noch vorne beim Aufzug, im rechten Flügel direkt die erste Tür links. Da wo die... wir ziehen aber auch jetzt im Februar, glaube ich, um. Ja, schon gehört, da tut sich was. Aber schauen wir mal. Also danke schon mal, ne? Ciao. \ No newline at end of file