Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/new-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ body:
- type: dropdown
id: category
attributes:
label: Categorie / Category
label: Catégorie / Category
options:
- AI / IA
- Cybersecurity / Cybersecurite
- Cybersecurity / Cybersécurité
- IoT
- Windows
- Mac / Apple
- Linux
- Tech Generale
- Tech Générale
- Entrepreneuriat / Entrepreneurship
- Bourse & Finance
- Crypto & Blockchain
Expand Down
8 changes: 4 additions & 4 deletions .github/ISSUE_TEMPLATE/subscribe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ body:
- type: checkboxes
id: categories
attributes:
label: Categories souhaitees / Desired categories
label: Catégories souhaitées / Desired categories
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistency: The workflow was updated to match "Tech Générale" with an accent (line 92), but the issue template at line 24 still has "Tech Generale" without the accent. This will cause the category mapping to fail for users who select this option.

Update line 24 to "Tech Générale" to match the workflow parsing logic.

Copilot uses AI. Check for mistakes.
options:
- label: AI / IA
- label: Cybersecurity
Expand All @@ -30,17 +30,17 @@ body:
- type: dropdown
id: language
attributes:
label: Langue preferee / Preferred language
label: Langue préférée / Preferred language
options:
- Francais
- Français
- English
- Les deux / Both
validations:
required: true
- type: dropdown
id: frequency
attributes:
label: Frequence / Frequency
label: Fréquence / Frequency
options:
- Chaque mise a jour (toutes les 3h) / Every update
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing accent: "mise a jour" should be "mise à jour" (with the accent on à) to properly follow French orthography. This is consistent with other accent fixes throughout this PR.

Suggested change
- Chaque mise a jour (toutes les 3h) / Every update
- Chaque mise à jour (toutes les 3h) / Every update

Copilot uses AI. Check for mistakes.
- Quotidien / Daily digest
Expand Down
57 changes: 49 additions & 8 deletions .github/workflows/add-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ jobs:
const issue = context.payload.issue;
const body = issue.body;

// Constants
const MAX_SOURCE_NAME_LENGTH = 100;
const MAX_TAG_LENGTH = 50;

// Parse form fields from issue body
function getField(label) {
const regex = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i');
Expand All @@ -33,24 +37,61 @@ jobs:
const sourceName = getField('Nom de la source / Source Name');
const sourceUrl = getField('URL du site / Website URL');
const rssUrl = getField('URL du flux RSS / RSS Feed URL');
const categoryRaw = getField('Categorie / Category');
const categoryRaw = getField('Catégorie / Category');
const languageRaw = getField('Langue / Language');
const tags = getField('Tags / Mots-cles');

// Validate required fields
if (!sourceName || !rssUrl) {
console.log('Missing required fields');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'Missing required fields (source name or RSS URL). Please provide all required information.'
});
return;
}

// Sanitize and validate source name (prevent injection)
const sanitizedSourceName = sourceName.replace(/[<>'"]/g, '').slice(0, MAX_SOURCE_NAME_LENGTH);
if (sanitizedSourceName !== sourceName) {
console.log('Source name contains invalid characters');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'Source name contains invalid characters. Please use only alphanumeric characters, spaces, and basic punctuation.'
});
return;
}

// Validate RSS URL format
try {
const urlObj = new URL(rssUrl);
if (!['http:', 'https:'].includes(urlObj.protocol)) {
throw new Error('Invalid protocol');
}
} catch (e) {
console.log('Invalid RSS URL:', rssUrl);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Invalid RSS URL format: "${rssUrl}". Please provide a valid HTTP or HTTPS URL.`
});
return;
}

// Map category
const catMap = {
'AI / IA': 'ai',
'Cybersecurity / Cybersecurite': 'cybersecurity',
'Cybersecurity / Cybersécurité': 'cybersecurity',
'IoT': 'iot',
'Windows': 'windows',
'Mac / Apple': 'mac',
'Linux': 'linux',
'Tech Generale': 'tech',
'Tech Générale': 'tech',
'Entrepreneuriat / Entrepreneurship': 'entrepreneurship',
'Bourse & Finance': 'finance',
'Crypto & Blockchain': 'crypto',
Expand All @@ -60,10 +101,10 @@ jobs:
const category = catMap[categoryRaw] || 'tech';

// Map language
const lang = languageRaw.includes('Francais') ? 'fr' : 'en';
const lang = languageRaw.includes('Français') ? 'fr' : 'en';

// Parse tags
const tagList = tags ? tags.split(',').map(t => t.trim()).filter(t => t) : [category];
// Parse and sanitize tags
const tagList = tags ? tags.split(',').map(t => t.trim().replace(/[<>'"]/g, '').slice(0, MAX_TAG_LENGTH)).filter(t => t) : [category];

// Read and update config.json
const config = JSON.parse(fs.readFileSync('config.json', 'utf-8'));
Expand All @@ -85,9 +126,9 @@ jobs:
return;
}

// Add new feed
// Add new feed with sanitized data
config.categories[category].feeds.push({
name: sourceName,
name: sanitizedSourceName,
url: rssUrl,
tags: tagList,
lang: lang
Expand Down
52 changes: 44 additions & 8 deletions .github/workflows/manage-subscriber.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
const issue = context.payload.issue;
const body = issue.body;

// Constants
const MAX_AT_SYMBOLS_THRESHOLD = 5; // Maximum @ symbols allowed to prevent email harvesting

function getField(label) {
const regex = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i');
const match = body.match(regex);
Expand All @@ -36,15 +39,48 @@ jobs:
return [...match[1].matchAll(/- \[X\] (.+)/gi)].map(m => m[1].trim());
}

const email = getField('Votre email / Your email');
if (!email || !email.includes('@')) {
console.log('Invalid email');
// Validate email address format
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return (
typeof email === 'string' &&
email.length > 0 &&
email.length <= 254 &&
emailRegex.test(email)
);
}

// Basic abuse heuristic: ignore issues that appear to contain many email-like strings
const atCount = (body.match(/@/g) || []).length;
if (atCount > MAX_AT_SYMBOLS_THRESHOLD) {
console.log('Suspicious issue body (too many @ symbols); skipping subscription.');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'This subscription request appears suspicious and has been rejected. If this is an error, please contact the maintainers.'
});
return;
}

const rawEmail = getField('Votre email / Your email');
// In case multiple emails or extra text are provided, only keep the first token
const email = rawEmail.split(/[,\s]+/).filter(Boolean)[0] || '';

if (!isValidEmail(email)) {
console.log('Invalid email:', rawEmail);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Invalid email address format: "${rawEmail}". Please provide a valid email address.`
});
return;
}

const selectedCats = getCheckboxes('Categories souhaitees / Desired categories');
const langRaw = getField('Langue preferee / Preferred language');
const freqRaw = getField('Frequence / Frequency');
const selectedCats = getCheckboxes('Catégories souhaitées / Desired categories');
const langRaw = getField('Langue préférée / Preferred language');
const freqRaw = getField('Fréquence / Frequency');

const catMap = {
'AI / IA': 'ai',
Expand All @@ -53,7 +89,7 @@ jobs:
'Windows': 'windows',
'Mac / Apple': 'mac',
'Linux': 'linux',
'Tech Generale': 'tech',
'Tech Générale': 'tech',
'Entrepreneuriat': 'entrepreneurship',
'Bourse & Finance': 'finance',
'Crypto & Blockchain': 'crypto',
Expand All @@ -62,7 +98,7 @@ jobs:
};

const categories = selectedCats.map(c => catMap[c]).filter(Boolean);
const lang = langRaw.includes('Francais') ? 'fr' : langRaw.includes('English') ? 'en' : 'all';
const lang = langRaw.includes('Français') ? 'fr' : langRaw.includes('English') ? 'en' : 'all';
const frequency = freqRaw.includes('Quotidien') ? 'daily' : 'each_update';

// Read subscribers
Expand Down
28 changes: 14 additions & 14 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
]
},
"cybersecurity": {
"labels": { "en": "Cybersecurity", "fr": "Cybersecurite" },
"labels": { "en": "Cybersecurity", "fr": "Cybersécurité" },
"icon": "shield",
"priority": 2,
"feeds": [
Expand Down Expand Up @@ -117,7 +117,7 @@
]
},
"tech": {
"labels": { "en": "General Tech", "fr": "Tech Generale" },
"labels": { "en": "General Tech", "fr": "Tech Générale" },
"icon": "zap",
"priority": 7,
"feeds": [
Expand Down Expand Up @@ -230,33 +230,33 @@
"artificial_intelligence": { "en": "Artificial Intelligence", "fr": "Intelligence Artificielle" },
"machine_learning": { "en": "Machine Learning", "fr": "Apprentissage Automatique" },
"deep_learning": { "en": "Deep Learning", "fr": "Apprentissage Profond" },
"cybersecurity": { "en": "Cybersecurity", "fr": "Cybersecurite" },
"data_breach": { "en": "Data Breach", "fr": "Fuite de Donnees" },
"privacy": { "en": "Privacy", "fr": "Vie Privee" },
"cybersecurity": { "en": "Cybersecurity", "fr": "Cybersécurité" },
"data_breach": { "en": "Data Breach", "fr": "Fuite de Données" },
"privacy": { "en": "Privacy", "fr": "Vie Privée" },
"home_automation": { "en": "Home Automation", "fr": "Domotique" },
"connected_objects": { "en": "Connected Objects", "fr": "Objets Connectes" },
"connected_objects": { "en": "Connected Objects", "fr": "Objets Connectés" },
"open_source": { "en": "Open Source", "fr": "Logiciel Libre" },
"update": { "en": "Update", "fr": "Mise a jour" },
"vulnerability": { "en": "Vulnerability", "fr": "Vulnerabilite" },
"neural_network": { "en": "Neural Network", "fr": "Reseau de Neurones" },
"update": { "en": "Update", "fr": "Mise à jour" },
"vulnerability": { "en": "Vulnerability", "fr": "Vulnérabilité" },
"neural_network": { "en": "Neural Network", "fr": "Réseau de Neurones" },
"computer_vision": { "en": "Computer Vision", "fr": "Vision par Ordinateur" },
"robotics": { "en": "Robotics", "fr": "Robotique" },
"quantum_computing": { "en": "Quantum Computing", "fr": "Informatique Quantique" },
"stock_market": { "en": "Stock Market", "fr": "Bourse" },
"cryptocurrency": { "en": "Cryptocurrency", "fr": "Cryptomonnaie" },
"fundraising": { "en": "Fundraising", "fr": "Levee de Fonds" },
"fundraising": { "en": "Fundraising", "fr": "Levée de Fonds" },
"entrepreneurship": { "en": "Entrepreneurship", "fr": "Entrepreneuriat" },
"startup": { "en": "Startup", "fr": "Startup" },
"investment": { "en": "Investment", "fr": "Investissement" },
"bug_bounty": { "en": "Bug Bounty", "fr": "Bug Bounty" },
"exploit": { "en": "Exploit", "fr": "Exploit" },
"regulation": { "en": "Regulation", "fr": "Regulation" },
"regulation": { "en": "Regulation", "fr": "Réglementation" },
"new_product": { "en": "New Product", "fr": "Nouveau Produit" },
"launch": { "en": "Launch", "fr": "Lancement" },
"crowdfunding": { "en": "Crowdfunding", "fr": "Financement Participatif" },
"repository": { "en": "Repository", "fr": "Depot" },
"self_hosted": { "en": "Self-hosted", "fr": "Auto-heberge" },
"library": { "en": "Library", "fr": "Bibliotheque" },
"repository": { "en": "Repository", "fr": "Dépôt" },
"self_hosted": { "en": "Self-hosted", "fr": "Auto-hébergé" },
"library": { "en": "Library", "fr": "Bibliothèque" },
"tool": { "en": "Tool", "fr": "Outil" }
}
}
Expand Down
Loading