Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 0 additions & 1 deletion cli/src/commands/checkDns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ export async function checkDns(
fix?: boolean
} = {},
) {

let activities: ActivityMetadataAndFolder[] = []

if (all) {
Expand Down
67 changes: 67 additions & 0 deletions websites/D/Dealabs/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"$schema": "https://schemas.premid.app/metadata/1.16",
"apiVersion": 1,
"author": {
"id": "188370726279970816",
"name": "linkredible"
},
"service": "Dealabs",
"description": {
"en": "The largest community for sharing deals and coupons in France.",
"fr": "La plus grande communauté de partage de bons plans et codes promo en France."
},
"url": "dealabs.com",
"regExp": "^https?://(www\\.)?dealabs\\.com/.*",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"regExp": "^https?://(www\\.)?dealabs\\.com/.*",
"regExp": "^https?[:][/][/](www[.])?dealabs[.]com[/]",

"version": "1.0.0",
"logo": "https://www.dealabs.com/assets/img/appicon_cbea2.png",
"thumbnail": "https://www.dealabs.com/assets/img/appicon_cbea2.png",
"color": "#36bca1",
"category": "socials",
"tags": [
"deals",
"shopping",
"community",
"france"
],
"settings": [
{
"id": "lang",
"multiLanguage": true
},
{
"id": "privacyMode",
"title": "Mode Confidentiel (Global)",
"icon": "fas fa-user-secret",
"placeholder": "Masquer tous les détails (titres, prix, images)",
"value": false
},
{
"id": "hideDealTitles",
"title": "Masquer titres (Bons plans)",
"icon": "fas fa-shopping-bag",
"placeholder": "Remplace le titre du produit par un texte générique",
"value": false
},
{
"id": "hideDiscussionTitles",
"title": "Masquer titres (Forum)",
"icon": "fas fa-comments",
"placeholder": "Remplace le sujet de la discussion par un texte générique",
"value": false
},
{
"id": "hideImages",
"title": "Images génériques uniquement",
"icon": "fas fa-image",
"placeholder": "Affiche toujours le logo Dealabs au lieu des produits",
"value": false
},
{
"id": "hidePrices",
"title": "Masquer les prix",
"icon": "fas fa-tag",
"placeholder": "N'affiche pas le prix du deal dans le statut",
"value": false
}
]
}
153 changes: 153 additions & 0 deletions websites/D/Dealabs/presence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import type { DealabsSettings, Resolver } from './util/interfaces.js'
import dealResolver from './sources/deal.js'

import discussionResolver from './sources/discussion.js'
import groupResolver from './sources/group.js'
import listingResolver from './sources/listing.js'
import merchantResolver from './sources/merchant.js'
import { activityAssets, presence } from './util/index.js'

const browsingTimestamp = Math.floor(Date.now() / 1000)

async function getStrings() {
const fr = {
navigating: 'Navigue sur Dealabs',
hunting: 'Chasse les bons plans',
viewDeal: 'Regarde un bon plan',
onDealabs: 'Sur Dealabs',
onForum: 'Sur le forum',
readDiscuss: 'Lit une discussion',
searchCode: 'Cherche un code promo',
viewCodes: 'Voir les codes',
browseDiscuss: 'Parcourt les discussions',
detail: 'Détail',
free: 'GRATUIT',
hiddenDeal: 'Un bon plan',
hiddenTopic: 'Une discussion',
viewPage: 'Voir la page',
exploreCat: 'Explore une catégorie',
homepage: 'Sur la page d\'accueil',
forYou: 'Pour vous',
searchDeal: 'Cherche un bon plan',
newDeals: 'Nouveaux deals 🕒',
hotDeals: 'Hot & Tendance 🔥',
topDeals: 'Les plus Hot 🌶️',
searching: 'Recherche',
searchingEllipsis: 'Recherche...',
}

const en = {
navigating: 'Browsing Dealabs',
hunting: 'Hunting for deals',
viewDeal: 'Viewing a deal',
onDealabs: 'On Dealabs',
onForum: 'On the Forum',
readDiscuss: 'Reading a discussion',
searchCode: 'Looking for a promo code',
viewCodes: 'View codes',
browseDiscuss: 'Browsing discussions',
detail: 'Detail',
free: 'FREE',
hiddenDeal: 'A deal',
hiddenTopic: 'A discussion',
viewPage: 'View page',
exploreCat: 'Browsing a category',
homepage: 'On the homepage',
forYou: 'For You',
searchDeal: 'Searching for a deal',
newDeals: 'New Deals 🕒',
hotDeals: 'Hot & Trending 🔥',
topDeals: 'Hottest Deals 🌶️',
searching: 'Searching',
searchingEllipsis: 'Searching...',
}

let lang = await presence.getSetting<string>('lang').catch(() => null)

if (!lang) {
lang = navigator.language.startsWith('fr') ? 'fr' : 'en'
}

return lang === 'fr' ? fr : en
}

presence.on('UpdateData', async () => {
const settings: DealabsSettings = {
privacyMode: await presence.getSetting('privacyMode'),
hideDealTitles: await presence.getSetting('hideDealTitles'),
hideDiscussionTitles: await presence.getSetting('hideDiscussionTitles'),
hideImages: await presence.getSetting('hideImages'),
hidePrices: await presence.getSetting('hidePrices'),
Comment on lines +21 to +25
Copy link
Member

Choose a reason for hiding this comment

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

maybe use a Promise.all?

}

const strings = await getStrings()
const t: any = strings
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

Using 'any' type for the translation parameter defeats the purpose of TypeScript's type safety. The Translation interface is already defined in interfaces.ts and should be used instead to provide proper type checking and autocompletion.

Copilot uses AI. Check for mistakes.
const presenceData: PresenceData = {
largeImageKey: activityAssets.logo,
startTimestamp: browsingTimestamp,
type: 3, // Watching
details: strings?.navigating,
state: strings?.hunting,
}

const pathname = document.location.pathname

if (settings.privacyMode) {
if (pathname.includes('/bons-plans/') || pathname.includes('/codes-promo/')) {
presenceData.details = strings?.viewDeal
presenceData.state = strings?.onDealabs
}
else if (pathname.includes('/discussions/') || pathname.includes('/groupe/')) {
presenceData.details = strings?.onForum
presenceData.state = strings?.readDiscuss
}
else {
presenceData.details = strings?.navigating
presenceData.state = strings?.hunting
}
presence.setActivity(presenceData)
return
}

const resolvers: Resolver[] = [
merchantResolver,
dealResolver,
discussionResolver,
groupResolver,
listingResolver,
]

const activeResolver = resolvers.filter(r => r.isActive(pathname))[0]

if (activeResolver) {
const state = activeResolver.getState(t, settings)
const details = activeResolver.getDetails(t, settings)

if (state)
presenceData.state = state
if (details)
presenceData.details = details

if (!settings.hideImages && activeResolver.getLargeImage) {
const img = activeResolver.getLargeImage()
if (img)
presenceData.largeImageKey = img
}

if (activeResolver.getButtons) {
presenceData.buttons = activeResolver.getButtons(t)
}

presenceData.smallImageKey = activityAssets.logo
presenceData.smallImageText = 'Dealabs'
}

if (!presenceData.state || (typeof presenceData.state === 'string' && presenceData.state.length < 2)) {
presenceData.state = strings?.hunting
}
if (!presenceData.details || (typeof presenceData.details === 'string' && presenceData.details.length < 2)) {
presenceData.details = strings?.onDealabs
}

presence.setActivity(presenceData)
})
71 changes: 71 additions & 0 deletions websites/D/Dealabs/sources/deal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { ButtonArray, DealabsSettings, Resolver } from '../util/interfaces.js'
import { getMainContent } from '../util/index.js'

const dealResolver: Resolver = {
isActive: (pathname: string) => pathname.includes('/bons-plans/'),

getState: (t: any, settings: DealabsSettings) => {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

Using 'any' type for the translation parameter defeats the purpose of TypeScript's type safety. The Translation interface is already defined in interfaces.ts and should be used instead.

Copilot uses AI. Check for mistakes.
if (settings.hideDealTitles)
return t.hiddenDeal

let title = document.querySelector('h1')?.textContent?.trim()
if (!title) {
const metaTitle = document.querySelector('meta[property="og:title"]')?.getAttribute('content')
title = metaTitle ? ((metaTitle.split(' - Dealabs')[0] ?? metaTitle).trim()) : t.detail
}
return title
},

getDetails: (t: any, settings: DealabsSettings) => {
const mainContent = getMainContent()

let price: string | undefined
if (!settings.hidePrices) {
const priceEl = mainContent.querySelector('.thread-price')
if (priceEl && priceEl.textContent) {
price = priceEl.textContent.trim()
}
else {
const freeLabel = mainContent.querySelector('.text--color-free')
if (freeLabel || (mainContent.textContent && mainContent.textContent.includes('Gratuit') && !mainContent.textContent.includes('€'))) {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The free detection logic hardcodes the French word "Gratuit" to detect free items. This will not work correctly if the website displays content in other languages or if the text changes. Consider using a more robust detection method, such as checking for specific CSS classes or data attributes that indicate a free item, rather than relying on hardcoded text matching.

Suggested change
if (freeLabel || (mainContent.textContent && mainContent.textContent.includes('Gratuit') && !mainContent.textContent.includes('€'))) {
if (freeLabel) {

Copilot uses AI. Check for mistakes.
price = t.free
}
}
}

let temp: string | undefined
const tempEl = mainContent.querySelector('.vote-temp')
if (tempEl && tempEl.textContent) {
temp = tempEl.textContent.trim()
}

const parts: string[] = []
if (temp) {
let emoji = '❄️'
const val = Number.parseInt(temp)
if (!isNaN(val)) {
if (val > 100)
emoji = '🔥'
else if (val > 0)
emoji = '🌡️'
}
parts.push(`${temp} ${emoji}`)
}
if (price)
parts.push(price)

return parts.length > 0 ? parts.join(' • ') : t.viewDeal
},

getLargeImage: () => {
const image = document.querySelector('meta[property="og:image"]')?.getAttribute('content')
if (image && (image.includes('logo-dark') || image.includes('logo-white') || image.includes('assets/img'))) {
return undefined
}
return image || undefined
},

getButtons: (t: any) => [{ label: t.viewPage, url: document.location.href }] as ButtonArray,
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

Using 'any' type for the translation parameter defeats the purpose of TypeScript's type safety. The Translation interface is already defined in interfaces.ts and should be used instead.

Copilot uses AI. Check for mistakes.
}

export default dealResolver
36 changes: 36 additions & 0 deletions websites/D/Dealabs/sources/discussion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { ButtonArray, DealabsSettings, Resolver } from '../util/interfaces.js'

const discussionResolver: Resolver = {
isActive: (pathname: string) => {
return pathname === '/discussions' || pathname.includes('/discussions/')
},

getState: (t: any, settings: DealabsSettings) => {
if (document.location.pathname === '/discussions')
return t.browseDiscuss

if (settings.hideDiscussionTitles)
return t.hiddenTopic

const h1 = document.querySelector('h1')?.textContent?.trim()
if (h1)
return h1

const metaTitle = document.querySelector('meta[property="og:title"]')?.getAttribute('content')
return metaTitle ? ((metaTitle.split(' - Dealabs')[0] ?? metaTitle).trim()) : t.detail
},

getDetails: (t: any) => {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

Using 'any' type for the translation parameter defeats the purpose of TypeScript's type safety. The Translation interface is already defined in interfaces.ts and should be used instead.

Copilot uses AI. Check for mistakes.
if (document.location.pathname === '/discussions')
return t.onForum
return t.readDiscuss
},

getButtons: (t: any) => {
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

Using 'any' type for the translation parameter defeats the purpose of TypeScript's type safety. The Translation interface is already defined in interfaces.ts and should be used instead.

Copilot uses AI. Check for mistakes.
if (document.location.pathname === '/discussions')
return undefined
return [{ label: t.viewPage, url: document.location.href }] as ButtonArray
},
}

export default discussionResolver
25 changes: 25 additions & 0 deletions websites/D/Dealabs/sources/group.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { Resolver } from '../util/interfaces.js'
import { formatSlug } from '../util/index.js'

const groupResolver: Resolver = {
isActive: (pathname: string) => pathname.includes('/groupe'),

getState: (t: any) => {
const docTitle = document.title
const titleMatch = docTitle.match(/Bon plan (.+?) [⇒|:\-]/)
if (titleMatch && titleMatch[1])
return titleMatch[1].trim()

const h1 = document.querySelector('h1')?.textContent?.trim()
if (h1)
return h1

const parts = document.location.pathname.split('/groupe/')
const slug = (parts[1] ?? '').split('/')[0] || 'Inconnue'
Copy link

Copilot AI Jan 6, 2026

Choose a reason for hiding this comment

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

The fallback value "Inconnue" (French for "Unknown", feminine form) is hardcoded. Since this presence claims multilanguage support, this should use a translatable string instead. Consider adding an "unknown" translation key to the Dealabs.json file and using it here.

Copilot uses AI. Check for mistakes.
return formatSlug(slug)
},

getDetails: (t: any) => t.exploreCat,
}

export default groupResolver
Loading
Loading