diff --git a/django/importer/__init__.py b/django/importer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/importer/apps.py b/django/importer/apps.py new file mode 100644 index 0000000..ff9040f --- /dev/null +++ b/django/importer/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ImporterConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "importer" diff --git a/django/importer/forms.py b/django/importer/forms.py new file mode 100644 index 0000000..24911fa --- /dev/null +++ b/django/importer/forms.py @@ -0,0 +1,17 @@ +from django import forms + +IMPORTER_CHOICES = ( + ("BIP-0329", "BIP-329 .jsonl"), + #("BIP-0329-7z-enc" , "BIP-329 (encrypted) .7z"), + ("csv-bluewallet", "BlueWallet .csv"), + ("csv-bitbox", "BitBox .csv"), + ("pocket-accointing", "Pocket Accointing .csv") +) + + +class UploadFileForm(forms.Form): + labelbase_id = forms.IntegerField(widget=forms.HiddenInput()) + import_type = forms.ChoiceField( + choices=IMPORTER_CHOICES #, widget=forms.HiddenInput() + ) + file = forms.FileField() diff --git a/django/importer/pocket.py b/django/importer/pocket.py new file mode 100644 index 0000000..bbc88ca --- /dev/null +++ b/django/importer/pocket.py @@ -0,0 +1,49 @@ +import csv +import json +from labelbase.models import Label +from labelbase.models import Labelbase + + +def validate_csv_format(csv_file_path): + expected_sequence = ['withdraw', 'order', 'deposit'] + with open(csv_file_path, newline='') as csvfile: + csv_reader = csv.reader(csvfile) + next(csv_reader) # Skip the header + for row in csv_reader: + if len(row) != 11 or row[0] not in expected_sequence: + return False + expected_index = expected_sequence.index(row[0]) + if expected_index == 0 and expected_index != 0: + return False + expected_sequence.append(expected_sequence.pop(0)) + return True + + +def parse_csv_to_json(csv_file_path): + json_list = [] + with open(csv_file_path, newline='') as csvfile: + csv_reader = csv.reader(csvfile) + next(csv_reader) # Skip the header + group = [] + for row in csv_reader: + if row[0] == 'withdraw': + if group: + json_list.append(group) + group = [] + data = { + 'transactionType': row[0], + 'date': row[1], + 'inBuyAmount': row[2], + 'inBuyAsset': row[3], + 'outSellAmount': row[4], + 'outSellAsset': row[5], + 'feeAmount': row[6] if row[6] else "", + 'feeAsset': row[7] if row[7] else "", + 'classification': row[8] if row[8] else "", + 'operationId': row[9] if row[9] else "", + 'comments': row[10] if row[10] else "" + } + group.append(data) + if group: + json_list.append(group) + return json_list diff --git a/django/importer/templatetags/__init__.py b/django/importer/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django/importer/templatetags/importer_tags.py b/django/importer/templatetags/importer_tags.py new file mode 100644 index 0000000..abc4fbf --- /dev/null +++ b/django/importer/templatetags/importer_tags.py @@ -0,0 +1,34 @@ +from django import template +from importer.forms import UploadFileForm + +register = template.Library() + +@register.simple_tag +def genericlabeluploadform(labelbase_id): + form = UploadFileForm() + form.fields["labelbase_id"].initial = labelbase_id + form.fields["import_type"].initial = "BIP-0329" + return form + +@register.simple_tag +def bip0329labeluploadform(labelbase_id): + form = UploadFileForm() + form.fields["labelbase_id"].initial = labelbase_id + form.fields["import_type"].initial = "BIP-0329" + return form + + +@register.simple_tag +def csvBlueWalletlabeluploadform(labelbase_id): + form = UploadFileForm() + form.fields["labelbase_id"].initial = labelbase_id + form.fields["import_type"].initial = "csv-bluewallet" + return form + + +@register.simple_tag +def csvBitBoxLabeluploadform(labelbase_id): + form = UploadFileForm() + form.fields["labelbase_id"].initial = labelbase_id + form.fields["import_type"].initial = "csv-bitbox" + return form diff --git a/django/importer/views.py b/django/importer/views.py new file mode 100644 index 0000000..c9a5acc --- /dev/null +++ b/django/importer/views.py @@ -0,0 +1,184 @@ +from django.http import HttpResponseRedirect +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +import os +import json +from labelbase.models import Labelbase +from labelbase.serializers import LabelSerializer +from django.shortcuts import get_object_or_404 +from django.contrib import messages +import decimal + + +from .forms import UploadFileForm +from tempfile import NamedTemporaryFile + +EOLSTOP = [b"", "", None, "\n"] + + +def handle_uploaded_file(f): + fp = NamedTemporaryFile(delete=False) + for chunk in f.chunks(): + fp.write(chunk) + return fp + + +@login_required +def upload_labels(request): + """ + Used to import labels manually using files. + """ + + if request.method == "POST": + form = UploadFileForm(request.POST, request.FILES) + if form.is_valid(): + imported_lables = 0 + labelbase = get_object_or_404( + Labelbase, + id=form.cleaned_data.get("labelbase_id", ""), + user_id=request.user.id, + ) + fp = handle_uploaded_file(request.FILES["file"]) + fp.seek(0) + # BIP-0329 + if form.cleaned_data.get("import_type", "") == "BIP-0329": + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + data = json.loads(buf) + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + elif form.cleaned_data.get("import_type", "") == "BIP-0329-7z-enc": + # TODO: Implement + pass + # Bitbox App + elif form.cleaned_data.get("import_type", "") == "csv-bitbox": + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + try: + buf = str(buf.decode("utf-8")) + sbuf = buf.split(",") + # Time,Type,Amount,Unit,Fee,Address,Transaction ID,Note + for elem in [("tx", 6), ("addr", 5)]: + data = { + "type": elem[0], + "ref": sbuf[elem[1]], + "label": " ".join(sbuf[7:]), + } + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + else: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}".'.format(buf), + ) + except Exception as ex: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}", {}.'.format(buf, ex), + ) + # Pocket Accointing + elif form.cleaned_data.get("import_type", "") == "pocket-accointing": + fp.close() + csv_file_path = fp.name + mempool_api = labelbase.get_mempool_api() + from .pocket import validate_csv_format, parse_csv_to_json + if validate_csv_format(csv_file_path): + for item in parse_csv_to_json(csv_file_path): + #label = "Got {} {} for {} {} in tx=\"{}\" ref=\"{} #Pocket\" ".format(item[0].get('outSellAmount'), item[1].get('inBuyAsset') , item[2].get('inBuyAmount'), item[2].get('inBuyAsset') , item[0].get('operationId'), item[1].get('operationId')) + + label = "Got {} {} for {:.2f} {} with reference: {} #Pocket".format(item[0].get('outSellAmount'), item[1].get('inBuyAsset'), decimal.Decimal(item[2].get('inBuyAmount')), item[2].get('inBuyAsset'), item[1].get('operationId')) + txid = item[0].get('operationId') + tx = mempool_api.get_transaction(txid) + potential_utxos = [] + vouts = tx.get("vout", []) + for i in range(len(vouts)): + if vouts[i].get('value', 0) == decimal.Decimal(item[0].get('outSellAmount'))*100000000: + potential_utxos.append("{}:{}".format(txid, i )) + data = {} + if len(potential_utxos) == 1: + # label UTXO/output of tx + data = { + "type": "output", + "ref": potential_utxos[0], + "label": label, + } + if len(potential_utxos) > 1: + # mark tx, add warning tag + data = { + "type": "tx", + "ref": txid, + "label": "{} {}".format(label, "#W001_UTXO_NOT_FOUND"), + } + if data: + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + else: + messages.add_message( + request, + messages.ERROR, + 'Could not process record "{}".'.format(item), + ) + else: + print("ERROR") # TODO + # BlueWallet + elif form.cleaned_data.get("import_type", "") == "csv-bluewallet": + while True: + buf = fp.readline() + if buf in EOLSTOP: + break + try: + buf = str(buf.decode("utf-8")) + sbuf = buf.split(",") + data = { + "type": "tx", + "ref": sbuf[1], + "label": " ".join(sbuf[3:]), + } + data["labelbase"] = labelbase.id + serializer = LabelSerializer(data=data) + if serializer.is_valid(): + serializer.save() + imported_lables += 1 + else: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}".'.format(buf), + ) + except Exception as ex: + messages.add_message( + request, + messages.ERROR, + 'Could not process line "{}", {}.'.format(buf, ex), + ) + else: + fp.close() + os.unlink(fp.name) + return HttpResponseRedirect("/failed/url/") + fp.close() + os.unlink(fp.name) + if imported_lables: + messages.add_message( + request, + messages.INFO, + "Processed and imported {} labels.".format(imported_lables), + ) + return HttpResponseRedirect(labelbase.get_absolute_url()) + else: + form = UploadFileForm() + return render(request, "upload.html", {"form": form}) diff --git a/django/templates/_base.html b/django/templates/_base.html index d831357..acfd652 100644 --- a/django/templates/_base.html +++ b/django/templates/_base.html @@ -386,7 +386,6 @@ $(document).ready(function () { - // Disable the Origin field by default if not Tx {% if label.type == "tx" %} {% else %} diff --git a/django/templates/_modal_exportLabelbaseModal.html b/django/templates/_modal_exportLabelbaseModal.html index 92b31c9..45629e3 100644 --- a/django/templates/_modal_exportLabelbaseModal.html +++ b/django/templates/_modal_exportLabelbaseModal.html @@ -17,17 +17,15 @@
{% labelbaseform_export as export_form %} {{ export_form|bootstrap }} -
+ +
+
+

Automated Output Management

+

We utilize Electrum servers to automatically update your spendable outputs. Labelbase keeps the state of your spendable outputs up to date, helping you keep track of your labels and funds.

+
+
+ +
+
+

Automated Fiat Currency Evaluation

+

Labelbase converts the value of your spendable outputs to fiat currency, assisting you in keeping track of your portfolio value measured in your chosen currency.

+
+
+ + +
+
+



+

.. and much more.

+
+
+ @@ -293,10 +315,10 @@

Ready to optimize your Bitcoin labeling for optimal efficiency? -

-Discover the power of streamlined label management with Labelbase. -

-Join Labelbase now and unlock the full potential of your Bitcoin experience! +

+ Discover the power of streamlined label management with Labelbase. +

+ Join Labelbase now and unlock the full potential of your Bitcoin experience!

@@ -306,12 +328,15 @@ Join Labelbase now and unlock the full potential of your Bitcoin experience! {% endif %} + + --> {% endblock %} diff --git a/django/templates/privacy.html b/django/templates/privacy.html index 9561877..7e0f66c 100644 --- a/django/templates/privacy.html +++ b/django/templates/privacy.html @@ -10,24 +10,32 @@

Privacy Policy

- The privacy policy on this website specifically applies to the labelbase.space web application, which is hosted by the Labelbase team.
Last updated: July 26th 2023.
+

+ The privacy policy applies to labelbase.space hosted by Labelbase. For self-hosted versions, variations or customizations may exist. + + Users are responsible for reviewing any specific privacy considerations or customizations made to their instance.

+ + Last updated: December 19th 2023.

    -
  1. We do not use any analytics or tracking tools that collect personal information, but {# offer an optional opt-in to use #} we use Sentry for anonymized error tracking, ensuring users are not associated with reported errors.
  2. -
  3. This website does not use cookies, apart from the login session. However, your IP address may be collected as part of basic webserver logs necessary for systems administration purposes.
  4. +
  5. We do not use any analytics or tracking tools that collect personal information, but {# offer an optional opt-in to use #} we use Sentry for anonymized error tracking, ensuring users are not associated with reported errors.
  6. +
  7. This website does not use cookies, except for the login session and other essential cookies required for the functionality of the web application. However, your IP address may be collected as part of basic webserver logs necessary for systems administration purposes.
  8. We do not collect or store information, expect the information that is attached directly to labels. Deleting your Labelbase removes the labels attached to it.
  9. We do not share any information with third parties including advertisers. See 6. for interated APIs and services.
  10. -
  11. We cannot access your funds.
  12. -
  13. We are and act as your data processor. In other words: You are responsible for the labelbases and labels you create on or upload to Labelbase, which we store and process to provide our services to you.
  14. +
  15. This is not a wallet software. We cannot access your funds.
  16. +
  17. We are and act as your data processor. In other words: You are responsible for the labelbases, labels and additional date you create on or upload to Labelbase, which we store and process to provide our services to you.
  18. "Labelbase" uses the following 3rd party APIs and services:
    • The addresses and transactions are provided with hyperlinks to mempool.space. These links must be actively clicked.
    • -
    • We have no influence over wallets and services that implement our API.
    • +
    • We connect and may get data (like exchange rates) from mempool.space
    • +
    • We connect and may get data from some "community trusted" Electrum servers.
    + We have no influence over wallets and services that implement our API. +

- + {% endblock %} diff --git a/django/templates/terms.html b/django/templates/terms.html index 1c067b2..5806cf4 100644 --- a/django/templates/terms.html +++ b/django/templates/terms.html @@ -9,9 +9,9 @@

Terms of Service

- February 10th 2023 + December 19th 2023

- "LABELBASE" (labelbase.space) is provided without any guarantees of anything, not + "LABELBASE" (labelbase.space, but also the self-hosted versions) is provided without any guarantees of anything, not even that it will do its job properly. If you have any doubts, please use an alternate service.

@@ -107,5 +107,5 @@ FIRE, FLOODS, ACCIDENTS, SERVICE OUTAGES RESULTING FROM EQUIPMENT AND/OR SOFTWAR
- + {% endblock %} diff --git a/django/templates/upload.html b/django/templates/upload.html new file mode 100644 index 0000000..5123ff5 --- /dev/null +++ b/django/templates/upload.html @@ -0,0 +1,11 @@ +{% extends "_base.html" %} +{% load i18n %} +{% load bootstrap %} +{% block content %} +

Import labels

+
+ {% csrf_token %} + {{ form|bootstrap }} + +
+{% endblock %}