Skip to content
Closed
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 worldcup26.ir",
"dataStream": { "name": "worldcup2026-teams-import" },
"required": true,
"error": "Could not reach the worldcup26.ir API. 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"
}
]
67 changes: 67 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/group-standings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
var games = data.games || [];
var teamFilter = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : '';

var groupGames = games.filter(function(g) { return g.type === 'group'; });

var standings = {};

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

groupGames.forEach(function(g) {
ensureTeam(g.home_team_name_en, g.home_team_id, g.group);
ensureTeam(g.away_team_name_en, g.away_team_id, g.group);

if (g.finished !== 'TRUE') return;

var homeScore = parseInt(g.home_score, 10) || 0;
var awayScore = parseInt(g.away_score, 10) || 0;
var home = standings[g.home_team_name_en];
var away = standings[g.away_team_name_en];

home.mp++; away.mp++;
home.gf += homeScore; home.ga += awayScore;
away.gf += awayScore; away.ga += homeScore;

if (homeScore > awayScore) {
home.w++; home.pts += 3; away.l++;
} else if (awayScore > homeScore) {
away.w++; away.pts += 3; home.l++;
} else {
home.d++; home.pts++; away.d++; away.pts++;
}
});

var rows = Object.keys(standings).map(function(k) {
var s = standings[k];
return {
sourceId: s.id,
team: k,
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;
35 changes: 35 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/knockout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
var games = data.games || [];

var stageMap = {
r32: 'Round of 32',
r16: 'Round of 16',
qf: 'Quarter-Final',
sf: 'Semi-Final',
third: 'Third Place',
final: 'Final'
};

result = games.filter(function(g) { return g.type !== 'group'; }).map(function(g) {
var homeTeam = g.home_team_id !== '0' ? g.home_team_name_en : (g.home_team_label || 'TBD');
var awayTeam = g.away_team_id !== '0' ? g.away_team_name_en : (g.away_team_label || 'TBD');

var score = '-';
if (g.finished === 'TRUE') {
score = g.home_score + '-' + g.away_score;
} else if (g.time_elapsed !== 'notstarted') {
score = g.home_score + '-' + g.away_score + ' (Live)';
}

var status = 'Upcoming';
if (g.finished === 'TRUE') status = 'Finished';
else if (g.time_elapsed !== 'notstarted') status = 'Live';

return {
date: g.local_date,
round: stageMap[g.type] || g.type,
home_team: homeTeam,
away_team: awayTeam,
score: score,
status: status
};
});
51 changes: 51 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/last-match.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
var games = data.games || [];
var teamId = context.objects[0] ? String(context.objects[0].teamId) : '';

var stageMap = {
group: 'Group Stage',
r32: 'Round of 32',
r16: 'Round of 16',
qf: 'Quarter-Final',
sf: 'Semi-Final',
third: 'Third Place',
final: 'Final'
};

function parseDate(d) {
var parts = d.split(' ');
var dateParts = parts[0].split('/');
return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00');
}

var played = games.filter(function(g) {
return g.finished === 'TRUE' &&
(g.home_team_id === teamId || g.away_team_id === teamId);
});

played.sort(function(a, b) {
return parseDate(b.local_date) - parseDate(a.local_date);
});

var sourceId = context.objects[0] ? context.objects[0].sourceId : '';

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.home_team_id === teamId;
var opponent = isHome ? last.away_team_name_en : last.home_team_name_en;
var myScore = parseInt(isHome ? last.home_score : last.away_score, 10);
var oppScore = parseInt(isHome ? last.away_score : last.home_score, 10);
var scoreStr = isHome ? last.home_score + '-' + last.away_score : last.away_score + '-' + last.home_score;
var matchResult = myScore > oppScore ? 'Win' : myScore < oppScore ? 'Loss' : 'Draw';

result = [{
date: last.local_date,
home_away: isHome ? 'Home' : 'Away',
opponent: opponent,
score: scoreStr,
result: matchResult,
stage: stageMap[last.type] || last.type,
sourceId: sourceId
}];
}
56 changes: 56 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/matches.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
var games = data.games || [];
var teamId = context.objects[0] ? String(context.objects[0].teamId) : '';

var stageMap = {
group: 'Group Stage',
r32: 'Round of 32',
r16: 'Round of 16',
qf: 'Quarter-Final',
sf: 'Semi-Final',
third: 'Third Place',
final: 'Final'
};

function parseDate(d) {
var parts = d.split(' ');
var dateParts = parts[0].split('/');
return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00');
}

function getTeamName(game, side) {
if (side === 'home') {
return game.home_team_id !== '0' ? game.home_team_name_en : (game.home_team_label || 'TBD');
}
return game.away_team_id !== '0' ? game.away_team_name_en : (game.away_team_label || 'TBD');
}

var filtered = games.filter(function(g) {
return g.home_team_id === teamId || g.away_team_id === teamId;
});

filtered.sort(function(a, b) {
return parseDate(a.local_date) - parseDate(b.local_date);
});

result = filtered.map(function(g) {
var score = '-';
if (g.finished === 'TRUE') {
score = g.home_score + '-' + g.away_score;
} else if (g.time_elapsed !== 'notstarted') {
score = g.home_score + '-' + g.away_score + ' (Live)';
}

var status = 'Upcoming';
if (g.finished === 'TRUE') status = 'Finished';
else if (g.time_elapsed !== 'notstarted') status = 'Live';

return {
date: g.local_date,
home_team: getTeamName(g, 'home'),
away_team: getTeamName(g, 'away'),
score: score,
group: g.group,
stage: stageMap[g.type] || g.type,
status: status
};
});
51 changes: 51 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/next-match.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
var games = data.games || [];
var teamId = context.objects[0] ? String(context.objects[0].teamId) : '';

var stageMap = {
group: 'Group Stage',
r32: 'Round of 32',
r16: 'Round of 16',
qf: 'Quarter-Final',
sf: 'Semi-Final',
third: 'Third Place',
final: 'Final'
};

function parseDate(d) {
var parts = d.split(' ');
var dateParts = parts[0].split('/');
return new Date(dateParts[2] + '-' + dateParts[0] + '-' + dateParts[1] + 'T' + parts[1] + ':00');
}

var teamGames = games.filter(function(g) {
return g.home_team_id === teamId || g.away_team_id === teamId;
});

var upcoming = teamGames.filter(function(g) {
return g.finished !== 'TRUE' && g.time_elapsed === 'notstarted';
});

upcoming.sort(function(a, b) {
return parseDate(a.local_date) - parseDate(b.local_date);
});

var sourceId = context.objects[0] ? context.objects[0].sourceId : '';

if (upcoming.length === 0) {
result = [{ date: 'No upcoming matches', home_away: '', opponent: '', stage: '', group: '', sourceId: sourceId }];
} else {
var next = upcoming[0];
var isHome = next.home_team_id === teamId;
var opponent = isHome ? next.away_team_name_en : next.home_team_name_en;
if (!opponent || opponent === 'undefined') {
opponent = isHome ? (next.away_team_label || 'TBD') : (next.home_team_label || 'TBD');
}
result = [{
date: next.local_date,
home_away: isHome ? 'Home' : 'Away',
opponent: opponent,
stage: stageMap[next.type] || next.type,
group: next.type === 'group' ? 'Group ' + next.group : '',
sourceId: sourceId
}];
}
29 changes: 29 additions & 0 deletions plugins/WorldCup2026/v1/dataStreams/scripts/team-standing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
var games = data.games || [];
var teamId = context.objects[0] ? String([].concat(context.objects[0].teamId)[0] || '') : '';
var teamName = context.objects[0] ? [].concat(context.objects[0].teamName)[0] || '' : '';
var group = context.objects[0] ? [].concat(context.objects[0].group)[0] || '' : '';

var groupGames = games.filter(function(g) {
return g.type === 'group' &&
(g.home_team_id === teamId || g.away_team_id === teamId);
});

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

groupGames.forEach(function(g) {
if (g.finished !== 'TRUE') return;
mp++;
var isHome = g.home_team_id === teamId;
var myScore = parseInt(isHome ? g.home_score : g.away_score, 10) || 0;
var oppScore = parseInt(isHome ? g.away_score : g.home_score, 10) || 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: teamId }];
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": "/get/games",
"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": "/get/games",
"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"] } }]
}
]
}
Loading
Loading