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 .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ plugins/SumoLogic/* @richpeters
plugins/TransportForLondon/* @clarkd
plugins/UniFi/* @adamkinniburgh
plugins/UptimeRobot/* @kieranlangton
plugins/WorldCup2026/* @TimWheeler-SQUP


# Fallback – if a plugin has no specified author
Expand Down
11 changes: 11 additions & 0 deletions plugins/WorldCup2026/v1/configValidation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"steps": [
{
"displayName": "Connect to openfootball",
"dataStream": { "name": "worldcup2026-teams-import" },
"required": true,
"error": "Could not reach the openfootball data source. Check your network connection.",
"success": "Connected successfully."
}
]
}
9 changes: 9 additions & 0 deletions plugins/WorldCup2026/v1/custom_types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"name": "World Cup Team",
"sourceType": "World Cup Team",
"icon": "flag",
"singular": "Team",
"plural": "Teams"
}
]
61 changes: 61 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
var matches = data.matches || [];
var teamFilter = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : '';

var groupMatches = matches.filter(function(m) { return m.group; });

var standings = {};

function ensureTeam(name, group) {
if (!standings[name]) {
standings[name] = { group: group, mp: 0, w: 0, d: 0, l: 0, gf: 0, ga: 0, pts: 0 };
}
}

groupMatches.forEach(function(m) {
ensureTeam(m.team1, m.group);
ensureTeam(m.team2, m.group);

if (!m.score || !m.score.ft) return;

var s1 = m.score.ft[0];
var s2 = m.score.ft[1];
var t1 = standings[m.team1];
var t2 = standings[m.team2];

t1.mp++; t2.mp++;
t1.gf += s1; t1.ga += s2;
t2.gf += s2; t2.ga += s1;

if (s1 > s2) {
t1.w++; t1.pts += 3; t2.l++;
} else if (s2 > s1) {
t2.w++; t2.pts += 3; t1.l++;
} else {
t1.d++; t1.pts++; t2.d++; t2.pts++;
}
});

var rows = Object.keys(standings).map(function(name) {
var s = standings[name];
return {
sourceId: name,
team: name,
group: s.group,
mp: s.mp, w: s.w, d: s.d, l: s.l,
gf: s.gf, ga: s.ga, gd: s.gf - s.ga, pts: s.pts
};
});

if (teamFilter && standings[teamFilter]) {
var targetGroup = standings[teamFilter].group;
rows = rows.filter(function(r) { return r.group === targetGroup; });
}

rows.sort(function(a, b) {
if (a.group !== b.group) return a.group.localeCompare(b.group);
if (b.pts !== a.pts) return b.pts - a.pts;
if (b.gd !== a.gd) return b.gd - a.gd;
return b.gf - a.gf;
});

result = rows;
28 changes: 28 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
var matches = data.matches || [];

var stageMap = {
'Round of 32': 'Round of 32',
'Round of 16': 'Round of 16',
'Quarter-final': 'Quarter-Final',
'Semi-final': 'Semi-Final',
'Third-place play-off': 'Third Place',
'Final': 'Final'
};

result = matches.filter(function(m) { return !m.group; }).map(function(m) {
var score = '-';
var status = 'Upcoming';
if (m.score && m.score.ft) {
score = m.score.ft[0] + '-' + m.score.ft[1];
status = 'Finished';
}

return {
date: m.date,
round: stageMap[m.round] || m.round,
home_team: m.team1,
away_team: m.team2,
score: score,
status: status
};
});
41 changes: 41 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
var matches = data.matches || [];
var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : '';
var sourceId = context.objects[0] ? context.objects[0].sourceId : '';

var knockoutStageMap = {
'Round of 32': 'Round of 32',
'Round of 16': 'Round of 16',
'Quarter-final': 'Quarter-Final',
'Semi-final': 'Semi-Final',
'Third-place play-off': 'Third Place',
'Final': 'Final'
};

var played = matches.filter(function(m) {
return m.score && (m.team1 === teamName || m.team2 === teamName);
});

played.sort(function(a, b) {
return new Date(b.date) - new Date(a.date);
});

if (played.length === 0) {
result = [{ date: 'No matches played yet', home_away: '', opponent: '', score: '', result: '', stage: '', sourceId: sourceId }];
} else {
var last = played[0];
var isHome = last.team1 === teamName;
var opponent = isHome ? last.team2 : last.team1;
var myScore = isHome ? last.score.ft[0] : last.score.ft[1];
var oppScore = isHome ? last.score.ft[1] : last.score.ft[0];
var matchResult = myScore > oppScore ? 'Win' : myScore < oppScore ? 'Loss' : 'Draw';

result = [{
date: last.date,
home_away: isHome ? 'Home' : 'Away',
opponent: opponent,
score: myScore + '-' + oppScore,
result: matchResult,
stage: last.group ? 'Group Stage' : (knockoutStageMap[last.round] || last.round),
sourceId: sourceId
}];
}
38 changes: 38 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
var matches = data.matches || [];
var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : '';

var knockoutStageMap = {
'Round of 32': 'Round of 32',
'Round of 16': 'Round of 16',
'Quarter-final': 'Quarter-Final',
'Semi-final': 'Semi-Final',
'Third-place play-off': 'Third Place',
'Final': 'Final'
};

var filtered = matches.filter(function(m) {
return m.team1 === teamName || m.team2 === teamName;
});

filtered.sort(function(a, b) {
return new Date(a.date) - new Date(b.date);
});

result = filtered.map(function(m) {
var score = '-';
var status = 'Upcoming';
if (m.score && m.score.ft) {
score = m.score.ft[0] + '-' + m.score.ft[1];
status = 'Finished';
}

return {
date: m.date,
home_team: m.team1,
away_team: m.team2,
score: score,
group: m.group || '',
stage: m.group ? 'Group Stage' : (knockoutStageMap[m.round] || m.round),
status: status
};
});
36 changes: 36 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
var matches = data.matches || [];
var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : '';
var sourceId = context.objects[0] ? context.objects[0].sourceId : '';

var knockoutStageMap = {
'Round of 32': 'Round of 32',
'Round of 16': 'Round of 16',
'Quarter-final': 'Quarter-Final',
'Semi-final': 'Semi-Final',
'Third-place play-off': 'Third Place',
'Final': 'Final'
};

var upcoming = matches.filter(function(m) {
return !m.score && (m.team1 === teamName || m.team2 === teamName);
});

upcoming.sort(function(a, b) {
return new Date(a.date) - new Date(b.date);
});

if (upcoming.length === 0) {
result = [{ date: 'No upcoming matches', home_away: '', opponent: '', stage: '', group: '', sourceId: sourceId }];
} else {
var next = upcoming[0];
var isHome = next.team1 === teamName;
var opponent = isHome ? next.team2 : next.team1;
result = [{
date: next.date,
home_away: isHome ? 'Home' : 'Away',
opponent: opponent,
stage: next.group ? 'Group Stage' : (knockoutStageMap[next.round] || next.round),
group: next.group || '',
sourceId: sourceId
}];
}
27 changes: 27 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
var matches = data.matches || [];
var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : '';
var group = context.objects[0] ? [].concat(context.objects[0].group)[0] || '' : '';

var groupGames = matches.filter(function(m) {
return m.group && (m.team1 === teamName || m.team2 === teamName);
});

var mp = 0, w = 0, d = 0, l = 0, gf = 0, ga = 0;

groupGames.forEach(function(m) {
if (!m.score || !m.score.ft) return;
mp++;
var isHome = m.team1 === teamName;
var myScore = isHome ? m.score.ft[0] : m.score.ft[1];
var oppScore = isHome ? m.score.ft[1] : m.score.ft[0];
gf += myScore;
ga += oppScore;
if (myScore > oppScore) w++;
else if (myScore < oppScore) l++;
else d++;
});

var pts = (w * 3) + d;
var gd = gf - ga;

result = [{ country: teamName, group: group, mp: mp, w: w, d: d, l: l, pts: pts, gf: gf, ga: ga, gd: gd, sourceId: teamName }];
14 changes: 14 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/teams-import.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
var matches = data.matches || [];
var seen = {};
var teams = [];

matches.filter(function(m) { return m.group; }).forEach(function(m) {
[{ name: m.team1, group: m.group }, { name: m.team2, group: m.group }].forEach(function(t) {
if (!seen[t.name]) {
seen[t.name] = true;
teams.push({ sourceId: t.name, name_en: t.name, group: t.group });
}
});
});

result = teams;
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "worldcup2026-group-standings",
"displayName": "Group Standings",
"description": "Group stage standings calculated from match results, with optional filter to a specific team's group",
"tags": ["Standings"],
"baseDataSourceName": "httpRequestScopedSingle",
"config": {
"httpMethod": "get",
"endpointPath": "/worldcup.json",
"postRequestScript": "group-standings.js"
},
"matches": {
"sourceType": {
"type": "equals",
"value": "World Cup Team"
}
},
"timeframes": false,
"metadata": [
{ "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false },
{ "name": "group", "displayName": "Group", "shape": "string" },
{ "name": "team", "displayName": "Team", "shape": "string", "role": "label" },
{ "name": "mp", "displayName": "Played", "shape": "number" },
{ "name": "w", "displayName": "W", "shape": "number" },
{ "name": "d", "displayName": "D", "shape": "number" },
{ "name": "l", "displayName": "L", "shape": "number" },
{ "name": "gf", "displayName": "GF", "shape": "number" },
{ "name": "ga", "displayName": "GA", "shape": "number" },
{ "name": "gd", "displayName": "GD", "shape": "number" },
{ "name": "pts", "displayName": "Points", "shape": "number" },
{ "name": "team", "sourceId": "sourceId", "sourceType": "World Cup Team" }
]
}
26 changes: 26 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/worldcup2026-knockout.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "worldcup2026-knockout",
"displayName": "Knockout Matches",
"description": "All knockout stage fixtures, showing placeholder labels until teams are determined",
"tags": ["Matches"],
"baseDataSourceName": "httpRequestUnscoped",
"config": {
"httpMethod": "get",
"endpointPath": "/worldcup.json",
"postRequestScript": "knockout.js"
},
"matches": "none",
"timeframes": false,
"metadata": [
{ "name": "date", "displayName": "Date", "shape": "string", "role": "label" },
{ "name": "round", "displayName": "Round", "shape": "string" },
{ "name": "home_team", "displayName": "Home Team", "shape": "string" },
{ "name": "away_team", "displayName": "Away Team", "shape": "string" },
{ "name": "score", "displayName": "Score", "shape": "string" },
{
"name": "status",
"displayName": "Status",
"shape": ["state", { "map": { "success": ["Finished"], "warning": ["Live"], "unknown": ["Upcoming"] } }]
}
]
}
32 changes: 32 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/worldcup2026-last-match.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "worldcup2026-last-match",
"displayName": "Last Match",
"description": "The most recently completed match for the selected team",
"tags": ["Matches"],
"baseDataSourceName": "httpRequestScopedSingle",
"config": {
"httpMethod": "get",
"endpointPath": "/worldcup.json",
"postRequestScript": "last-match.js"
},
"matches": {
"sourceType": {
"type": "equals",
"value": "World Cup Team"
}
},
"timeframes": false,
"metadata": [
{ "name": "date", "displayName": "Date", "shape": "string", "role": "label" },
{ "name": "home_away", "displayName": "Home / Away", "shape": "string" },
{ "name": "opponent", "displayName": "Opponent", "shape": "string" },
{ "name": "score", "displayName": "Score", "shape": "string" },
{
"name": "result",
"displayName": "Result",
"shape": ["state", { "map": { "success": ["Win"], "error": ["Loss"], "unknown": ["Draw"] } }]
},
{ "name": "stage", "displayName": "Stage", "shape": "string" },
{ "name": "sourceId", "displayName": "Object ID", "shape": "string", "visible": false }
]
}
Loading
Loading