Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions LocalSettings.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
$wgLogos = [
'1x' => "$wgResourceBasePath/assets/logo.png",
];
$wgFavicon = "$wgResourceBasePath/assets/favicon.ico";

## Skins
wfLoadSkin( 'MonoBook' );
Expand Down
Binary file added assets/favicon.ico
Binary file not shown.
18 changes: 11 additions & 7 deletions extensions/BlueRailroadIntegration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ wiki editors to add/modify leaderboards without touching code.

{{BlueRailroadLeaderboard
|page=Blue Railroad Squats Leaderboard
|filter_song_id=5
|filter_song_id=7
|description=Leaderboard for Squats (Blue Railroad Train)
}}
```
Expand All @@ -65,13 +65,17 @@ wiki editors to add/modify leaderboards without touching code.
| `description` | Description shown at top of leaderboard |
| `sort` | Sort order: `count` (default), `newest`, `oldest` |

### Song IDs
### Song IDs (Manzanita Track Numbers)

| ID | Exercise | Song |
|----|----------|------|
| 5 | Squats | Blue Railroad Train |
| 6 | Pushups | Nine Pound Hammer |
| 10 | Army Crawls | Ginseng Sullivan |
Song IDs correspond to track numbers on Tony Rice's *Manzanita* album (1979):

| Track # | Song | Exercise |
|---------|------|----------|
| 5 | Nine Pound Hammer | Pushups |
| 7 | Blue Railroad Train | Squats |
| 8 | Ginseng Sullivan | Army Crawls |

The chain data generator should use these track numbers as the `songId` value.

## Running the Import

Expand Down
13 changes: 13 additions & 0 deletions extensions/BlueRailroadIntegration/extension.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,18 @@
"AutoloadNamespaces": {
"MediaWiki\\Extension\\BlueRailroadIntegration\\": "src/"
},
"ResourceFileModulePaths": {
"localBasePath": "",
"remoteExtPath": "BlueRailroadIntegration"
},
"ResourceModules": {
"ext.bluerailroad.datepicker": {
"scripts": ["resources/ext.bluerailroad.datepicker.js"],
"targets": ["desktop", "mobile"]
}
},
"Hooks": {
"BeforePageDisplay": "MediaWiki\\Extension\\BlueRailroadIntegration\\Hooks::onBeforePageDisplay"
},
"manifest_version": 2
}
138 changes: 97 additions & 41 deletions extensions/BlueRailroadIntegration/maintenance/importBlueRailroads.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ public function execute() {
$this->fatalError("Failed to parse JSON: " . json_last_error_msg());
}

// Aggregate all tokens from all sources for leaderboards
$allTokens = [];

// Process each source
foreach ($config['sources'] as $source) {
$chainDataKey = $source['chain_data_key'] ?? 'blueRailroads';
Expand Down Expand Up @@ -93,18 +96,23 @@ public function execute() {
} else {
$errors++;
}

// Add to aggregated tokens for leaderboards
// Use source-prefixed key to avoid collisions between V1 and V2 token IDs
$aggregateKey = $chainDataKey . '_' . $tokenId;
$allTokens[$aggregateKey] = $token;
}

$this->output("\nToken Import Summary:\n");
$this->output(" Imported: $imported\n");
$this->output(" Updated: $updated\n");
$this->output(" Errors: $errors\n");
}

// Generate leaderboards from config
$this->output("\nGenerating leaderboards...\n");
foreach ($config['leaderboards'] as $leaderboard) {
$this->generateLeaderboard($blueRailroads, $leaderboard, $dryRun);
}
// Generate leaderboards from aggregated tokens (outside source loop)
$this->output("\nGenerating leaderboards from " . count($allTokens) . " total tokens...\n");
foreach ($config['leaderboards'] as $leaderboard) {
$this->generateLeaderboard($allTokens, $leaderboard, $dryRun);
}
}

Expand Down Expand Up @@ -344,6 +352,20 @@ private function generateLeaderboard($blueRailroads, $leaderboardConfig, $dryRun
return;
}

// Check if content has actually changed
if ($exists) {
$services = MediaWiki\MediaWikiServices::getInstance();
$revisionLookup = $services->getRevisionLookup();
$currentRevision = $revisionLookup->getRevisionByTitle($title);
if ($currentRevision) {
$currentContent = $currentRevision->getContent('main');
if ($currentContent && $currentContent->getText() === $content) {
$this->output(" No changes needed (content identical)\n");
return;
}
}
}

// Save the page
$page = MediaWiki\MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle($title);
$contentObj = ContentHandler::makeContent($content, $title);
Expand Down Expand Up @@ -542,58 +564,92 @@ private function buildTokenPageContent($tokenId, $token) {
// Extract values, handling potential BigInt serialization and missing keys
$id = isset($token['id']) ? (is_array($token['id']) ? $token['id'][0] : $token['id']) : $tokenId;
$songId = isset($token['songId']) ? (is_array($token['songId']) ? $token['songId'][0] : $token['songId']) : '';
$date = isset($token['date']) ? (is_array($token['date']) ? $token['date'][0] : $token['date']) : '';
$owner = $token['owner'] ?? '';
$ownerDisplay = $token['ownerDisplay'] ?? $owner;
$uri = $token['uri'] ?? '';

// Determine URI type for template
$uriType = 'unknown';
if (strpos($uri, 'ipfs://') === 0) {
$uriType = 'ipfs';
} elseif (strpos($uri, 'https://') === 0) {
$uriType = 'https';
// Detect V2 vs V1 by presence of blockheight (V2) vs date (V1)
$isV2 = isset($token['blockheight']);

// V2 uses blockheight, V1 uses date
$blockheight = '';
$date = '';
$formattedDate = '';

if ($isV2) {
$blockheight = isset($token['blockheight']) ? (is_array($token['blockheight']) ? $token['blockheight'][0] : $token['blockheight']) : '';
} else {
$date = isset($token['date']) ? (is_array($token['date']) ? $token['date'][0] : $token['date']) : '';

// Convert date to readable format
// Handles both YYYYMMDD format (8 digits) and Unix timestamps (10 digits)
$dateStr = (string)$date;
if (strlen($dateStr) === 8 && $dateStr[0] === '2') {
// YYYYMMDD format (e.g., 20260113)
$year = substr($dateStr, 0, 4);
$month = substr($dateStr, 4, 2);
$day = substr($dateStr, 6, 2);
$formattedDate = "$year-$month-$day";
} elseif (strlen($dateStr) >= 10 && is_numeric($dateStr)) {
// Unix timestamp (e.g., 1705685808)
$timestamp = (int)$dateStr;
$formattedDate = date('Y-m-d', $timestamp);
}
}

// Extract IPFS CID if applicable
// V2 uses videoHash, V1 uses uri
$uri = '';
$videoHash = '';
$uriType = 'unknown';
$ipfsCid = '';
if ($uriType === 'ipfs') {
$ipfsCid = substr($uri, 7);
}

// Convert date to readable format
// Handles both YYYYMMDD format (8 digits) and Unix timestamps (10 digits)
$dateStr = (string)$date;
$formattedDate = '';
if (strlen($dateStr) === 8 && $dateStr[0] === '2') {
// YYYYMMDD format (e.g., 20260113)
$year = substr($dateStr, 0, 4);
$month = substr($dateStr, 4, 2);
$day = substr($dateStr, 6, 2);
$formattedDate = "$year-$month-$day";
} elseif (strlen($dateStr) >= 10 && is_numeric($dateStr)) {
// Unix timestamp (e.g., 1705685808)
$timestamp = (int)$dateStr;
$formattedDate = date('Y-m-d', $timestamp);
if ($isV2) {
$videoHash = $token['videoHash'] ?? '';
// V2 videoHash is stored as bytes32 hex, can be used with IPFS
if (!empty($videoHash) && $videoHash !== '0x0000000000000000000000000000000000000000000000000000000000000000') {
$uriType = 'ipfs';
// Remove 0x prefix for IPFS CID
$ipfsCid = strpos($videoHash, '0x') === 0 ? substr($videoHash, 2) : $videoHash;
$uri = "ipfs://$ipfsCid";
}
} else {
$uri = $token['uri'] ?? '';
if (strpos($uri, 'ipfs://') === 0) {
$uriType = 'ipfs';
$ipfsCid = substr($uri, 7);
} elseif (strpos($uri, 'https://') === 0) {
$uriType = 'https';
}
}

// Build page content using a template
$lines = [
"{{Blue Railroad Token",
"|token_id=$id",
"|song_id=$songId",
"|date=$formattedDate",
"|date_raw=$date",
"|owner=$owner",
"|owner_display=$ownerDisplay",
"|uri=$uri",
"|uri_type=$uriType",
"|ipfs_cid=$ipfsCid",
"}}",
"",
"[[Category:Blue Railroad Tokens]]",
"|contract_version=" . ($isV2 ? 'V2' : 'V1'),
];

// Add version-specific fields
if ($isV2) {
$lines[] = "|blockheight=$blockheight";
$lines[] = "|video_hash=$videoHash";
} else {
$lines[] = "|date=$formattedDate";
$lines[] = "|date_raw=$date";
}

$lines[] = "|owner=$owner";
$lines[] = "|owner_display=$ownerDisplay";
$lines[] = "|uri=$uri";
$lines[] = "|uri_type=$uriType";
$lines[] = "|ipfs_cid=$ipfsCid";
$lines[] = "}}";
$lines[] = "";
$lines[] = "[[Category:Blue Railroad Tokens]]";
if ($isV2) {
$lines[] = "[[Category:Blue Railroad V2 Tokens]]";
}

return implode("\n", $lines);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Date to Block Height Converter for Blue Railroad submission forms
*/
(function() {
'use strict';

var AVG_BLOCK_TIME = 12.12; // Post-merge average

function getCurrentBlockFromFooter() {
var link = document.querySelector('a[href*="etherscan.io/block/"]');
if (link) {
var match = link.href.match(/block\/(\d+)/);
if (match) return parseInt(match[1]);
}
return null;
}

function dateToBlockHeight(targetDate, refBlock, refTimestamp) {
var targetTimestamp = targetDate.getTime() / 1000;
var blocksDiff = Math.round((refTimestamp - targetTimestamp) / AVG_BLOCK_TIME);
return refBlock - blocksDiff;
}

function init() {
var blockInput = document.querySelector('input[name*="[block_height]"]');
if (!blockInput) return;

var container = document.createElement('div');
container.style.cssText = 'margin-top:8px;';
container.innerHTML =
'<label style="display:block;margin-bottom:4px;font-size:0.9em;">Or pick date/time:</label>' +
'<input type="datetime-local" id="br-datepicker" style="padding:4px;margin-right:8px;">' +
'<button type="button" id="br-convert" style="padding:4px 12px;">Convert</button>' +
'<span id="br-status" style="margin-left:8px;font-size:0.9em;color:#666;"></span>';

blockInput.parentNode.appendChild(container);

var picker = document.getElementById('br-datepicker');
var btn = document.getElementById('br-convert');
var status = document.getElementById('br-status');

picker.value = new Date().toISOString().slice(0, 16);

btn.onclick = function() {
var date = new Date(picker.value);
if (isNaN(date.getTime())) {
status.textContent = 'Invalid date';
status.style.color = 'red';
return;
}

var block = getCurrentBlockFromFooter();
if (!block) {
status.textContent = 'No reference block found';
status.style.color = 'red';
return;
}

var est = dateToBlockHeight(date, block, Date.now() / 1000);
blockInput.value = est;
status.textContent = est > block ? 'Future!' : '~estimated';
status.style.color = est > block ? 'orange' : 'green';
};
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
19 changes: 19 additions & 0 deletions extensions/BlueRailroadIntegration/src/Hooks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
namespace MediaWiki\Extension\BlueRailroadIntegration;

use MediaWiki\Output\OutputPage;
use Skin;

class Hooks {
/**
* Load datepicker module on Blue Railroad form pages
*/
public static function onBeforePageDisplay(OutputPage $out, Skin $skin): void {
$title = $out->getTitle();

// Load on FormEdit pages for Blue Railroad
if ($title && $title->isSpecial('FormEdit')) {
$out->addModules(['ext.bluerailroad.datepicker']);
}
}
}
7 changes: 7 additions & 0 deletions tools/blue-railroad-import/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.venv/
__pycache__/
*.pyc
*.egg-info/
.pytest_cache/
dist/
build/
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Blue Railroad Import Bot - Python implementation with tests."""
Loading