Categories
blandet

Mit eventyr med Facebook

Jeg syntes, det var ubehageligt at kunne se, hvordan Facebook lod forskellige virksomheder matche deres oplysninger om mig (e-mails, telefonnummer, den slags) med min Facebook-konto.

Det drejede sig både om store, grimme virksomheder:

Og små, søde, rettighedsorienterede NGO’er:

Min idé til en løsning var:

  • At slette mit telefonnummer fra min profil på Facebook
  • At oprette en e-mail særligt til min Facebook-profil (lad os sige: facebookholderojemed@helmstedt.dk)

…Og så håbede jeg ellers, at “matchene” ville ophøre.

Men: Det gjorde de ikke.

Hvordan kan det være?

Facebook beholder alle mailadresser, du nogensinde har haft tilknyttet din konto – også de, du selv sletter! Og de lader annoncører matche deres oplysninger om DIG med de oplysninger, du troede, du havde slettet.

(Du kan prøve at se selv, ved at bruge Facebook’s funktion til at downloade en kopi af dine data.)

Så begyndte mit rigtige eventyr:

Mit forsøg på at få slettet gamle mailadresser hos Facebook

Efter at have svaret på et hav af spørgsmål, fik jeg lov at udfylde en kontaktformular:

Select the product that you need help with : Facebook
What can we help you with? : I want to manage my data
Select one of the following options : I have a different objection to the use of my data
Full name : Morten Helmstedt
Please provide your best contact email address : facebookholderojemed@helmstedt.dk
Where do you live? : Denmark

What data processing activity or activities are you objecting to? : I am objecting to Facebook matching my personal information with information uploaded by advertisers and do not consent to Facebook allowing advertisers to do this.
Even though I have changed my e-mail address to an address only used for Facebook communication and have deleted my phone number and additional e-mail addresses from Facebook, Facebook still matches my information with lists from advertisers. See https://helmstedt.dk/Clipboard01.png for a screenshot.
I would like to be informed in what way advertisers are able to match their lists with my Facebook profile, when no information from my Facebook profile could be in possession by these advertisers after I changed my Facebook profile e-mail and phone number.

If this matching is done by Facebook keeping deleted information from my profile, I do not consent to Facebook keeping this information and I request that this information be deleted, as Facebook no longer has any valid grounds or consent for keeping this information.

Please explain how this processing impacts you. : I have a right to control my personal data according to the EU GDPR regulations and have not consented to Facebook matching my personal data with personal data from advertisers.

By submitting this notice, you represent that all of the information you’ve provided is true and accurate. : I agree

Det var i første omgang ikke til så meget hjælp. Facebook bekræftede blot, deres praksisser:

Hi,

Thanks for contacting us.

To build a product that connects people across continents and cultures, we need to make sure that everyone can afford it. Advertising lets us keep Facebook free. You can’t opt out of ads altogether because ads are what keep Facebook free, but you do have different options to control how your personal data can and can’t be used to show you ads. They’re all found in ad preferences:
https://www.facebook.com/ads/preferences/?ref=CR

Please note that we do not tell advertisers who you are or sell your information to anyone.

There are a few ways that advertisers can reach you with ads on Facebook:

Information from your use of Facebook

When you use Facebook, you can choose to share things about yourself, such as your age, gender, home town or friends. You can also click or like posts, Pages or articles. We use this information to understand what you might be interested in and hopefully show you ads that are relevant. If a bike shop comes to Facebook wanting to reach female cyclists in Liverpool, we can show their ad to women in Liverpool who liked a Page about bikes. But here’s what’s key: these businesses don’t know who you are. We provide advertisers with reports about the kinds of people seeing their ads and how their ads are performing, but we don’t share information that personally identifies you. You can always see the “interests” assigned to you in your ad preferences, and if you want, remove them.

Information that an advertiser shares with us

In this case, advertisers bring us their customer information so they can reach the same people on Facebook. These advertisers might have your email address from a purchase you made, or from some other data source. We find Facebook accounts that match that data, but we don’t tell the advertiser which accounts were matched. In ad preferences (https://www.facebook.com/ads/preferences/) you can see which advertisers with your contact information are currently running campaigns – and you can click the top right-hand corner of any ad to hide all ads from that business.

Information that websites and apps send to Facebook

Some of the websites and apps you visit may use Facebook tools to make their content and ads more relevant and better understand the results of their ad campaigns. For example, if an online retailer is using Facebook pixel, they can ask Facebook to show ads to people who looked at a certain style of shoe or put a pair of shoes into their shopping basket. If you don’t want this data used to show you ads, you can turn it off in ad preferences.

You can learn more about Facebook pixel and how it works here:
https://www.facebook.com/business/learn/facebook-ads-pixel/?ref=CR

You can decide which parts of your profile you want to be used for ad targeting in the Information section under “About you”. You can remove yourself from interests under “Interests” and categories under “Your categories”. You can also turn off ads that use data from apps or websites that you visit in the Ads settings section under “Ads based on use of websites and apps”.

The “How is this information shared?” section of our Data Policy also discusses in more detail how ads work on Facebook: https://www.facebook.com/about/privacy/?ref=CR

The form that you submitted allows EU residents to report objections to certain types of processing of their personal data under the EU General Data Protection Regulation (GDPR). If you want to object to a specific type of data processing listed in our Data Policy, please visit the Help Centre to learn more about making an objection under GDPR and in what circumstances an objection may be successful:
https://www.facebook.com/help/2069235856423257/?ref=CR

Before you submit another objection, you may want to learn more about our legal bases for processing data, including the instances where the processing is necessary for our legitimate interests (or that of a third party) or for a task carried out in the public interest: https://www.facebook.com/about/privacy/legal_bases/?ref=CR

We hope this helps, but please let us know if you have any other questions.

Thanks,
Ryan,
Privacy Operations
Facebook

Jeg bad så specifikt Facebook om at slette mine gamle e-mailadresser, da jeg havde svært ved at se noget som helst gyldigt formål ved at opbevare disse data. Her er svaret, jeg fik:

Hi Morten,

Thanks for following up with us.

From your report it appears you would like to delete information from your Facebook account.

Deleting your information from Facebook
You can delete specific data points from your Facebook account via your Activity Log. You can access your Activity Log and to choose to delete certain data points by accessing your Settings > Your Facebook Information > Activity Log.
It appears from your report you are already aware of this, but please note you can choose to permanently delete your profile, photos, posts, videos, and everything else you’ve added at any time by deleting your Facebook account. You can follow the steps outlined in the following Help Center article to understand how to permanently delete your account:
https://www.facebook.com/help/224562897555674

Deleting previous email addresses
From your report it appears you would like us to delete the email addresses previously associated with your account.
As explained in our Data Policy (https://www.facebook.com/policy.php (https://www.facebook.com/about/privacy/update#legal-requests-prevent-harm)), we need to process certain information in order to detect and prevent spam and other bad experiences on Facebook, maintain the integrity of our Products, and promote safety and security on and off the Facebook Products.
In relation to your specific request, the information you have asked to be deleted is necessary for us to maintain the integrity of our Products, and promote safety and security on and off the Facebook Products.

[…]

Under Article 17 of the GDPR there are limited grounds on which the erasure of personal data can be obtained. We have reviewed your request in light of the information you have provided to us, and we have found that your request does not meet one of the grounds listed in Article17 of the GDPR. We are therefore unable to take further action on your request to have this data point deleted.

Please note that you have a right to contact the Irish Data Protection Commission, which is Facebook’s lead supervisory authority (please see www.dataprotection.ie (http://www.dataprotection.ie/))

You also have the right to contact your local data protection authority and to bring a claim before the courts.

We trust this addresses your query but please let us know if you have any further questions.

Thanks,
Elsa
Privacy Operations
Facebook

Så Facebook mener, at:

In relation to your specific request, the information you have asked to be deleted is necessary for us to maintain the integrity of our Products, and promote safety and security on and off the Facebook Products

Aha!

Jeg forsøgte mig med det (næst)sidste kort, jeg havde på hånden:

Dear Elsa
Under GDPR I have a right to rectify incorrect data. As my previous e-mail addresses are no longer my e-mail addresses, they should be deleted. You have no valid use for e-mail addresses that I no longer use. Please delete those e-mail addresses and confirm.
Best regards,
Morten

Den logik kunne Elsa fra Facebook ikke følge:

Hi Morten,

Thanks for following up with us and for your patience.

As explained in our Data Policy (https://www.facebook.com/policy.php), we need to process certain information in order to detect and prevent spam and other bad experiences on Facebook, maintain the integrity of our Products, and promote safety and security on and off the Facebook Products. Please note that all these measures are taken to make the platform safer for our users.

If your personal data is inaccurate, you have the right to have the data rectified by Facebook. In this case, there is no suggestion that we are processing inaccurate data, but rather retaining your previous email address for the reasons explained above.

We won’t be able to take any further action on this request, but please note that you have a right to contact the Irish Data Protection Commission, which is Facebook’s lead supervisory authority (please see www.dataprotection.ie (http://www.dataprotection.ie/))

Best regards,
Elsa
Privacy Operations
Facebook

For Facebook handler ukorrekte persondata ikke om data, der ikke længere/nogensinde beskriver personen korrekt. For alt data, der er tilgået Facebook, er per definition korrekt råmateriale for Facebooks overvågning.

Moralen?

Nu har jeg slettet min Facebookkonto. Jeg opfordrer dig til at gøre det samme.

Categories
blandet

Britta Nielsen-generator

Du ser et stort pengebeløb og tænker: Hvor mange gange Britta Nielsen svarer det egentlig til?

Nu kan du få svaret med regnemaskinen på https://wallnot.dk/e/britta

Categories
blandet

Sådan laver du en gratis Weekendavisen

Nu afslører jeg lige noget jeg opdagede, da jeg lavede https://wallnot.dk (som kun offentliggør gratisartikler): Weekendavisen er gratis!

Et lille udsnit af en betalingsartikel fra Weekendavisen.dk som flot struktureret JSON.

Selv om https://www.weekendavisen.dk/ ligner en typisk dansk netavis med gratis-artikler og paywall-artikler i én pærevælling, offentliggør Weekendavisen faktisk hele sit indhold. De ved det sikkert ikke selv – men udvikleren hos det smarte webbureau, der har udviklet deres side, ved det med sikkerhed.

Avisens oversigt over ugens avis – denne uge er det https://www.weekendavisen.dk/2019-51/oversigt – indeholder en fuldt offentlig JSON-streng med hele avisens indhold: fuld tekst, links til artikeloplæsninger, hele dynen.

Det er ret amatøragtigt.

Du ser det ikke i din browser når du besøger siden, men det er der.

Jeg har lavet et lille Python-script, der genererer din egen personlige Weekendavisen for den aktuelle uge i en fil, der hedder index.html. Det ser ikke særligt godt ud, der er kun de fulde tekster, ikke billeder og links til oplæsning – du kan selv arbejde videre med JSON-strengen, hvis du vil have det til at se flot ud.

Det kan være, jeg ødelægger det for mig selv, for hvis Weekendavisen retter fejlen, bliver jeg formentlig nødt til at omkode den del af wallnot.dk, der viser gratis Weekendavisen-artikler.

God fornøjelse med din gratis Weekendavisen.

# The Danish newspaper Weekendavisen.dk publishes all articles - even those supposedly behind a paywall - as json on their homepage.
# This small script creates an index.html file to read all articles from the current edition.

import requests
from bs4 import BeautifulSoup
import json

def weekendavisen():
	# Request front page
	data = requests.get("https://weekendavisen.dk")
	result = data.text

	# Soup site and create a list of links and their titles
	soup = BeautifulSoup(result, "html.parser")

	for a in soup.find_all('a'):
		if "/oversigt" in a['href']:
			overviewurl = a['href']

	edition = overviewurl[overviewurl.find(".dk/") + 4:overviewurl.find(".dk/") + 11]
	request = "https://weekendavisen.dk/" + edition + "/oversigt"

	# Request site and soup it
	data = requests.get(request)
	result = requests.utils.get_unicode_from_response(data) 
		
	soup = BeautifulSoup(result, "html.parser")
	content = soup.find('script', attrs={'class':'js-react-on-rails-component', 'data-component-name':"IndexPage"})
	jsonobject = content.string
		
	# Create json object
	jsondecode = json.loads(jsonobject)
	
	# Iterate through articles and articles to dictionary
	articlelist = []
	
	for section in jsondecode["sections"]:
		for item in section["items"]:
			summary = item["summary"]
			summary_output = '<b>' + summary[:summary.find(".") + 1] + '</b> ' + summary[summary.find(".") + 1:] + ''
			title = item["title"]
			title_output = '<h1><big>' + title + '</big></h1>'
			if item["type"] == "newsarticleplus":
				article = item["body"] + item["paidBody"]
			else:
				article = item["body"]
			output = summary_output + title_output + article

			articlelist.append(output)

	week_linkstr = ""
	for article in articlelist:
		week_linkstr += article
			
	return week_linkstr	

def htmlgenerator():
	htmlstart = '''<!DOCTYPE HTML>
	<head>
	<meta charset="utf-8"/>

	<title>Weekendavisen</title>

	</head>
	<body>'''
	
	htmlend = '</body></html>'
	
	finalhtml = htmlstart + week_links + htmlend

	# Saves to disc
	with open("./index.html", "wt", encoding="utf8") as fout:
		fout.write(finalhtml)	
			
week_links = weekendavisen()
htmlgenerator()
Categories
blandet

Lidt internethistorie #2

En anden gammel hjemmeside, jeg har fundet frem, er https://helmstedt.dk/plague/.

Et skoleprojekt, vist nok fra 8. klasse ca. 1997/1998, som var øvelse til projektopgaven i 9.

Roterende kranier, frames og kildehenvisninger – den sidste er bedst:

Clara John D: Middelalderbyen.
Dybmose Børge og Frederiksen Knud: Middelalderen.
Tallerud Berndt: Den Sorte Død.
Thiedecke Johnny: Pokker, Pest og Piller.
Tuchman Barbara: Et fjernt spejl.
Internettet.

Categories
blandet

Lidt internethistorie #1

For over 20 år siden fandtes “Mortens kodeside”. Min allerførste hjemmeside. Lavet i Microsoft Frontpage. Med frames.

Nu er den online igen på https://helmstedt.dk/kodeside/

Sidst opdateret 7/9/1998.

Siden førte til mit første “rigtige” job som redaktionsassistent på Jubii. Jeg blev kontaktet af “funtown.dk” for et bannerbyt. Det gjorde vi, og mit 14-årige selv spurgte, om ikke jeg kunne anmelde spil for dem. Det kunne jeg, og kort tid efter blev “funtown.dk” købt af Jubii og blev til Jubii Games.

Jubii havde hovedkvarter siloen på Rahbeks Allé, tæt på hvor jeg boede i Valby, så jeg blev ansat af Martin Thorborg og læste korrektur og fyldte indhold i et CMS – det var ret nyt dengang, sådan noget. Jeg oversatte også vBulletin til dansk til Jubii’s debatside – det skulle gøres direkte i koden.

Jeg kan huske, jeg bad om 50 kr. i timen – Martin synes, jeg skulle have 60.

God fornøjelse med Mortens kodeside.

Categories
blandet

Kortlinkværktøj med Django/Python

Der er nok ikke mange mennesker efterhånden, der ikke har deres egen kortlinkservice. En af de mest kendte er https://bitly.com/.

Som en øvelse har jeg lavet kortlinkservicen https://wallnot.dk/e/link. Linkene bliver godt nok ikke specielt korte, men indtil videre sparer jeg udgiften til et selvstændigt domænenavn. Det er ikke fordi, der mangler muligheder andre steder.

At lave et kortlink-værktøj i Django er overraskende nemt.

Her er en lille opskrift.

Opskrift på kortlinkværktøj

Efter at have oprettet mit projekt (se evt. guide på https://www.djangoproject.com/start/) går jeg i gang.

Jeg starter med min datamodel i models.py. Hvert link har en destination (det lange link), et kort link og et tidsstempel. Destinationen er en URL, det korte link er et antal tegn og tidsstemplet er – et tidsstempel:

from django.db import models
from django.utils import timezone

class Link(models.Model):
    destination = models.URLField(max_length=500)
    shortlink = models.CharField(max_length=6, unique=True)
    date = models.DateTimeField(default=timezone.now, editable=False)

Jeg ved, at jeg skal bruge en formular. Den opretter jeg i forms.py. Her bruger jeg en type formular, der kaldes ModelForm. Django sørger for, at valideringsreglerne følger samme type data, som jeg har i min bagvedliggende datamodel:

from django.forms import ModelForm, URLInput
from .models import Link

class LinkForm(ModelForm):
    class Meta:
        model = Link
        fields = ['destination']
        widgets = {
            'destination': URLInput(attrs={'placeholder': 'Indsæt link'}),
        }

Logikkerne bag de enkelte visninger i Django laves i views.py. Jeg har to forskellige visninger. Én visning som jeg bruger til at vise min forside, hvor jeg både viser min formular til indtastning af links og det korte link (index). Én visning, som aktiveres når brugeren besøger et kort link (redirect).

Endelig har jeg en funktion, som jeg bruger til at generere selve de korte links.

Jeg har kommenteret koden en masse, så jeg håber den er til at følge med i:

from django.shortcuts import render
from django.http import HttpRequest, HttpResponseRedirect
from .models import Link
from .forms import LinkForm
import hashlib
import bcrypt

# Function to create a random hash to use as short link address
def create_shortlink(destination):
	salt = bcrypt.gensalt().decode()	# Random salt
	destination = destination+salt		# Salt added to destination URL
	hash = hashlib.md5(destination.encode()).hexdigest() # Hashed to alphanumeric string
	return hash[:6]	# First 6 characters of that string 

# Front page with a form to enter destination address. Short URL returned.
def index(request):
	form = LinkForm()	# Loads form
	url = 'https://wallnot.dk/e/link/'	# site url
	# If a destination is submitted, a short link is returned
	if request.method == 'POST':
		form = LinkForm(request.POST) # Form instance with submitted data
		# Check whether submitted data is valid
		if form.is_valid():
			destination = form.cleaned_data['destination'] # Submitted destination
			# If destination is already in database, return short link for destination from database
			try:
				link = Link.objects.get(destination=destination)
				sharelink = url + link.shortlink # Creates full URL using page URL and hash
			# If destination is not in database, create a new short link
			except:
				# Loop to create a unique hash value for short link
				unique_link = False
				while unique_link == False:
					hash = create_shortlink(destination)	# Return hash
					# First we check whether the hash is a duplicate
					try:
						Link.objects.get(shortlink=hash)	# Check whether hash is used
					# If not a duplicate, an error is thrown, and we can save the hash
					except:
						link = form.save(commit=False)	# Prepare to save form destination data and hash
						link.shortlink = hash	# Sets short link to hash value
						link.save()	# Saves destination and short link to database
						sharelink = url + link.shortlink # Creates full URL using page URL and hash
						unique_link = True	# If check causes error, hash is unused, exit loop
			context = {'sharelink': sharelink, 'form': form}	# Dictionary with variables used in template
			return render(request, 'links/index.html', context)
		# If form is invalid, just renders page.
		else:
			context = {'form': form}
			return render(request, 'links/index.html', context)
	# Render page with form before user has submitted
	context = {'form': form}
	return render(request, 'links/index.html', context)

# Short link redirect to destination URL
def redirect(request, shortlink):
	# Query the database for short link, if there is a hit, redirect to destination URL
	try:
		link = Link.objects.get(shortlink=shortlink)
		return HttpResponseRedirect(link.destination)
	# An error means the short link doesn't exist, so the front page template is shown with an error variable
	except:
		error = True
		context = {'error': error}
		return render(request, 'links/index.html', context)

For at kunne servere siderne, har jeg urls.py, der fortæller Django hvordan en indtastet URL af brugeren skal pege på funktioner i views.py:

from django.urls import path
from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<shortlink>', views.redirect, name='redirect'),
]

Og endelig har jeg index.html, som er den skabelon, som min side genereres på baggrund af. Hvis du ikke har prøvet Django før, så læg mærke til alt det, der står i tuborgklammer ({}). De bruges dels til simple funktioner (fx if-funktioner, dels til at indsætte variable fra views.py i den side, der genereres.

Hvis du lægger mærke til funktionerne, bruger jeg if-funktionerne til at nøjes med en skabelon, uanset hvilken situation brugeren er havnet i, sådan at indholdet fx er anderledes, når brugeren har lavet en fejl i udfyldelsen af formularen, end når brugeren ikke har udfyldt formularen endnu.

Der er også et lille javascript i filen, der sørger for at brugeren kan kopiere det korte link til sin udklipsholder.

<!doctype html>
<html lang="da">
  <head>
    <!-- Required meta tags -->
	<title>Korte links</title>
	<meta name="description" content="Skønne korte links">
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
	<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
	<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
	<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
	<link rel="manifest" href="/site.webmanifest">
	<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
	<meta name="msapplication-TileColor" content="#ffc40d">
	<meta name="theme-color" content="#ffffff">

	<style>
	body { 
		font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
		text-align: center;
		box-sizing: border-box;	
	}

	h1 {
		margin-top: 0;
		font-size: 4.0rem;
		font-weight: 300;
		line-height: 1.2;
		margin-bottom: 1.5rem;
	}

	h2 {
		margin-top: 1.5rem;
		font-size: 2.5rem;
		font-weight: 300;
		line-height: 1.2;
		margin-bottom: 1.5rem;
	}

	input {
		width: 60%;
		line-height: 1.2;
		font-size: 1.0rem;
		height: 1.5rem;
		padding: 10px;
	}

	button {
		width: 50%;
		border: 1px solid transparent;
		padding: .375rem .75rem;
		font-size: 1rem;
		line-height: 1.8;
		height: 2.5rem;
		border-radius: .25rem;
		color: #fff;
		background-color: #28a745;
		border-color: #28a745;
	}

	button:focus {
		box-shadow: 0 0 0 0.2rem rgba(72,180,97,.5)
	}

	button:hover {
		background-color: #218838;
		border-color: #1e7e34;
	}
	
	.footer {
		position: fixed;
		left: 0;
		bottom: 0;
		width: 100%;
		background-color: #f1f1f1;
		color: black;
	}	
	</style>
  </head>
  <body>

<h1>Lav et kort link</h1>

{% if form %}
	<form method="post">
	{% csrf_token %}
	<p>{{ form.destination }}</p>
	<p><button type="submit" value="Lav et kort link">Lav et kort link</button></p>
	</form>

	{% if form.destination.errors %}
		<h2>Tast et gyldigt link!</h2>
		<p><em>Du har tastet et ugyldigt link. Prøv igen med et gyldigt link med http://, https://, ftp:// eller ftps:// foran.</em></p>
	{% endif %}

	{% if request.method == "POST" and not form.destination.errors %}
		<h2>Her er dit link:</h2>
		<p><a href="{{ sharelink }}">{{ sharelink }}</a></p>
		<button class="copy">Kopier link</button>
	{% endif %} 

{% endif %}

{% if error %}
<h2>Har du tastet forkert?</h2>
<p><em>Du har prøvet at bruge et kort link. Desværre er det link, du har tastet, ikke registreret. Måske er du kommet til at taste forkert?</em></p>
<p><a href="{% url 'index' %}">Til forsiden</a>
{% endif %} 

<div class="footer">
  <p>Lav relativt korte links på wallnot.dk. Gratis og fri for annoncer og overvågning.</p>
</div>


<script>
function fallbackCopyTextToClipboard(text) {
  var textArea = document.createElement("textarea");
  textArea.value = text;
  document.body.appendChild(textArea);
  textArea.focus();
  textArea.select();

  try {
    var successful = document.execCommand("copy");
    var msg = successful ? "successful" : "unsuccessful";
    console.log("Fallback: Kopiering gik fint " + msg);
  } catch (err) {
    console.error("Fallback: Kunne ikke kopiere", err);
  }
  document.body.removeChild(textArea);
}

function copyTextToClipboard(text) {
  if (!navigator.clipboard) {
    fallbackCopyTextToClipboard(text);
    return;
  }
  navigator.clipboard.writeText(text).then(function() {
    console.log('Kopiering gik fint');
  }, function(err) {
    console.error('Kunne ikke kopiere', err);
  });
}

var copy = document.querySelector('.copy');

copy.addEventListener('click', function(event) {
  copyTextToClipboard('{{ sharelink }}');
});
</script>

</body>
</html>
Categories
blandet

Pakkesporing fra flere forskellige transportører

For tiden øver jeg mig i at bruge Django – et værktøj til at lave webapplikationer i Python. Det er vildt smart.

Det tog et par timer at få https://wallnot.dk/e/pak/ i luften, men så er der heller ikke gjort noget ud af brugerfladen og det bagvedliggende kunne helt sikkert også gøres smartere. Siden kan bruges til at spore pakker til levering fra flere forskellige transportører (PostNord, GLS, DAO).

Hvis du har pakker på vej fra andre transportører og vil dele pakkenumrene med mig, er jeg interesseret.

Categories
blandet

Et nyt bud på en simulation af krig

Min Python-simulation af kortspillet Krig var ikke særlig elegant. Ved krig og dobbelt-krig osv. var en masse “if”-sætninger inde i hinanden med samme logik. (Jeg fandt også nogle dumme fejl, så jeg har opdateret det oprindelige indlæg.)

Derfor har jeg prøvet at skrive en ny version.

Den fungerer fint og giver følgende output ved 1.000.000 spil:

Der blev spillet 1000000 spil
Det gennemsnitlige antal dueller var 177.217668
Det højeste antal dueller var 2238
Det laveste antal dueller var 3
Den spiller med højest sum af kort vandt 573276 gange (57%)
Den spiller med højest sum af kort tabte 397771 gange (40%)
Uafgjorte spil: 1
Antal enkeltkrig, dobbeltkrig, osv.: 12348559, 886651, 60655, 3722, 218, 11, 2
Vendte kort uden krig og med krig: 176766958, 13299818
Spillene tog 225.4 sekunder

Det nye program:

# KRIG #
import time
start_time = time.time()
import random

number_of_games_to_play = 1000000
number_of_games_counter = 0
number_of_plays_list = []
highest_deck_won = 0
highest_deck_lost = 0
equal_games = 0
war_types = [0,0,0,0,0,0,0]
war_or_not_war = [0,0]

# Loop to play games
percentage_copy = 0
i = 0
while i < number_of_games_to_play:
	# One is added to i so loop finishes once number of games have been played
	i += 1
	
	# Prints percentage done with 1 decimal every time it changes
	percentage_completed = round((i/number_of_games_to_play*100), 1)
	if percentage_copy != percentage_completed:
		print("{}% done".format(percentage_completed))
	percentage_copy = percentage_completed

	# Create a deck, shuffle it and divide between players
	deck = [2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,6,7,7,7,7,8,8,8,8,9,9,9,9,10,10,10,10,11,11,11,11,12,12,12,12,13,13,13,13,14,14,14,14]
	random.shuffle(deck)
	player_a_deck = deck[0:26]
	player_b_deck = deck[26:52]

	# Which player has the highest sum of cards
	card_sum_a = sum(player_a_deck)
	card_sum_b = sum(player_b_deck)
	if card_sum_a > card_sum_b:
		highest_deck = "a"
	elif card_sum_a < card_sum_b:
		highest_deck = "b"
	else:
		highest_deck = "equal"
	
	# Loop to turn cards within games
	number_of_plays = 0
	index = 1
	while True:
		try:
			if index == 1:
				number_of_plays += 1	# Add 1 to number of plays counter
				war_count = 0			# Reset war counter	
			# Player a has the largest card
			if player_a_deck[index-1] > player_b_deck[index-1]:
				war_or_not_war[0] += 1
				player_a_deck.extend(player_a_deck[:index])
				player_a_deck.extend(player_b_deck[:index])	
				del player_a_deck[:index]
				del player_b_deck[:index]
				index = 1			# If a play is decided, index is reset
			# Player b has the largest card
			elif player_a_deck[index-1] < player_b_deck[index-1]:
				war_or_not_war[0] += 1
				# Cards are added in different order to deck in order to avoid (game) risk of going on forever (infinite loop)!
				player_b_deck.extend(player_b_deck[:index])	
				player_b_deck.extend(player_a_deck[:index])
				del player_a_deck[:index]
				del player_b_deck[:index]
				index = 1			# If a play is decided, index is reset
			# War is on!
			else:
				war_or_not_war[1] += 1
				index += 4			# In case of war the index is upped by four cards
				war_types[war_count] += 1
				war_count += 1
		# If a player has too few cards left to participate, game is over
		except IndexError:
			# If a player had no cards left and index is 1, the game was already over, so number of plays is corrected
			if index == 1:
				number_of_plays -= 1
			break
	
	# Single game is over #
	# Compare deck sizes to decide winner and add values to counters and lists
	deck_a = len(player_a_deck)
	deck_b = len(player_b_deck)
	if deck_a > deck_b:
		if highest_deck == "a":
			highest_deck_won += 1
		elif highest_deck == "b":
			highest_deck_lost += 1
	elif deck_a < deck_b:
		if highest_deck == "a":
			highest_deck_lost += 1
		elif highest_deck == "b"    :
			highest_deck_won += 1
	else:
		equal_games += 1
	
	number_of_plays_list.append(number_of_plays)
	number_of_games_counter += 1
	
# All games are over #
print("Der blev spillet {} spil".format(number_of_games_counter))
print("Det gennemsnitlige antal dueller var {}".format(sum(number_of_plays_list)/len(number_of_plays_list)))
print("Det højeste antal dueller var {}".format(max(number_of_plays_list)))
print("Det laveste antal dueller var {}".format(min(number_of_plays_list)))
print("Den spiller med højest sum af kort vandt {} gange ({}%)".format(highest_deck_won, round(highest_deck_won/number_of_games_counter*100)))
print("Den spiller med højest sum af kort tabte {} gange ({}%)".format(highest_deck_lost, round(highest_deck_lost/number_of_games_counter*100)))
print("Uafgjorte spil: {}".format(equal_games))
print("Antal enkeltkrig, dobbeltkrig, osv.: {}".format(", ".join(str(x) for x in war_types)))
print("Vendte kort uden krig og med krig: {}".format(", ".join(str(x) for x in war_or_not_war)))
print("Spillene tog {} sekunder".format(round(time.time() - start_time, 1)))
Categories
blandet

Hent transaktioner ud af Nordnet – med PowerShell!

Jeg blev spurgt om man kan få mit Python-program til at hente transaktioner ud af Nordnet oversat til PowerShell. Det kan man, dog i en lidt mere rudimentær version. Her er kode til login i Nordnet og hentning af transaktionsdata for en enkelt konto/portefølje. For at få scriptet til at virke, skal du indsætte nogle værdier de rigtige steder i scriptet:

  • brugernavn og password til Nordnet
  • til- og fradato, du vil hente transaktioner for
  • kontonummer på den konto i Nordnet, du vil hente fra (din første konto har kontonummer 1 osv.

Her er koden:

[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$url = 'https://classic.nordnet.dk/mux/login/start.html?cmpi=start-loggain&amp;state=signin'
$r1 = iwr $url -SessionVariable cookies
 
$url = 'https://classic.nordnet.dk/api/2/login/anonymous/'
$r2 = iwr $url -method 'POST' -Headers @{'Accept' = '*/*'} -WebSession $cookies
 
$body = @{'username'=''; 'password'=''}
$url = 'https://classic.nordnet.dk/api/2/authentication/basic/login'
$r3 = iwr $url -method 'POST' -Body $body -Headers @{'Accept' = '*/*'} -WebSession $cookies
 
$url = 'https://classic.nordnet.dk/oauth2/authorize?client_id=NEXT&amp;response_type=code&amp;redirect_uri=https://www.nordnet.dk/oauth2/'
$r4 = iwr $url -WebSession $cookies
 
$url = 'https://www.nordnet.dk/mediaapi/transaction/csv/filtered?locale=da-DK&amp;account_id=1&amp;from=2019-08-01&amp;to=2019-10-01'
$r5 = iwr $url -WebSession $cookies

$content = $r5.Content
$encoding = [System.Text.Encoding]::unicode
$bytes = $encoding.GetBytes($content)

$decoded_content = [System.Text.Encoding]::utf32.GetString($bytes)
$decoded_content = $decoded_content.Substring(1,$decoded_content.length-1)
Categories
blandet

Hiv dine transaktioner ud af det nye Nordnet

Her er en opdatering af mit gamle program til at hente transaktioner ud fra Nordnet. Det er opdateret til at fungere med Nordnets nye design og API:

# -*- coding: utf-8 -*-
# Author: Morten Helmstedt. E-mail: helmstedt@gmail.com
""" This program logs into a Nordnet account and extracts transactions as a csv file.
Handy for exporting to Excel with as few manual steps as possible """

import requests 
from datetime import datetime
from datetime import date

# USER ACCOUNT, PORTFOLIO AND PERIOD DATA. SHOULD BE EDITED FOR YOUR NEEDS #

# Nordnet user account credentials and accounts/portfolios names (choose yourself) and numbers.
# To get account numbers go to https://www.nordnet.dk/transaktioner and change
# between accounts. The number after "accid=" in the new URL is your account number.
# If you have only one account, your account number is 1.
user = ''
password = ''
accounts = {
	"Frie midler: Nordnet": "1",
	"Ratepension": "3",
}

# Start date (start of period for transactions) and date today used for extraction of transactions
startdate = '2013-01-01'
today = date.today()
enddate = datetime.strftime(today, '%Y-%m-%d')

# Manual data lines. These can be used if you have portfolios elsewhere that you would
# like to add manually to the data set. If no manual data the variable manualdataexists
# should be set to False
manualdataexists = True
manualdata = """
Id;Bogføringsdag;Handelsdag;Valørdag;Transaktionstype;Værdipapirer;Instrumenttyp;ISIN;Antal;Kurs;Rente;Afgifter;Beløb;Valuta;Indkøbsværdi;Resultat;Totalt antal;Saldo;Vekslingskurs;Transaktionstekst;Makuleringsdato;Verifikations-/Notanummer;Depot
;30-09-2013;30-09-2013;30-09-2013;KØBT;Obligationer 3,5%;Obligationer;;72000;;;;-69.891,54;DKK;;;;;;;;;;Frie midler: Finansbanken
"""

# CREATE VARIABLES FOR LATER USE. #

# Creates a dictionary to use with cookies	
cookies = {}

# A variable to store transactions before saving to csv
transactions = ""

# LOGIN TO NORDNET #

# First part of cookie setting prior to login
url = 'https://classic.nordnet.dk/mux/login/start.html?cmpi=start-loggain&amp;state=signin'
request = requests.get(url)
cookies['LOL'] = request.cookies['LOL']
cookies['TUX-COOKIE'] = request.cookies['TUX-COOKIE']

# Second part of cookie setting prior to login
url = 'https://classic.nordnet.dk/api/2/login/anonymous'
request = requests.post(url)
cookies['NOW'] = request.cookies['NOW']

# Actual login that gets us cookies required for later use
url = 'https://classic.nordnet.dk/api/2/authentication/basic/login'
request = requests.post(url,cookies=cookies, data = {'username': user, 'password': password})
cookies['NOW'] = request.cookies['NOW']
cookies['xsrf'] = request.cookies['xsrf']

# Getting a NEXT cookie
url = 'https://classic.nordnet.dk/oauth2/authorize?client_id=NEXT&amp;response_type=code&amp;redirect_uri=https://www.nordnet.dk/oauth2/'
request = requests.get(url, cookies=cookies)
cookies['NEXT'] = request.history[1].cookies['NEXT']

# GET TRANSACTION DATA #

# Payload and url for transaction requests
payload = {
'locale': 'da-DK',
'from': startdate,
'to': enddate,
}

url = "https://www.nordnet.dk/mediaapi/transaction/csv/filtered"

firstaccount = True
for portfolioname, id in accounts.items():
	payload['account_id'] = id
	data = requests.get(url, params=payload, cookies=cookies)
	result = data.content.decode('utf-16')
	result = result.replace('\t',';')

	result = result.splitlines()
	
	firstline = True
	for line in result:
		# For first account and first line, we use headers and add an additional column
		if line and firstline == True and firstaccount == True:
			transactions += line + ';' + "Depot" + "\n"
			firstaccount = False
			firstline = False
		# First lines of additional accounts are discarded
		elif line and firstline == True and firstaccount == False:
			firstline = False
		# Content lines are added
		elif line and firstline == False:
			# Fix because Nordnet sometimes adds one empty column too many
			if line.count(';') == 23:
				line = line.replace('; ',' ')
			transactions += line + ';' + portfolioname + "\n"

# ADD MANUAL LINES IF ANY #
if manualdataexists == True:
	manualdata = manualdata.split("\n",2)[2]
	transactions += manualdata

# Saves CSV
with open("transactions.csv", "w", encoding='utf8') as fout:
	fout.write(transactions)