Skip to content

Commit 0809b40

Browse files
[ADD] crm_mention_leads: Add module to generate leads from internet mentions
- Introduces a new module designed to generate leads. - Leads are sourced from internet mentions. - Utilizes an API to fetch relevant posts from the internet. - Includes functionality to score these fetched posts. - Generates new leads based on the scoring results.
1 parent 781b590 commit 0809b40

13 files changed

Lines changed: 647 additions & 0 deletions

crm_mention_leads/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from . import models
2+
from . import wizard

crm_mention_leads/__manifest__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
'name': 'Mentions to Leads',
3+
'category': 'Demo',
4+
'summary': 'Convert Reddit mentions into CRM leads automatically',
5+
'description': """
6+
Scans Reddit for posts matching your product/service keywords,
7+
scores them for buying intent, and creates leads in CRM automatically.
8+
""",
9+
'author': 'ASURK',
10+
'depends': ['crm', 'base_setup'],
11+
'data': [
12+
'security/ir.model.access.csv',
13+
'data/cron_job.xml',
14+
'views/res_config_settings_views.xml',
15+
'views/crm_mention_log_views.xml',
16+
'wizard/mention_setup_wizard_views.xml',
17+
],
18+
'installable': True,
19+
'application': False,
20+
'license': 'LGPL-3',
21+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<data noupdate="1">
4+
<record id="ir_cron_mention_leads_scan" model="ir.cron">
5+
<field name="name">Mentions to Leads: Scan Reddit</field>
6+
<field name="model_id" ref="model_crm_mention_job"/>
7+
<field name="state">code</field>
8+
<field name="code">model.run_mention_scan()</field>
9+
<field name="interval_number">1</field>
10+
<field name="interval_type">days</field>
11+
<field name="active">False</field>
12+
</record>
13+
</data>
14+
</odoo>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import res_config_settings
2+
from . import crm_mention_log
3+
from . import crm_mention_job
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
import json
2+
import logging
3+
import requests
4+
5+
from odoo import api, models
6+
7+
_logger = logging.getLogger(__name__)
8+
9+
10+
class CrmMentionJob(models.Model):
11+
_name = 'crm.mention.job'
12+
_description = 'Mention Lead Scanner'
13+
14+
# ------------------------------------------------------------------
15+
# ENTRY POINT — called by cron
16+
# ------------------------------------------------------------------
17+
18+
@api.model
19+
def run_mention_scan(self):
20+
params = self.env['ir.config_parameter'].sudo()
21+
breakpoint()
22+
if not params.get_param('crm_mention_leads.enabled'):
23+
_logger.info("Mentions to Leads: disabled, skipping scan.")
24+
return
25+
26+
# Load config
27+
product_desc = params.get_param('crm_mention_leads.product_desc', '')
28+
target_customer = params.get_param(
29+
'crm_mention_leads.target_customer', '')
30+
subreddits = params.get_param(
31+
'crm_mention_leads.subreddits', 'entrepreneur,smallbusiness')
32+
threshold = int(params.get_param(
33+
'crm_mention_leads.score_threshold', 60))
34+
35+
if not product_desc:
36+
_logger.warning(
37+
"Mentions to Leads: no product description configured. Run setup wizard.")
38+
return
39+
40+
# Step 1 — Generate intent queries via AI
41+
queries = self._generate_intent_queries(product_desc, target_customer)
42+
_logger.info("Mentions to Leads: generated %d queries", len(queries))
43+
44+
# Step 2 — Fetch Reddit posts via ScrapeCreators
45+
posts = self._fetch_reddit_posts(subreddits.split(','), queries)
46+
_logger.info("Mentions to Leads: fetched %d posts", len(posts))
47+
48+
# Step 3 — Score and create leads
49+
leads_created = 0
50+
for post in posts:
51+
# Skip already-processed posts
52+
if self.env['crm.mention.log'].post_already_processed(post['id']):
53+
continue
54+
55+
score, reason = self._score_post(
56+
post, product_desc, target_customer)
57+
58+
lead = None
59+
if score >= threshold:
60+
lead = self._create_lead(post, score)
61+
leads_created += 1
62+
63+
# Always log the post
64+
self._log_mention(post, score, reason, lead)
65+
66+
_logger.info(
67+
"Mentions to Leads: scan complete. %d leads created.", leads_created)
68+
69+
# ------------------------------------------------------------------
70+
# STEP 1 — Generate intent queries using Gemini
71+
# ------------------------------------------------------------------
72+
73+
def _gemini_generate(self, prompt, temperature=0.3):
74+
params = self.env['ir.config_parameter'].sudo()
75+
api_key = params.get_param('crm_mention_leads.gemini_api_key')
76+
77+
if not api_key:
78+
raise ValueError("Gemini API key not configured")
79+
80+
response = requests.post(
81+
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
82+
params={"key": api_key},
83+
headers={"Content-Type": "application/json"},
84+
json={
85+
"contents": [
86+
{
87+
"parts": [
88+
{
89+
"text": prompt
90+
}
91+
]
92+
}
93+
],
94+
"generationConfig": {
95+
"temperature": temperature,
96+
"responseMimeType": "application/json"
97+
}
98+
},
99+
timeout=30,
100+
)
101+
102+
response.raise_for_status()
103+
data = response.json()
104+
return data["candidates"][0]["content"]["parts"][0]["text"]
105+
106+
def _generate_intent_queries(self, product_desc, target_customer):
107+
params = self.env['ir.config_parameter'].sudo()
108+
api_key = params.get_param('crm_mention_leads.gemini_api_key')
109+
110+
if not api_key:
111+
return [product_desc[:50]]
112+
113+
prompt = f"""
114+
You are a B2B sales expert.
115+
116+
Generate 2 short Reddit search queries to find posts where people are
117+
looking for or complaining about problems that this product solves.
118+
119+
Product:
120+
{product_desc}
121+
122+
Target Customer:
123+
{target_customer}
124+
125+
Return ONLY a JSON array.
126+
127+
Example:
128+
[
129+
"looking for HR software",
130+
"payroll tool recommendation",
131+
"replacing BambooHR"
132+
]
133+
"""
134+
135+
try:
136+
content = self._gemini_generate(prompt, temperature=0.7)
137+
return json.loads(content)
138+
except Exception as e:
139+
_logger.error(
140+
"Mentions to Leads: query generation failed: %s", e)
141+
return [product_desc[:50]]
142+
143+
# ------------------------------------------------------------------
144+
# STEP 2 — Fetch Reddit posts via ScrapeCreators API
145+
# Docs: https://docs.scrapecreators.com/v1/reddit/subreddit/search
146+
# https://docs.scrapecreators.com/v1/reddit/search
147+
# ------------------------------------------------------------------
148+
149+
def _get_scrapecreators_headers(self):
150+
"""Return auth headers for ScrapeCreators API."""
151+
params = self.env['ir.config_parameter'].sudo()
152+
api_key = params.get_param('crm_mention_leads.scrapecreators_api_key')
153+
if not api_key:
154+
raise ValueError("ScrapeCreators API key not configured")
155+
return {"x-api-key": api_key}
156+
157+
def _parse_scrapecreators_posts(self, data, subreddit_name):
158+
posts = []
159+
160+
for item in data.get('posts', []):
161+
162+
subreddit = item.get('subreddit')
163+
164+
if isinstance(subreddit, dict):
165+
subreddit = subreddit.get('name')
166+
167+
posts.append({
168+
'id': str(item.get('id', '')),
169+
'title': item.get('title', ''),
170+
'body': item.get('selftext') or item.get('body') or '',
171+
'url': item.get('url', ''),
172+
'subreddit': subreddit or subreddit_name,
173+
'author': item.get('author') or item.get('author_name') or '',
174+
})
175+
176+
return posts
177+
178+
def _fetch_reddit_posts(self, subreddits, queries):
179+
"""
180+
For each subreddit + query pair, call ScrapeCreators
181+
/v1/reddit/subreddit/search and collect posts.
182+
183+
Falls back to global /v1/reddit/search when no subreddits are
184+
configured.
185+
"""
186+
posts = [{'id': '1u0hln0', 'title': 'I am looking to buy computers for my office workers', 'body': '', 'url': 'https://www.reddit.com/r/Entrepreneur/comments/1u0hln0/iot_business_has_anyone_here_built_one/', 'subreddit': 'Entrepreneur', 'author': 'Draviddavid'}
187+
]
188+
try:
189+
headers = self._get_scrapecreators_headers()
190+
191+
for subreddit in subreddits:
192+
subreddit = subreddit.strip()
193+
if not subreddit:
194+
continue
195+
196+
for query in queries:
197+
try:
198+
199+
_logger.info(
200+
"Fetching r/%s query='%s'",
201+
subreddit,
202+
query,
203+
)
204+
205+
response = requests.get(
206+
"https://api.scrapecreators.com/v1/reddit/subreddit/search",
207+
headers=headers,
208+
params={
209+
"subreddit": subreddit, # no "r/" prefix
210+
"query": query,
211+
"sort": "new",
212+
"timeframe": "week", # recent posts only
213+
},
214+
timeout=15,
215+
)
216+
217+
_logger.info(
218+
"Response %s for r/%s query='%s'",
219+
response.status_code,
220+
subreddit,
221+
query,
222+
)
223+
224+
if response.status_code != 200:
225+
_logger.warning(
226+
"Mentions to Leads: ScrapeCreators returned %d "
227+
"for r/%s query '%s'",
228+
response.status_code, subreddit, query,
229+
)
230+
continue
231+
232+
data = response.json()
233+
234+
_logger.info(
235+
"Received %d posts",
236+
len(data.get("posts", []))
237+
)
238+
239+
parsed = self._parse_scrapecreators_posts(
240+
data,
241+
subreddit,
242+
)
243+
posts.extend(parsed)
244+
245+
except Exception as e:
246+
_logger.error(
247+
"Mentions to Leads: fetch failed for r/%s '%s': %s",
248+
subreddit, query, e,
249+
)
250+
251+
except ValueError as e:
252+
# API key not configured
253+
_logger.error("Mentions to Leads: %s", e)
254+
255+
return posts
256+
257+
# ------------------------------------------------------------------
258+
# STEP 3 — Score post for buying intent
259+
# ------------------------------------------------------------------
260+
261+
def _score_post(self, post, product_desc, target_customer):
262+
params = self.env['ir.config_parameter'].sudo()
263+
api_key = params.get_param('crm_mention_leads.gemini_api_key')
264+
265+
if not api_key:
266+
return 50, "No Gemini key configured"
267+
268+
prompt = f"""
269+
You are a B2B sales qualification expert.
270+
271+
Our Product:
272+
{product_desc}
273+
274+
Our Target Customer:
275+
{target_customer}
276+
277+
Post Title:
278+
{post['title']}
279+
280+
Post Body:
281+
{post['body'][:500]}
282+
283+
Score from 0 to 100 where:
284+
285+
80-100 = Strong buying intent
286+
60-79 = Moderate intent
287+
40-59 = Weak intent
288+
0-39 = Not relevant
289+
290+
Return ONLY JSON.
291+
292+
Example:
293+
{{
294+
"score": 75,
295+
"reason": "User is actively comparing tools and mentions budget"
296+
}}
297+
"""
298+
299+
try:
300+
content = self._gemini_generate(prompt, temperature=0.2)
301+
breakpoint()
302+
result = json.loads(content)
303+
return (
304+
int(result.get("score", 0)),
305+
result.get("reason", "")
306+
)
307+
except Exception as e:
308+
_logger.error(
309+
"Mentions to Leads: scoring failed: %s", e)
310+
return 0, f"Scoring error: {e}"
311+
312+
# ------------------------------------------------------------------
313+
# STEP 4 — Create CRM lead
314+
# ------------------------------------------------------------------
315+
316+
def _create_lead(self, post, score):
317+
breakpoint()
318+
source = self.env['utm.source'].search(
319+
[('name', '=', 'Reddit')], limit=1)
320+
if not source:
321+
source = self.env['utm.source'].create({'name': 'Reddit'})
322+
323+
lead = self.env['crm.lead'].create({
324+
'name': f"Reddit Mention — {post['title'][:60]}",
325+
'description': (
326+
f"<b>Subreddit:</b> r/{post['subreddit']}<br/>"
327+
f"<b>Author:</b> u/{post['author']}<br/>"
328+
f"<b>Intent Score:</b> {score}/100<br/>"
329+
f"<b>URL:</b> <a href='{post['url']}'>{post['url']}</a><br/><br/>"
330+
f"{post['body'][:1000]}"
331+
),
332+
'source_id': source.id,
333+
'type': 'opportunity',
334+
})
335+
return lead
336+
337+
# ------------------------------------------------------------------
338+
# STEP 5 — Log the mention
339+
# ------------------------------------------------------------------
340+
341+
def _log_mention(self, post, score, reason, lead=None):
342+
self.env['crm.mention.log'].create({
343+
'post_title': post['title'],
344+
'post_url': post['url'],
345+
'post_body': post['body'][:2000],
346+
'subreddit': post['subreddit'],
347+
'reddit_author': post['author'],
348+
'post_reddit_id': post['id'],
349+
'intent_score': score,
350+
'score_reason': reason,
351+
'lead_created': bool(lead),
352+
'lead_id': lead.id if lead else False,
353+
})

0 commit comments

Comments
 (0)