From 61b4d673517c18ddf8ba2f9cf7ea60e5b16b83ec Mon Sep 17 00:00:00 2001
From: Mandragorat Wandystanu <wktir@wandystan.eu>
Date: Sun, 22 Mar 2020 01:29:07 +0100
Subject: [PATCH] Initialise repository

---
 .htaccess            |  13 +++++
 byPeriodDataset.php  |  49 +++++++++++++++++++
 byPeriodSlice.php    | 110 +++++++++++++++++++++++++++++++++++++++++++
 common.php           |  33 +++++++++++++
 context.jsonld       | 106 +++++++++++++++++++++++++++++++++++++++++
 doc/Makefile         |   2 +
 doc/icon-author.svg  |   1 +
 doc/icon-date.svg    |   1 +
 doc/icon-json-ld.png | Bin 0 -> 491 bytes
 doc/index.html       |  87 ++++++++++++++++++++++++++++++++++
 doc/index.markdown   |  67 ++++++++++++++++++++++++++
 doc/style.css        |  15 ++++++
 properties.jsonld    | 104 ++++++++++++++++++++++++++++++++++++++++
 sender.php           |  14 ++++++
 structures.jsonld    |  37 +++++++++++++++
 15 files changed, 639 insertions(+)
 create mode 100644 .htaccess
 create mode 100644 byPeriodDataset.php
 create mode 100644 byPeriodSlice.php
 create mode 100644 common.php
 create mode 100644 context.jsonld
 create mode 100644 doc/Makefile
 create mode 100644 doc/icon-author.svg
 create mode 100644 doc/icon-date.svg
 create mode 100644 doc/icon-json-ld.png
 create mode 100644 doc/index.html
 create mode 100644 doc/index.markdown
 create mode 100644 doc/style.css
 create mode 100644 properties.jsonld
 create mode 100644 sender.php
 create mode 100644 structures.jsonld

diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..567cee6
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,13 @@
+AddType application/ld+json .jsonld
+Header set Access-Control-Allow-Origin *
+
+RewriteEngine On
+RewriteBase /statistics/
+
+RewriteRule ^by-period/$ byPeriodDataset.php
+RewriteRule ^by-period/(.*)/(.*) byPeriodSlice.php?date-start=$1&date-end=$2 [B]
+RewriteRule ^sender/(.*) sender.php?hash=$1 [B]
+
+RewriteRule ^$ doc/ [L,R=301]
+RewriteRule ^(key|structure)/ structures.jsonld [L,R=303]
+RewriteRule ^property/ properties.jsonld [L,R=303]
diff --git a/byPeriodDataset.php b/byPeriodDataset.php
new file mode 100644
index 0000000..675d534
--- /dev/null
+++ b/byPeriodDataset.php
@@ -0,0 +1,49 @@
+<?php
+require_once 'common.php';
+
+// load document with month statistics
+try {
+	$document = load_remote_dom_document('https://wandystan.groups.io/g/wandystan/months');
+} catch (Exception $e) {
+	exit_error($e->getMessage());
+}
+
+$xpath = new DOMXPath($document);
+
+// find all months with non-zero number of posts
+$months = [];
+$rows = $xpath->query('//table/tbody/tr');
+
+foreach ($rows as $row) {
+	$cols = $xpath->query('td', $row);
+	$year = $cols[0]->nodeValue;
+
+	for ($i = 1; $i < $cols->length; $i++) {
+		if (
+			$cols[$i]->firstChild->nodeType === XML_ELEMENT_NODE &&
+			$cols[$i]->firstChild->tagName === 'a'
+		) {
+			$months []= $year . '-' . str_pad($i, 2, '0', STR_PAD_LEFT);
+		}
+	}
+}
+
+// construct a linked data object corresponding to gathered data
+$ld = [
+	'@context' => 'https://wandystan.eu/statistics/context.jsonld',
+	'@type' => 'DataSet',
+	'title' => [
+		'en' => 'Posts by date and sender',
+		'pl' => 'Wiadomości wg daty i wysyłającego'
+	],
+	'description' => [
+		'en' => 'Statistics of number of posts sent by a given sender on a given date, grouped by period in which they were sent.',
+		'pl' => 'Statystyki liczby wiadomości wysłanych przez danego wysyłającego w danym dniu, pogrupowane wg okresu w jakim zostały wysłane.'
+	],
+	'publisher' => 'https://wandystan.eu/',
+	'structure' => 'structure/by-period',
+	'slices' => array_map(function ($month) { return "by-period/$month-01/P1M"; }, $months)
+];
+
+header('Content-type: application/ld+json');
+print json_encode($ld);
diff --git a/byPeriodSlice.php b/byPeriodSlice.php
new file mode 100644
index 0000000..7c39e33
--- /dev/null
+++ b/byPeriodSlice.php
@@ -0,0 +1,110 @@
+<?php
+require_once 'common.php';
+
+// get request data and validate it
+$param_date_start = $_GET['date-start'];
+$param_date_end = $_GET['date-end'];
+
+if (! $param_date_start) {
+	exit_error('No start date specified.');
+} elseif (! $param_date_end) {
+	exit_error('No end date specified.');
+} elseif (! mb_ereg_match('^' . REGEXP_ISO8601_DATE_OR_INTERVAL . '$', $param_date_start)) {
+	exit_error('Invalid start date.');
+} elseif (! mb_ereg_match('^' . REGEXP_ISO8601_DATE_OR_INTERVAL . '$', $param_date_end)) {
+	exit_error('Invalid end date.');
+} elseif (starts_with($param_date_start, 'P') && starts_with($param_date_end, 'P')) {
+	exit_error('Both start and end dates cannot be periods.');
+}
+
+// create start and end date objects
+$tz = new DateTimeZone('UTC');
+
+try {
+	if (! starts_with($param_date_start, 'P'))
+		$date_start = new DateTimeImmutable($param_date_start, $tz);
+
+	if (! starts_with($param_date_end, 'P'))
+		$date_end = new DateTimeImmutable($param_date_end, $tz);
+
+	if (starts_with($param_date_start, 'P')) {
+		$interval = new DateInterval($param_date_start);
+		$date_start = $date_end->sub($interval);
+	}
+
+	if (starts_with($param_date_end, 'P')) {
+		$interval = new DateInterval($param_date_end);
+		$date_end = $date_start->add($interval);
+	}
+} catch (Exception $e) {
+	exit_error('Invalid date specification: ' . $e->getMessage());
+}
+
+// get page with the search results for the given period
+try {
+	$document = load_remote_dom_document('https://wandystan.groups.io/g/wandystan/search?p=Created,,,10000,2,0,0&d=6&startdate=' . $date_start->format('m/d/Y') . '&enddate=' . $date_end->modify('-1 second')->format('m/d/Y'));
+} catch (Exception $e) {
+	exit_error($e->getMessage());
+}
+
+$xpath = new DOMXPath($document);
+
+// find all messages in search results and summarise them
+$messages = $xpath->query('//div[@id = "maincontent"]/table//td[span[@class = "subject"]]');
+$summary = [];
+
+foreach ($messages as $message) {
+	$sender = trim($xpath->evaluate('substring-before(substring-after(normalize-space(div[@class = "hidden-xs"]/following-sibling::text()[1]), "By "), " · #")', $message), '"');
+	$timestamp = $xpath->evaluate('substring-before(substring-after(.//script[@class = "timedisp"], "DisplayShortTime( "), " ,")', $message);
+
+	$date = new DateTime(null, $tz);
+	$date->setTimestamp($timestamp / 1000000000);
+
+	$key = serialize([
+		'date' => $date->format('Y-m-d'),
+		'sender' => $sender
+	]);
+	$summary[$key]++;
+}
+
+// construct a linked data object corresponding to gathered data
+$ld = [
+	'@context' => 'https://wandystan.eu/statistics/context.jsonld',
+	'@type' => 'Slice',
+	'key' => 'key/by-period',
+	'period' => [
+		'@type' => 'Interval',
+		'hasBeginning' => [
+			'@type' => 'Instant',
+			'timestamp' => $date_start->format(DATE_ATOM)
+		],
+		'hasEnd' => [
+			'@type' => 'Instant',
+			'timestamp' => $date_end->format(DATE_ATOM)
+		]
+	]
+];
+
+$observations = [];
+foreach ($summary as $key => $entry) {
+	$key = unserialize($key);
+	$nameHash = hash('fnv1a32', $key['sender']);
+	$observations []= [
+		'@id' => 'by-period/' . $key['date'] . '/P1D#' . $nameHash,
+		'@type' => 'Observation',
+		'dataset' => "by-period/",
+		'date' => $key['date'],
+		'sender' => [
+			'@id' => 'sender/' . $nameHash,
+			'@type' => 'Agent',
+			'name' => $key['sender'],
+			'name_fnv1a32sum' => $nameHash
+		],
+		'posts' => $entry
+	];
+}
+
+$ld['observations'] = $observations;
+
+header('Content-type: application/ld+json');
+print json_encode($ld);
diff --git a/common.php b/common.php
new file mode 100644
index 0000000..1be3e2b
--- /dev/null
+++ b/common.php
@@ -0,0 +1,33 @@
+<?php
+define('REGEXP_ISO8601_DATE', '\d{4}(-\d{2}|-?(\d{2}-?\d{2}|W\d{2}(-\d)?|\d{3}))');
+define('REGEXP_ISO8601_INTERVAL', 'P(\d+Y(\d+M)?(\d+D)?|\d+M(\d+D)?|\d+[DW])');
+define('REGEXP_ISO8601_DATE_OR_INTERVAL', '(' . REGEXP_ISO8601_DATE . '|' . REGEXP_ISO8601_INTERVAL . ')');
+
+function exit_error($message) {
+	http_response_code(500);
+	header('Content-type: text/plain; charset=utf-8');
+	print 'ERROR: ' . $message . PHP_EOL;
+	exit;
+}
+
+function exit_error_handler($exception) {
+	exit_error($exception->getMessage());
+}
+set_exception_handler('exit_error_handler');
+
+function load_remote_dom_document($uri) {
+	$content = file_get_contents($uri);
+	$document = new DOMDocument;
+
+	if ($content === false)
+		throw new Exception("Cannot load remote URI: $uri");
+
+	if (! $document->loadHTML($content, LIBXML_NOWARNING | LIBXML_NOERROR))
+		throw new Exception("Cannot parse remote URI as HTML: $uri");
+
+	return $document;
+}
+
+function starts_with($haystack, $needle) {
+    return substr_compare($haystack, $needle, 0, strlen($needle)) === 0;
+}
diff --git a/context.jsonld b/context.jsonld
new file mode 100644
index 0000000..634565c
--- /dev/null
+++ b/context.jsonld
@@ -0,0 +1,106 @@
+{
+	"@context": {
+		"@base": "https://wandystan.eu/statistics/",
+		"@vocab": "http://purl.org/linked-data/cube#",
+		"dcterms": "http://purl.org/dc/terms/",
+		"foaf": "http://xmlns.com/foaf/0.1/",
+		"prop": "https://wandystan.eu/statistics/property/",
+		"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+		"time": "http://www.w3.org/2006/time#",
+		"xsd": "http://www.w3.org/2001/XMLSchema#",
+
+		"Agent": "foaf:Agent",
+		"Instant": "time:Instant",
+		"Interval": "time:Interval",
+		"comment": {
+			"@id": "rdfs:comment",
+			"@container": "@language"
+		},
+		"componentAttachment": {
+			"@type": "@vocab"
+		},
+		"componentProperty": {
+			"@type": "@vocab"
+		},
+		"dataset": {
+			"@id": "dataSet",
+			"@type": "@id"
+		},
+		"date": {
+			"@id": "prop:date",
+			"@type": "xsd:date"
+		},
+		"description": {
+			"@id": "dcterms:description",
+			"@container": "@language"
+		},
+		"dimension": {
+			"@type": "@id"
+		},
+		"hasBeginning": {
+			"@id": "time:hasBeginning",
+			"@type": "@id"
+		},
+		"hasEnd": {
+			"@id": "time:hasEnd",
+			"@type": "@id"
+		},
+		"issued": {
+			"@id": "dcterms:issued",
+			"@type": "xsd:date"
+		},
+		"key": {
+			"@id": "sliceStructure",
+			"@type": "@id"
+		},
+		"label": {
+			"@id": "rdfs:label",
+			"@container": "@language"
+		},
+		"measure": {
+			"@type": "@id"
+		},
+		"name": "foaf:name",
+		"name_fnv1a32sum": {
+			"@id": "prop:name_fnv1a32sum",
+			"@type": "xsd:hexBinary"
+		},
+		"observations": "observation",
+		"period": {
+			"@id": "prop:period",
+			"@type": "prop:ISO8601-1"
+		},
+		"posts": {
+			"@id": "prop:posts",
+			"@type": "xsd:nonNegativeInteger"
+		},
+		"publisher": {
+			"@id": "dcterms:publisher",
+			"@type": "@id"
+		},
+		"sender": "prop:sender",
+		"sliceKey": {
+			"@type": "@id"
+		},
+		"slices": {
+			"@id": "slice",
+			"@type": "@id"
+		},
+		"structure": {
+			"@id": "structure",
+			"@type": "@id"
+		},
+		"subject": {
+			"@id": "dcterms:subject",
+			"@type": "@id"
+		},
+		"timestamp": {
+			"@id": "time:inXSDDateTimeStamp",
+			"@type": "xsd:dateTimeStamp"
+		},
+		"title": {
+			"@id": "dcterms:title",
+			"@container": "@language"
+		}
+	}
+}
diff --git a/doc/Makefile b/doc/Makefile
new file mode 100644
index 0000000..900aa4f
--- /dev/null
+++ b/doc/Makefile
@@ -0,0 +1,2 @@
+index.html: index.markdown
+	pandoc -s -c style.css -M lang=pl --shift-heading-level-by 1 -o $@ $<
diff --git a/doc/icon-author.svg b/doc/icon-author.svg
new file mode 100644
index 0000000..58b25d9
--- /dev/null
+++ b/doc/icon-author.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
\ No newline at end of file
diff --git a/doc/icon-date.svg b/doc/icon-date.svg
new file mode 100644
index 0000000..4b62597
--- /dev/null
+++ b/doc/icon-date.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M20 3h-1V1h-2v2H7V1H5v2H4c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 18H4V8h16v13z"/><path d="M0 0h24v24H0z" fill="none"/></svg>
\ No newline at end of file
diff --git a/doc/icon-json-ld.png b/doc/icon-json-ld.png
new file mode 100644
index 0000000000000000000000000000000000000000..a80d9f63190a31d404476141bc2af2e60d29210b
GIT binary patch
diff --git a/doc/index.html b/doc/index.html
new file mode 100644
index 0000000..0883631
--- /dev/null
+++ b/doc/index.html
@@ -0,0 +1,87 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" lang="pl" xml:lang="pl">
+<head>
+  <meta charset="utf-8" />
+  <meta name="generator" content="pandoc" />
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
+  <meta name="author" content="Polyna" />
+  <meta name="dcterms.date" content="2020-03-12" />
+  <title>Statystyki Listy Dyskusyjnej Mandragoratu Wandystanu</title>
+  <style>
+    code{white-space: pre-wrap;}
+    span.smallcaps{font-variant: small-caps;}
+    span.underline{text-decoration: underline;}
+    div.column{display: inline-block; vertical-align: top; width: 50%;}
+    div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
+    ul.task-list{list-style: none;}
+  </style>
+  <link rel="stylesheet" href="style.css" />
+  <!--[if lt IE 9]>
+    <script src="//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv-printshiv.min.js"></script>
+  <![endif]-->
+</head>
+<body>
+<header id="title-block-header">
+<h1 class="title">Statystyki Listy Dyskusyjnej Mandragoratu Wandystanu</h1>
+<p class="author"><a href="/w/profile,196">Polyna</a></p>
+<p class="date">2020-03-12</p>
+</header>
+<p>Statystyki <a href="https://wandystan.groups.io/g/wandystan/">Listy Dyskusyjnej Mandragoratu Wandystanu</a> są udostępniane jako dane połączone, przy użyciu słownictwa <a href="https://www.w3.org/TR/vocab-data-cube/" lang="en">RDF Data Cube</a>. Zapisane są one w postaci <a href="https://json-ld.org/" lang="en"><img src="icon-json-ld.png" style="vertical-align:bottom" /> JSON-LD</a> z <a href="../context.jsonld">odpowiednim kontekstem</a>, i mogą być przetwarzane zarówno za pomocą narzędzi przeznaczonych dla JSON-LD, jak i bezpośrednio, tak jak zwykłe dokumenty <a href="https://pl.wikipedia.org/wiki/JSON">JSON</a>.</p>
+<p>Wszystkie odnośniki podane w postaci względnej posiadają podstawę <a href="../"><code>https://wandystan.eu/statistics/</code></a> – przykładowo, <code>"key": "key/by-period"</code> oznacza odnośnik do adresu <a href="../key/by-period"><code>https://wandystan.eu/statistics/key/by-period</code></a>.</p>
+<p>Udostępniane są następujące rodzaje danych:</p>
+<h2 id="wiadomości-wg-daty-i-wysyłającego">Wiadomości wg daty i wysyłającego</h2>
+<p>Statystyki liczby wiadomości wysłanych przez danego wysyłającego w danym dniu, pogrupowane wg okresu w jakim zostały wysłane.</p>
+<p>Ze względu na ogromną ilość udostępnianych danych, statystyki podzielone są na wiele dokumentów:</p>
+<h3 id="zbiór-danych">Zbiór danych</h3>
+<p>Dokument opisujący udostępniany zbiór danych wraz z odnośnikami do jego wycinków obejmujących okres pojedynczego miesiąca.</p>
+<p>Adres dokumentu: <a href="../by-period/"><code>https://wandystan.eu/statistics/by-period/</code></a>.</p>
+<p>Układ dokumentu:</p>
+<ul>
+<li><code>@type</code> – rodzaj obiektu: zbiór danych statystycznych.</li>
+<li><code>title</code> – tytuł zbioru danych; obiekt którego kluczami są kody języków, a wartościami – tytuł zapisany w tym języku.</li>
+<li><code>description</code> – opis zbioru danych; obiekt którego kluczami są kody języków, a wartościami – opis zapisany w tym języku.</li>
+<li><code>publisher</code> – odnośnik do wydawcy dokumentu.</li>
+<li><code>structure</code> – odnośnik do formalnego opisu układu danych statystycznych.</li>
+<li><code>slices</code> – tablica odnośników do wycinków zbioru danych obejmujących okres pojedynczego miesiąca.</li>
+</ul>
+<h3 id="wycinki-danych">Wycinki danych</h3>
+<p>Dokumenty zawierające dane statystyczne z wybranego okresu.</p>
+<p>Adres dokumentu: <code>https://wandystan.eu/statistics/by-period/<var>⟨przedział⟩</var></code>; gdzie <code><var>⟨przedział⟩</var></code> to przedział dat w zapisie <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>, z podaną co najmniej jedną datą początkową lub/i jedną datą końcową, bez podanego czasu. Data początkowa jest zawsze uznawana jako zawierająca się w danym okresie, zaś data końcowa jako niezawierająca się. Przykłady poprawnie zapisanych przedziałów:</p>
+<ul>
+<li><code>2018-12-11/2019-04-21</code> – okres od 11 grudnia 2018 r. (włącznie) do 21 kwietnia 2019 r. (wyłącznie).</li>
+<li><code>2020-02/P1M</code> – cały luty 2020 r.</li>
+<li><code>P2M5D/2020-W03</code> – 2 miesiące i 5 dni poprzedzające trzeci tydzień 2020 r.</li>
+</ul>
+<p>Jeżeli w podanym przedziale napisano więcej niż 10 000 wiadomości, pod uwagę brane będą tylko te najpóźniej wysłane, więc nie jest zalecane podawanie zbyt długich przedziałów.</p>
+<p>Układ dokumentu:</p>
+<ul>
+<li><code>@type</code> – rodzaj obiektu: wycinek zbioru danych statystycznych.</li>
+<li><code>key</code> – odnośnik do klucza danych wycinka.</li>
+<li><code>period</code> – okres, którego dotyczy dany wycinek.
+<ul>
+<li><code>@type</code> – rodzaj obiektu: przedział czasu.</li>
+<li><code>hasBeginning</code> – początek przedziału czasu.
+<ul>
+<li><code>@type</code> – rodzaj obiektu: chwila w czasie.</li>
+<li><code>timestamp</code> – znacznik czasu w zapisie <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.</li>
+</ul></li>
+<li><code>hasEnd</code> – koniec przedziału czasu; obiekt o takim samym układzie co <code>hasBeginning</code>.</li>
+</ul></li>
+<li><code>observations</code> – poszczególne dane jednostkowe (spostrzeżenia) dotyczące danego okresu – tablica obiektów o następującej strukturze:
+<ul>
+<li><code>@id</code> – oznaczenie przypisane do danego spostrzeżenia.</li>
+<li><code>@type</code> – rodzaj obiektu: dana jednostkowa (spostrzeżenie).</li>
+<li><code>dataset</code> – odnośnik do zbioru, do którego przypisana jest dana.</li>
+<li><code>date</code> – dzień w którym zostały wysłane wiadomości na listę, w zapisie <a href="https://en.wikipedia.org/wiki/ISO_8601">ISO 8601</a>.</li>
+<li><code>sender</code> – wysyłający wiadomości na listę w danym dniu.
+<ul>
+<li><code>@id</code> – oznaczenie przypisane do danego wysyłającego.</li>
+<li><code>@type</code> – rodzaj obiektu: osoba bądź inna jednostka (organizacja, program samoczynnie wysyłający wiadomości, itp.) mająca możliwość podejmowania działań.</li>
+<li><code>name</code> – nazwa tej osoby bądź jednostki.</li>
+<li><code>name_fnv1a32sum</code> – skrót nazwy utworzony za pomocą <a href="https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function">algorytmu FNV-1a</a> w odmianie 32-bitowej, zapisany w postaci szesnastkowej. Może być przekształcony w równoważny ciąg bitów, bądź 32-bitową liczbę bez znaku.</li>
+</ul></li>
+<li><code>posts</code> – ilość postów wysłanych na listę przez danego wysyłającego w danym dniu.</li>
+</ul></li>
+</ul>
+</body>
+</html>
diff --git a/doc/index.markdown b/doc/index.markdown
new file mode 100644
index 0000000..e37f683
--- /dev/null
+++ b/doc/index.markdown
@@ -0,0 +1,67 @@
+% Statystyki Listy Dyskusyjnej Mandragoratu Wandystanu
+% [Polyna](/w/profile,196)
+% 2020-03-12
+
+Statystyki [Listy Dyskusyjnej Mandragoratu Wandystanu](https://wandystan.groups.io/g/wandystan/) są udostępniane jako dane połączone, przy użyciu słownictwa [RDF Data Cube](https://www.w3.org/TR/vocab-data-cube/){lang=en}. Zapisane są one w postaci [![](icon-json-ld.png){style=vertical-align:bottom} JSON-LD](https://json-ld.org/){lang=en} z [odpowiednim kontekstem](../context.jsonld), i mogą być przetwarzane zarówno za pomocą narzędzi przeznaczonych dla JSON-LD, jak i bezpośrednio, tak jak zwykłe dokumenty [JSON](https://pl.wikipedia.org/wiki/JSON).
+
+Wszystkie odnośniki podane w postaci względnej posiadają podstawę [`https://wandystan.eu/statistics/`](../) – przykładowo, `"key": "key/by-period"` oznacza odnośnik do adresu [`https://wandystan.eu/statistics/key/by-period`](../key/by-period).
+
+Udostępniane są następujące rodzaje danych:
+
+Wiadomości wg daty i wysyłającego
+=================================
+
+Statystyki liczby wiadomości wysłanych przez danego wysyłającego w danym dniu, pogrupowane wg okresu w jakim zostały wysłane.
+
+Ze względu na ogromną ilość udostępnianych danych, statystyki podzielone są na wiele dokumentów:
+
+Zbiór danych
+------------
+
+Dokument opisujący udostępniany zbiór danych wraz z odnośnikami do jego wycinków obejmujących okres pojedynczego miesiąca.
+
+Adres dokumentu: [`https://wandystan.eu/statistics/by-period/`](../by-period/).
+
+Układ dokumentu:
+
+*   `@type` – rodzaj obiektu: zbiór danych statystycznych.
+*   `title` – tytuł zbioru danych; obiekt którego kluczami są kody języków, a wartościami – tytuł zapisany w tym języku.
+*   `description` – opis zbioru danych; obiekt którego kluczami są kody języków, a wartościami – opis zapisany w tym języku.
+*   `publisher` – odnośnik do wydawcy dokumentu.
+*   `structure` – odnośnik do formalnego opisu układu danych statystycznych.
+*   `slices` – tablica odnośników do wycinków zbioru danych obejmujących okres pojedynczego miesiąca.
+
+Wycinki danych
+--------------
+
+Dokumenty zawierające dane statystyczne z wybranego okresu.
+
+Adres dokumentu: <code>https://wandystan.eu/statistics/by-period/<var>⟨przedział⟩</var></code>; gdzie <code><var>⟨przedział⟩</var></code> to przedział dat w zapisie [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601), z podaną co najmniej jedną datą początkową lub/i jedną datą końcową, bez podanego czasu. Data początkowa jest zawsze uznawana jako zawierająca się w danym okresie, zaś data końcowa jako niezawierająca się. Przykłady poprawnie zapisanych przedziałów:
+
+*   `2018-12-11/2019-04-21` – okres od 11 grudnia 2018 r. (włącznie) do 21 kwietnia 2019 r. (wyłącznie).
+*   `2020-02/P1M` – cały luty 2020 r.
+*   `P2M5D/2020-W03` – 2 miesiące i 5 dni poprzedzające trzeci tydzień 2020 r.
+
+Jeżeli w podanym przedziale napisano więcej niż 10 000 wiadomości, pod uwagę brane będą tylko te najpóźniej wysłane, więc nie jest zalecane podawanie zbyt długich przedziałów.
+
+Układ dokumentu:
+
+*   `@type` – rodzaj obiektu: wycinek zbioru danych statystycznych.
+*   `key` – odnośnik do klucza danych wycinka.
+*   `period` – okres, którego dotyczy dany wycinek.
+    *   `@type` – rodzaj obiektu: przedział czasu.
+    *   `hasBeginning` – początek przedziału czasu.
+        *   `@type` – rodzaj obiektu: chwila w czasie.
+        *   `timestamp` – znacznik czasu w zapisie [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601).
+    *   `hasEnd` – koniec przedziału czasu; obiekt o takim samym układzie co `hasBeginning`.
+*   `observations` – poszczególne dane jednostkowe (spostrzeżenia) dotyczące danego okresu – tablica obiektów o następującej strukturze:
+    *   `@id` – oznaczenie przypisane do danego spostrzeżenia.
+    *   `@type` – rodzaj obiektu: dana jednostkowa (spostrzeżenie).
+    *   `dataset` – odnośnik do zbioru, do którego przypisana jest dana.
+    *   `date` – dzień w którym zostały wysłane wiadomości na listę, w zapisie [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601).
+    *   `sender` – wysyłający wiadomości na listę w danym dniu.
+        *   `@id` – oznaczenie przypisane do danego wysyłającego.
+        *   `@type` – rodzaj obiektu: osoba bądź inna jednostka (organizacja, program samoczynnie wysyłający wiadomości, itp.) mająca możliwość podejmowania działań.
+        *   `name` – nazwa tej osoby bądź jednostki.
+        *   `name_fnv1a32sum` – skrót nazwy utworzony za pomocą [algorytmu FNV-1a](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function) w odmianie 32-bitowej, zapisany w postaci szesnastkowej. Może być przekształcony w równoważny ciąg bitów, bądź 32-bitową liczbę bez znaku.
+    *   `posts` – ilość postów wysłanych na listę przez danego wysyłającego w danym dniu.
diff --git a/doc/style.css b/doc/style.css
new file mode 100644
index 0000000..0a3c1d1
--- /dev/null
+++ b/doc/style.css
@@ -0,0 +1,15 @@
+body {
+	width: 50em;
+	font-family: sans-serif;
+	line-height: 1.5;
+}
+
+.author {
+	padding-left: 2em;
+	background: url('icon-author.svg') left center / contain no-repeat;
+}
+
+.date {
+	padding-left: 2em;
+	background: url('icon-date.svg') left center / contain no-repeat;
+}
diff --git a/properties.jsonld b/properties.jsonld
new file mode 100644
index 0000000..59e11f8
--- /dev/null
+++ b/properties.jsonld
@@ -0,0 +1,104 @@
+{
+	"@context": {
+		"@language": "en",
+		"dct": "http://purl.org/dc/terms/",
+		"owl": "http://www.w3.org/2002/07/owl#",
+		"prop": "https://wandystan.eu/statistics/property/",
+		"qb": "http://purl.org/linked-data/cube#",
+		"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
+		"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
+		"sdmx-concept": "http://purl.org/linked-data/sdmx/2009/concept#",
+		"sdmx-dimension": "http://purl.org/linked-data/sdmx/2009/dimension#",
+		"sdmx-measure": "http://purl.org/linked-data/sdmx/2009/measure#",
+		"xsd": "http://www.w3.org/2001/XMLSchema#",
+
+		"Datatype": "rdfs:Datatype",
+		"DatatypeProperty": "owl:DatatypeProperty",
+		"DimensionProperty": "qb:DimensionProperty",
+		"FunctionalProperty": "owl:FunctionalProperty",
+		"DimensionProperty": "qb:DimensionProperty",
+		"MeasureProperty": "owl:MeasureProperty",
+		"Ontology": "owl:Ontology",
+		"Property": "rdf:Property",
+		"defines": {
+			"@reverse": "rdfs:isDefinedBy"
+		},
+		"comment": "rdfs:comment",
+		"concept": {
+			"@id": "qb:concept",
+			"@type": "@id"
+		},
+		"created": {
+			"@id": "dct:created",
+			"@type": "xsd:date"
+		},
+		"creator": {
+			"@id": "dct:creator",
+			"@type": "@id"
+		},
+		"description": "dct:description",
+		"isDefinedBy": {
+			"@id": "rdfs:isDefinedBy",
+			"@type": "@id"
+		},
+		"label": "rdfs:label",
+		"range": {
+			"@id": "rdfs:range",
+			"@type": "@id"
+		},
+		"seeAlso": {
+			"@id": "rdfs:seeAlso",
+			"@type": "@id"
+		},
+		"subPropertyOf": {
+			"@id": "rdfs:subPropertyOf",
+			"@type": "@id"
+		},
+		"title": "dct:title"
+	},
+	"@id": "prop:",
+	"@type": "Ontology",
+	"title": "Statistical ontology for on-line discussion spaces",
+	"created": "2020-03-06",
+	"creator": "https://wandystan.eu/B196",
+	"description": "This ontology defines properties used for publication of the statistical data about posts published in on-line discussion groups, lists and fora according to the RDF Data Cube model <https://www.w3.org/TR/vocab-data-cube/>.",
+	"defines": [
+		{
+			"@id": "prop:date",
+			"@type": ["Property", "DatatypeProperty", "DimensionProperty", "FunctionalProperty"],
+			"label": "Date",
+			"comment": "Date in which posts were was sent.",
+			"concept": "sdmx-concept:refPeriod",
+			"range": "xsd:date",
+			"subPropertyOf": ["sdmx-dimension:refPeriod", "dct:date"]
+		}, {
+			"@id": "prop:name_fnv1a32sum",
+			"@type": ["Property", "DatatypeProperty"],
+			"label": "32-bit FNV-1a sum of a name",
+			"comment": "A name for some thing hashed with the 32-bit flavor of the FNV-1a algorithm.",
+			"range": "xsd:hexBinary"
+		}, {
+			"@id": "prop:period",
+			"@type": ["Property", "DimensionProperty", "ObjectProperty"],
+			"label": "Period",
+			"comment": "Period in which posts were sent.",
+			"concept": "sdmx-concept:refPeriod",
+			"range": "http://www.w3.org/2006/time#Interval",
+			"subPropertyOf": ["sdmx-dimension:refPeriod", "dct:date"]
+		}, {
+			"@id": "prop:posts",
+			"@type": ["Property", "DatatypeProperty", "FunctionalProperty", "MeasureProperty"],
+			"label": "Number of posts",
+			"comment": "Number of posts sent in a certain period of time.",
+			"concept": "sdmx-concept:obsValue",
+			"range": "xsd:nonNegativeInteger",
+			"subPropertyOf": "sdmx-measure:obsValue"
+		}, {
+			"@id": "prop:sender",
+			"@type": ["Property", "DimensionProperty", "FunctionalProperty", "ObjectProperty"],
+			"label": "Sender",
+			"comment": "Sender of the post (not necessarily a person, may be e.g. an automated posting agent).",
+			"range": "dct:Agent"
+		}
+	]
+}
diff --git a/sender.php b/sender.php
new file mode 100644
index 0000000..eb9ef14
--- /dev/null
+++ b/sender.php
@@ -0,0 +1,14 @@
+<?php
+require_once 'common.php';
+
+$hash = $_GET['hash'];
+if (! mb_ereg_match('^[0-9a-f]{8}$', $hash))
+	exit_error('Invalid hash.');
+
+header('Content-type: application/ld+json');
+print json_encode([
+	'@context' => 'https://wandystan.eu/statistics/context.jsonld',
+	'@id' => 'sender/' . $hash,
+	'@type' => 'Agent',
+	'name_fnv1a32sum' => $hash
+]);
diff --git a/structures.jsonld b/structures.jsonld
new file mode 100644
index 0000000..7a314f1
--- /dev/null
+++ b/structures.jsonld
@@ -0,0 +1,37 @@
+{
+	"@context": "https://wandystan.eu/statistics/context.jsonld",
+	"@graph": [
+		{
+			"@id": "structure/by-period",
+			"@type": "DataStructureDefinition",
+			"component": [
+				{
+					"measure": "prop:posts"
+				}, {
+					"dimension": "prop:sender",
+					"order": 1
+				}, {
+					"dimension": "prop:date",
+					"order": 2
+				}, {
+					"componentAttachment": "Slice",
+					"dimension": "prop:period",
+					"order": 3
+				}
+			],
+			"sliceKey": "key/by-period"
+		}, {
+			"@id": "key/by-period",
+			"@type": "SliceKey",
+			"label": {
+				"en": "time slice",
+				"pl": "wycinek czasu"
+			},
+			"comment": {
+				"en": "Slice grouping data from a given time period.",
+				"pl": "Wycinek grupujący dane z danego okresu czasowego."
+			},
+			"componentProperty": "prop:period"
+		}
+	]
+}
