Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New DKB Connection after migration fails with KeyError: '999' #178

Open
peteh opened this issue Nov 30, 2024 · 1 comment
Open

New DKB Connection after migration fails with KeyError: '999' #178

peteh opened this issue Nov 30, 2024 · 1 comment

Comments

@peteh
Copy link

peteh commented Nov 30, 2024

Describe the bug
DKB moved their connection to a new fints server that supports the DKB App for TANs instead of the old deprecated TAN app.

However, I cannot get it to work with the bootstrap example.

*Bank I tested this with
Name of the bank:
FinTS URL: https://fints.dkb.de/fints
Supported TAN mechanisms are supposed to be: DKB-App (decoupled), chipTAN manuell, chipTAN QR

Expected behavior
I'd expect the bootstrap example to work and ask for a TAN from the DKB App.

In the following code I would expect this to return a list of possible TAN mediums, however the list is empty:

f.fetch_tan_mechanisms()
mechanisms = list(f.get_tan_mechanisms().items())
print(mechanisms)

Code required to reproduce

import datetime
import getpass
import logging
import sys
import json
from decimal import Decimal
from fints.client import FinTS3PinTanClient, NeedTANResponse, FinTSUnsupportedOperation
from fints.hhd.flicker import terminal_flicker_unix
from fints.utils import minimal_interactive_cli_bootstrap

logging.basicConfig(level=logging.DEBUG)

config_file = open("config_dkb.json", "r")
config = json.load(config_file)
config_file.close()

bank_account = config.get("bank_accounts")[0]
product_id="xxxx"
client_args = (
    bank_account.get("blz"), # Your bank's BLZ
    bank_account.get("login_name"), # Your login name
    bank_account.get("pin"), # Your banking PIN
    bank_account.get("fints_url"),
)

tan_medium = bank_account.get("tan_medium")
f = FinTS3PinTanClient(*client_args, product_id = product_id) 
f.fetch_tan_mechanisms()
mechanisms = list(f.get_tan_mechanisms().items())
print(mechanisms)
#f.selected_tan_medium = bank_account.get("tan_medium")
minimal_interactive_cli_bootstrap(f)


def ask_for_tan(response):
    print("A TAN is required")
    print(response.challenge)
    if getattr(response, 'challenge_hhduc', None):
        try:
            terminal_flicker_unix(response.challenge_hhduc)
        except KeyboardInterrupt:
            pass
    tan = input('Please enter TAN:')
    return f.send_tan(response, tan)


# Open the actual dialog
with f:
    # Since PSD2, a TAN might be needed for dialog initialization. Let's check if there is one required
    if f.init_tan_response:
        ask_for_tan(f.init_tan_response)

    # Fetch accounts
    accounts = f.get_sepa_accounts()
    if isinstance(accounts, NeedTANResponse):
        accounts = ask_for_tan(accounts)
    if len(accounts) == 1:
        account = accounts[0]
    else:
        print("Multiple accounts available, choose one")
        for i, mm in enumerate(accounts):
            print(i, mm.iban)
        choice = input("Choice: ").strip()
        account = accounts[int(choice)]

    # Test pausing and resuming the dialog
    dialog_data = f.pause_dialog()

client_data = f.deconstruct(including_private=True)

f = FinTS3PinTanClient(*client_args, from_data=client_data, product_id = product_id)
with f.resume_dialog(dialog_data):
    while True:
        operations = [
            "End dialog",
            "Fetch transactions of the last 30 days",
            "Fetch transactions of the last 120 days",
            "Fetch transactions XML of the last 30 days",
            "Fetch transactions XML of the last 120 days",
            "Fetch information",
            "Fetch balance",
            "Fetch holdings",
            "Fetch scheduled debits",
            "Fetch status protocol",
            "Make a simple transfer"
        ]

        print("Choose an operation")
        for i, o in enumerate(operations):
            print(i, o)
        choice = int(input("Choice: ").strip())
        try:
            if choice == 0:
                break
            elif choice == 1:
                res = f.get_transactions(account, datetime.date.today() - datetime.timedelta(days=30),
                                         datetime.date.today())
                while isinstance(res, NeedTANResponse):
                    res = ask_for_tan(res)
                print(res)
                print("Found", len(res), "transactions")
            elif choice == 2:
                res = f.get_transactions(account, datetime.date.today() - datetime.timedelta(days=120),
                                         datetime.date.today())
                while isinstance(res, NeedTANResponse):
                    res = ask_for_tan(res)
                print(res)
                print("Found", len(res), "transactions")
            elif choice == 3:
                res = f.get_transactions_xml(account, datetime.date.today() - datetime.timedelta(days=30),
                                             datetime.date.today())
                while isinstance(res, NeedTANResponse):
                    res = ask_for_tan(res)
                print("Found", len(res[0]) + len(res[1]), "XML documents")
            elif choice == 4:
                res = f.get_transactions_xml(account, datetime.date.today() - datetime.timedelta(days=120),
                                             datetime.date.today())
                while isinstance(res, NeedTANResponse):
                    res = ask_for_tan(res)
                print("Found", len(res[0]) + len(res[1]), "XML documents")
            elif choice == 5:
                print(f.get_information())
            elif choice == 6:
                res = f.get_balance(account)
                while isinstance(res, NeedTANResponse):
                    res = ask_for_tan(res)
                print(res)
            elif choice == 7:
                res = f.get_holdings(account)
                while isinstance(res, NeedTANResponse):
                    res = ask_for_tan(res)
                print(res)
            elif choice == 8:
                res = f.get_scheduled_debits(account)
                while isinstance(res, NeedTANResponse):
                    res = ask_for_tan(res)
                print(res)
            elif choice == 9:
                res = f.get_status_protocol()
                while isinstance(res, NeedTANResponse):
                    res = ask_for_tan(res)
                print(res)
            elif choice == 10:
                res = f.simple_sepa_transfer(
                    account=accounts[0],
                    iban=input('Target IBAN:'),
                    bic=input('Target BIC:'),
                    amount=Decimal(input('Amount:')),
                    recipient_name=input('Recipient name:'),
                    account_name=input('Your name:'),
                    reason=input('Reason:'),
                    endtoend_id='NOTPROVIDED',
                )

                if isinstance(res, NeedTANResponse):
                    ask_for_tan(res)
        except FinTSUnsupportedOperation as e:
            print("This operation is not supported by this bank:", e)

Log output / error message
It runs into

Exception has occurred: KeyError
'999'
  File "/home/pete/Programming/fints/app/test_fints.py", line 32, in <module>
    minimal_interactive_cli_bootstrap(f)
KeyError: '999'
DEBUG:fints.connection:Sending >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        fints.message.FinTSCustomerMessage([
            fints.segments.message.HNHBK3( # Nachrichtenkopf
                header = fints.formals.SegmentHeader('HNHBK', 1, 3), # Segmentkopf
                message_size = 389, # Größe der Nachricht (nach Verschlüsselung und Komprimierung)
                hbci_version = 300, # HBCI-Version
                dialog_id = '0', # Dialog-ID
                message_number = 1, # Nachrichtennummer
 
 [...]
                                ),
                        ),
                        fints.segments.auth.HITANS6(
                            header = fints.formals.SegmentHeader('HITANS', 49, 6, 4), # Segmentkopf
                            max_number_tasks = 1, # Maximale Anzahl Aufträge
                            min_number_signatures = 1, # Anzahl Signaturen mindestens
                            security_class = fints.formals.SecurityClass.NONE, # Sicherheitsklasse: Kein Sicherheitsdienst erforderlich
                            parameter = fints.formals.ParameterTwostepTAN6(
                                    onestep_method_allowed = False,
                                    multiple_tasks_allowed = False,
                                    task_hash_algorithm = fints.formals.TaskHashAlgorithm.NONE, # Auftrags-Hashwertverfahren: Auftrags-Hashwert nicht unterstützt
                                    twostep_parameters = [
                                                fints.formals.TwoStepParameters6(
                                                    security_function = '910', # Sicherheitsfunktion kodiert
                                                    tan_process = '2', # TAN-Prozess
                                                    tech_id = 'HHD1.3.0', # Technische Identifikation TAN-Verfahren
                                                    zka_id = None, # ZKA TAN-Verfahren
                                                    zka_version = None, # Version ZKA TAN-Verfahren
                                                    name = 'chipTAN manuell', # Name des Zwei-Schritt-Verfahrens
                                                    max_length_input = 6, # Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren
                                                    allowed_format = fints.formals.AllowedFormat.NUMERIC, # Erlaubtes Format im Zwei-Schritt-Verfahren: numerisch
                                                    text_return_value = 'TAN-Nummer', # Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren
                                                    max_length_return_value = 2048, # Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren
                                                    multiple_tans_allowed = False, # Mehrfach-TAN erlaubt
                                                    tan_time_dialog_association = fints.formals.TANTimeDialogAssociation.NOT_ALLOWED, # TAN Zeit- und Dialogbezug: TAN nicht zeitversetzt / dialogübergreifend erlaubt
                                                    cancel_allowed = False, # Auftragsstorno erlaubt
                                                    sms_charge_account_required = fints.formals.SMSChargeAccountRequired.MUST_NOT, # SMS-Abbuchungskonto erforderlich: SMS-Abbuchungskonto darf nicht angegeben werden
                                                    principal_account_required = fints.formals.PrincipalAccountRequired.MUST_NOT, # Auftraggeberkonto erforderlich: Auftraggeberkonto darf nicht angegeben werden
                                                    challenge_class_required = False, # Challenge-Klasse erforderlich
                                                    challenge_structured = True, # Challenge strukturiert
                                                    initialization_mode = fints.formals.InitializationMode.CLEARTEXT_PIN_NO_TAN, # Initialisierungsmodus: Initialisierungsverfahren mit Klartext-PIN und ohne TAN
                                                    description_required = fints.formals.DescriptionRequired.MUST_NOT, # Bezeichnung des TAN-Medium erforderlich: Bezeichnung des TAN-Mediums darf nicht angegeben werden
                                                    response_hhd_uc_required = False, # Antwort HHD_UC erforderlich
                                                ),
                                                fints.formals.TwoStepParameters6(
                                                    security_function = '913', # Sicherheitsfunktion kodiert
                                                    tan_process = '2', # TAN-Prozess
                                                    tech_id = 'Q1S', # Technische Identifikation TAN-Verfahren
                                                    zka_id = 'Secoder_UC', # ZKA TAN-Verfahren
                                                    zka_version = '1.2.0', # Version ZKA TAN-Verfahren
                                                    name = 'chipTAN QR', # Name des Zwei-Schritt-Verfahrens
                                                    max_length_input = 6, # Maximale Länge des Eingabewertes im Zwei-Schritt-Verfahren
                                                    allowed_format = fints.formals.AllowedFormat.NUMERIC, # Erlaubtes Format im Zwei-Schritt-Verfahren: numerisch
                                                    text_return_value = 'TAN-Nummer', # Text zur Belegung des Rückgabewertes im Zwei-Schritt-Verfahren
                                                    max_length_return_value = 2048, # Maximale Länge des Rückgabewertes im Zwei-Schritt-Verfahren
                                                    multiple_tans_allowed = False, # Mehrfach-TAN erlaubt
                                                    tan_time_dialog_association = fints.formals.TANTimeDialogAssociation.NOT_ALLOWED, # TAN Zeit- und Dialogbezug: TAN nicht zeitversetzt / dialogübergreifend erlaubt
                                                    cancel_allowed = False, # Auftragsstorno erlaubt
                                                    sms_charge_account_required = fints.formals.SMSChargeAccountRequired.MUST_NOT, # SMS-Abbuchungskonto erforderlich: SMS-Abbuchungskonto darf nicht angegeben werden
                                                    principal_account_required = fints.formals.PrincipalAccountRequired.MUST_NOT, # Auftraggeberkonto erforderlich: Auftraggeberkonto darf nicht angegeben werden
                                                    challenge_class_required = False, # Challenge-Klasse erforderlich
                                                    challenge_structured = True, # Challenge strukturiert
                                                    initialization_mode = fints.formals.InitializationMode.CLEARTEXT_PIN_NO_TAN, # Initialisierungsmodus: Initialisierungsverfahren mit Klartext-PIN und ohne TAN
                                                    description_required = fints.formals.DescriptionRequired.MUST_NOT, # Bezeichnung des TAN-Medium erforderlich: Bezeichnung des TAN-Mediums darf nicht angegeben werden
                                                    response_hhd_uc_required = False, # Antwort HHD_UC erforderlich
                                                ),
                                        ],
                                ),
                        ),
                        fints.segments.base.FinTS3Segment(
                            header = fints.formals.SegmentHeader('HITANS', 50, 7, 4), # Segmentkopf
                            _additional_data = ['1', '1', '0', ['N', 'N', '0', '940', '2', 'SealOne', 'Decoupled', None, 'DKB App', None, None, 'DKB App', '2048', 'N', '1', 'N', '0', '0', 'N', 'J', '00', '0', 'N', None, '999', '1', '1', None, None, '910', '2', 'HHD1.3.0', None, None, 'chipTAN manuell', '6', '1', 'TAN-Nummer', '2048', 'N', '1', 'N', '0', '0', 'N', 'J', '00', '0', 'N', None, None, None, None, None, None, '913', '2', 'Q1S', 'Secoder_UC', '1.2.0', 'chipTAN QR', '6', '1', 'TAN-Nummer', '2048', 'N', '1', 'N', '0', '0', 'N', 'J', '00', '0', 'N', None, None, None, None, None]],
                        ),
                        fints.segments.dialog.HISYN4( # Synchronisierungsantwort
                            header = fints.formals.SegmentHeader('HISYN', 51, 4, 5), # Segmentkopf
                            system_id = 'B8iy5L9UH0ecp2M01bB3bw', # Kundensystem-ID
                        ),
                    ]),
            ),
            fints.segments.message.HNHBS1( # Nachrichtenabschluss
                header = fints.formals.SegmentHeader('HNHBS', 52, 1), # Segmentkopf
                message_number = 1, # Nachrichtennummer
            ),
        ])

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

DEBUG:fints.connection:Sending >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        fints.message.FinTSCustomerMessage([
            fints.segments.message.HNHBK3( # Nachrichtenkopf
                header = fints.formals.SegmentHeader('HNHBK', 1, 3), # Segmentkopf
                message_size = 403, # Größe der Nachricht (nach Verschlüsselung und Komprimierung)
                hbci_version = 300, # HBCI-Version
                dialog_id = 'DKB_701vVgGgcvhD0cbilbhWLNW0ki', # Dialog-ID
                message_number = 2, # Nachrichtennummer
            ),
            fints.segments.message.HNVSK3( # Verschlüsselungskopf, version 3
                header = fints.formals.SegmentHeader('HNVSK', 998, 3), # Segmentkopf
                security_profile = fints.formals.SecurityProfile( # Sicherheitsprofil
                        security_method = fints.formals.SecurityMethod.PIN, # Sicherheitsverfahren
                        security_method_version = 1, # Version des Sicherheitsverfahrens
                    ),
                security_function = '998', # Sicherheitsfunktion, kodiert
                security_role = fints.formals.SecurityRole.ISS, # Rolle des Sicherheitslieferanten, kodiert: Erfasser, Erstsignatur
                security_identification_details = fints.formals.SecurityIdentificationDetails( # Sicherheitsidentifikation, Details
                        identified_role = fints.formals.IdentifiedRole.MS, # Message Sender
                        cid = None,
                        identifier = 'B8iy5L9UH0ecp2M01bB3bw',
                    ),
                security_datetime = fints.formals.SecurityDateTime( # Sicherheitsdatum und -uhrzeit
                        date_time_type = fints.formals.DateTimeType.STS, # Sicherheitszeitstempel
                        date = datetime.date(2024, 11, 30),
                        time = datetime.time(21, 32, 53, 912532),
                    ),
                encryption_algorithm = fints.formals.EncryptionAlgorithm( # Verschlüsselungsalgorithmus
                        usage_encryption = fints.formals.UsageEncryption.OSY, # Owner Symmetric
                        operation_mode = fints.formals.OperationMode.CBC, # Cipher Block Chaining
                        encryption_algorithm = fints.formals.EncryptionAlgorithmCoded.TWOKEY3DES, # 2-Key-Triple-DES
                        algorithm_parameter_value = b'\x00\x00\x00\x00\x00\x00\x00\x00',
                        algorithm_parameter_name = fints.formals.AlgorithmParameterName.KYE, # Symmetrischer Schlüssel, verschlüsselt mit symmetrischem Schlüssel
                        algorithm_parameter_iv_name = fints.formals.AlgorithmParameterIVName.IVC, # Initialization value, clear text
                    ),
                key_name = fints.formals.KeyName( # Schlüsselname
                        bank_identifier = fints.formals.BankIdentifier(
                                country_identifier = '280',
                                bank_code = '12030000',
                            ),
                        user_id = 'xxx',
                        key_type = fints.formals.KeyType.V, # Schlüsselart: Chiffrierschlüssel
                        key_number = 0,
                        key_version = 0,
                    ),
                compression_function = fints.formals.CompressionFunction.NULL, # Komprimierungsfunktion: Keine Kompression
            ),
            fints.segments.message.HNVSD1( # Verschlüsselte Daten, version 1
                header = fints.formals.SegmentHeader('HNVSD', 999, 1), # Segmentkopf
                data = fints.types.SegmentSequence([ # Daten, verschlüsselt
                        fints.segments.message.HNSHK4( # Signaturkopf, version 4
                            header = fints.formals.SegmentHeader('HNSHK', 2, 4), # Segmentkopf
                            security_profile = fints.formals.SecurityProfile( # Sicherheitsprofil
                                    security_method = fints.formals.SecurityMethod.PIN, # Sicherheitsverfahren
                                    security_method_version = 1, # Version des Sicherheitsverfahrens
                                ),
                            security_function = '999', # Sicherheitsfunktion, kodiert
                            security_reference = '4961088', # Sicherheitskontrollreferenz
                            security_application_area = fints.formals.SecurityApplicationArea.SHM, # Bereich der Sicherheitsapplikation, kodiert: Signaturkopf und HBCI-Nutzdaten
                            security_role = fints.formals.SecurityRole.ISS, # Rolle des Sicherheitslieferanten, kodiert: Erfasser, Erstsignatur
                            security_identification_details = fints.formals.SecurityIdentificationDetails( # Sicherheitsidentifikation, Details
                                    identified_role = fints.formals.IdentifiedRole.MS, # Message Sender
                                    cid = None,
                                    identifier = 'B8iy5L9UH0ecp2M01bB3bw',
                                ),
                            security_reference_number = 1, # Sicherheitsreferenznummer
                            security_datetime = fints.formals.SecurityDateTime( # Sicherheitsdatum und -uhrzeit
                                    date_time_type = fints.formals.DateTimeType.STS, # Sicherheitszeitstempel
                                    date = datetime.date(2024, 11, 30),
                                    time = datetime.time(21, 32, 53, 912294),
                                ),
                            hash_algorithm = fints.formals.HashAlgorithm( # Hashalgorithmus
                                    usage_hash = '1',
                                    hash_algorithm = '999',
                                    algorithm_parameter_name = '1',
                                ),
                            signature_algorithm = fints.formals.SignatureAlgorithm( # Signaturalgorithmus
                                    usage_signature = '6',
                                    signature_algorithm = '10',
                                    operation_mode = '16',
                                ),
                            key_name = fints.formals.KeyName( # Schlüsselname
                                    bank_identifier = fints.formals.BankIdentifier(
                                            country_identifier = '280',
                                            bank_code = '12030000',
                                        ),
                                    user_id = 'xxx',
                                    key_type = fints.formals.KeyType.S, # Schlüsselart: Signierschlüssel
                                    key_number = 0,
                                    key_version = 0,
                                ),
                        ),
                        fints.segments.dialog.HKEND1( # Dialogende, version 1
                            header = fints.formals.SegmentHeader('HKEND', 3, 1), # Segmentkopf
                            dialog_id = 'DKB_701vVgGgcvhD0cbilbhWLNW0ki', # Dialog-ID
                        ),
                        fints.segments.message.HNSHA2( # Signaturabschluss, version 2
                            header = fints.formals.SegmentHeader('HNSHA', 4, 2), # Segmentkopf
                            security_reference = '4961088', # Sicherheitskontrollreferenz
                            user_defined_signature = fints.formals.UserDefinedSignature( # Benutzerdefinierte Signatur
                                    pin = '***',
                                ),
                        ),
                    ]),
            ),
            fints.segments.message.HNHBS1( # Nachrichtenabschluss
                header = fints.formals.SegmentHeader('HNHBS', 5, 1), # Segmentkopf
                message_number = 2, # Nachrichtennummer
            ),
        ])

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): fints.dkb.de:443
DEBUG:urllib3.connectionpool:https://fints.dkb.de:443 "POST /fints HTTP/11" 200 None
DEBUG:fints.connection:Received <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
        fints.message.FinTSInstituteMessage([
            fints.segments.message.HNHBK3( # Nachrichtenkopf
                header = fints.formals.SegmentHeader('HNHBK', 1, 3), # Segmentkopf
                message_size = 283, # Größe der Nachricht (nach Verschlüsselung und Komprimierung)
                hbci_version = 300, # HBCI-Version
                dialog_id = 'DKB_701vVgGgcvhD0cbilbhWLNW0ki', # Dialog-ID
                message_number = 2, # Nachrichtennummer
                reference_message = fints.formals.ReferenceMessage( # Bezugsnachricht
                        dialog_id = 'DKB_701vVgGgcvhD0cbilbhWLNW0ki',
                        message_number = 2,
                    ),
            ),
            fints.segments.base.FinTS3Segment(
                header = fints.formals.SegmentHeader('HNVSK', 998, 3), # Segmentkopf
                _additional_data = [['PIN', '1'], '998', '1', '2', ['1', '20241130', '213254'], ['2', '2', '13', b'\x00\x00\x00\x00\x00\x00\x00\x00', '5', '1'], ['280', '12030000', '12030000', 'V', '0', '0'], '0'],
            ),
            fints.segments.message.HNVSD1( # Verschlüsselte Daten, version 1
                header = fints.formals.SegmentHeader('HNVSD', 999, 1), # Segmentkopf
                data = fints.types.SegmentSequence([ # Daten, verschlüsselt
                        fints.segments.dialog.HIRMG2( # Rückmeldungen zur Gesamtnachricht
                            header = fints.formals.SegmentHeader('HIRMG', 2, 2), # Segmentkopf
                            responses = [ # Rückmeldung
                                        fints.formals.Response( # Rückmeldung
                                            code = '0100',
                                            reference_element = None,
                                            text = 'Dialog beendet',
                                        ),
                                ],
                        ),
                        fints.segments.dialog.HIRMS2( # Rückmeldungen zu Segmenten
                            header = fints.formals.SegmentHeader('HIRMS', 3, 2, 3), # Segmentkopf
                            responses = [ # Rückmeldung
                                        fints.formals.Response( # Rückmeldung
                                            code = '0100',
                                            reference_element = None,
                                            text = 'Dialog beendet',
                                        ),
                                ],
                        ),
                    ]),
            ),
            fints.segments.message.HNHBS1( # Nachrichtenabschluss
                header = fints.formals.SegmentHeader('HNHBS', 4, 1), # Segmentkopf
                message_number = 2, # Nachrichtennummer
            ),
        ])

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

Additional context
Add any other context about the problem here.

@jeriox
Copy link

jeriox commented Dec 9, 2024

For me the new DKB FinTS server also does not work, while the library works flawlessly with the old one.

I moved my issue described below to #179

However I get one step further: get_tan_mechanisms() correctly displays the decoupled TAN method (DKB App) as the only available method for me. When starting the client, DKB returns the challenge to confirm the login in the app. The app then sends a push message where I can confirm the login attempt. However, when I send an empty tan with f.send_tan(), the dialog is cancelled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants