Feat: Add Social Lead Catcher webhook and CRM integration

This commit is contained in:
cerenX9
2026-07-30 19:44:05 +03:00
parent 075b79242d
commit 1626657868
15 changed files with 555 additions and 134 deletions

View File

@@ -42,10 +42,12 @@ Features:
'views/social_stream_post_kanban_views.xml', 'views/social_stream_post_kanban_views.xml',
'views/social_video_script_views.xml', 'views/social_video_script_views.xml',
'views/social_dashboard_views.xml', 'views/social_dashboard_views.xml',
'views/social_lead_views.xml',
'views/social_menu.xml', 'views/social_menu.xml',
'views/social_account_views.xml', 'views/social_account_views.xml',
'views/social_competitor_views.xml', 'views/social_competitor_views.xml',
'views/social_trend_views.xml', 'views/social_trend_views.xml',
'views/utm_campaign_ai_views.xml',
], ],
'assets': { 'assets': {
'web.assets_backend': [ 'web.assets_backend': [

View File

@@ -18,7 +18,7 @@ class LinkedInAPI:
'client_id': self.client_id, 'client_id': self.client_id,
'redirect_uri': self.redirect_uri, 'redirect_uri': self.redirect_uri,
'state': state, 'state': state,
'scope': 'w_member_social r_liteprofile w_organization_social r_organization_social' 'scope': 'openid profile email w_member_social'
} }
return f"{self.AUTH_URL}?{urlencode(params)}" return f"{self.AUTH_URL}?{urlencode(params)}"
@@ -35,6 +35,14 @@ class LinkedInAPI:
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
def get_profile_info(self, access_token):
"""Fetch user profile to get the person URN (sub)."""
url = "https://api.linkedin.com/v2/userinfo"
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
return response.json()
def upload_media(self, person_id, access_token, media_data, is_video=False): def upload_media(self, person_id, access_token, media_data, is_video=False):
"""Register and upload media to LinkedIn.""" """Register and upload media to LinkedIn."""
# 1. Register Upload # 1. Register Upload
@@ -91,12 +99,10 @@ class LinkedInAPI:
if media_urn: if media_urn:
share_content["shareMediaCategory"] = "VIDEO" if is_video else "IMAGE" share_content["shareMediaCategory"] = "VIDEO" if is_video else "IMAGE"
share_content["media"] = [ if isinstance(media_urn, list):
{ share_content["media"] = [{"status": "READY", "media": urn} for urn in media_urn[:9]]
"status": "READY", else:
"media": media_urn share_content["media"] = [{"status": "READY", "media": media_urn}]
}
]
payload = { payload = {
"author": f"urn:li:person:{person_id}", "author": f"urn:li:person:{person_id}",

View File

@@ -66,6 +66,21 @@ class MetaAPI:
raise Exception("Facebook sayfalarınızdan hiçbirine bağlı bir Instagram İşletme (Business/Creator) hesabı bulunamadı. Lütfen Instagram hesabınızı Facebook sayfanıza bağlayın ve hesabın İşletme türünde olduğundan emin olun.") raise Exception("Facebook sayfalarınızdan hiçbirine bağlı bir Instagram İşletme (Business/Creator) hesabı bulunamadı. Lütfen Instagram hesabınızı Facebook sayfanıza bağlayın ve hesabın İşletme türünde olduğundan emin olun.")
def get_business_discovery(self, ig_account_id, access_token, target_username):
"""
Belirtilen ig_account_id üzerinden, target_username (rakip) hesabının halka açık metriklerini çeker.
Gereksinim: ig_account_id bir Business/Creator hesabı olmalı, target_username de Business/Creator hesabı olmalıdır.
"""
url = f"{self.GRAPH_URL}/{ig_account_id}"
fields = f"business_discovery.username({target_username}){{followers_count,media_count,media{{like_count,comments_count,caption}}}}"
params = {
'fields': fields,
'access_token': access_token
}
response = requests.get(url, params=params, timeout=15)
response.raise_for_status()
return response.json()
def publish_post(self, page_id, access_token, message, link=None): def publish_post(self, page_id, access_token, message, link=None):
"""Belirtilen sayfada gönderi paylaşır.""" """Belirtilen sayfada gönderi paylaşır."""
url = f"{self.GRAPH_URL}/{page_id}/feed" url = f"{self.GRAPH_URL}/{page_id}/feed"
@@ -92,15 +107,33 @@ class MetaAPI:
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
def publish_photo(self, page_id, access_token, message, image_data): def publish_photo(self, page_id, access_token, message, image_data, published=True):
"""Facebook sayfasına görsel paylaşır.""" """Facebook sayfasına görsel paylaşır veya gizli yükler."""
url = f"{self.GRAPH_URL}/{page_id}/photos" url = f"{self.GRAPH_URL}/{page_id}/photos"
payload = {'message': message, 'access_token': access_token} payload = {'message': message, 'access_token': access_token, 'published': str(published).lower()}
files = {'source': ('image.jpg', image_data, 'image/jpeg')} files = {'source': ('image.jpg', image_data, 'image/jpeg')}
response = requests.post(url, data=payload, files=files, timeout=30) response = requests.post(url, data=payload, files=files, timeout=30)
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
def publish_carousel(self, page_id, access_token, message, image_data_list):
"""Facebook sayfasına çoklu görsel (carousel/albüm) paylaşır."""
media_ids = []
for img_data in image_data_list:
resp = self.publish_photo(page_id, access_token, "", img_data, published=False)
media_ids.append({"media_fbid": resp['id']})
import json
url = f"{self.GRAPH_URL}/{page_id}/feed"
payload = {
'message': message,
'attached_media': json.dumps(media_ids),
'access_token': access_token
}
response = requests.post(url, data=payload, timeout=30)
response.raise_for_status()
return response.json()
def publish_video(self, page_id, access_token, title, description, video_data): def publish_video(self, page_id, access_token, title, description, video_data):
"""Facebook sayfasına video yükler.""" """Facebook sayfasına video yükler."""
# Note: Video upload endpoint is graph-video.facebook.com # Note: Video upload endpoint is graph-video.facebook.com
@@ -139,6 +172,48 @@ class MetaAPI:
'creation_id': container_id, 'creation_id': container_id,
'access_token': access_token 'access_token': access_token
} }
pub_resp = requests.post(publish_url, data=publish_payload, timeout=20) publish_resp = requests.post(publish_url, data=publish_payload, timeout=20)
pub_resp.raise_for_status() publish_resp.raise_for_status()
return pub_resp.json() return publish_resp.json()
def publish_instagram_carousel(self, ig_account_id, access_token, media_url_list, caption):
"""Instagram hesabında çoklu resim (Carousel) paylaşır. Max 10 URL."""
import time
# 1. Her bir görsel için Item Container oluştur
children_ids = []
for url in media_url_list[:10]: # Max 10 images
container_url = f"{self.GRAPH_URL}/{ig_account_id}/media"
payload = {
'image_url': url,
'is_carousel_item': 'true',
'access_token': access_token
}
resp = requests.post(container_url, data=payload, timeout=20)
resp.raise_for_status()
children_ids.append(resp.json().get('id'))
time.sleep(3) # Container işleme süresi
# 2. Carousel Container oluştur
carousel_url = f"{self.GRAPH_URL}/{ig_account_id}/media"
carousel_payload = {
'media_type': 'CAROUSEL',
'children': ','.join(children_ids),
'caption': caption,
'access_token': access_token
}
carousel_resp = requests.post(carousel_url, data=carousel_payload, timeout=30)
carousel_resp.raise_for_status()
carousel_id = carousel_resp.json().get('id')
time.sleep(3)
# 3. Publish
publish_url = f"{self.GRAPH_URL}/{ig_account_id}/media_publish"
publish_payload = {
'creation_id': carousel_id,
'access_token': access_token
}
publish_resp = requests.post(publish_url, data=publish_payload, timeout=20)
publish_resp.raise_for_status()
return publish_resp.json()

View File

@@ -36,6 +36,18 @@ class YouTubeAPI:
response.raise_for_status() response.raise_for_status()
return response.json() return response.json()
def refresh_token(self, refresh_token):
"""Exchange a refresh token for a new access token."""
data = {
'client_id': self.client_id,
'client_secret': self.client_secret,
'refresh_token': refresh_token,
'grant_type': 'refresh_token'
}
response = requests.post(self.TOKEN_URL, data=data, timeout=10)
response.raise_for_status()
return response.json()
def upload_video(self, access_token, title, description, video_data, category_id="22", privacy_status="public"): def upload_video(self, access_token, title, description, video_data, category_id="22", privacy_status="public"):
"""Upload video to YouTube via Data API v3 multipart upload.""" """Upload video to YouTube via Data API v3 multipart upload."""
import json import json

View File

@@ -1,2 +1,3 @@
from . import main from . import main
from . import webhook_controller from . import webhook_controller
from . import lead_webhook

View File

@@ -0,0 +1,72 @@
from odoo import http
from odoo.http import request
import json
import logging
_logger = logging.getLogger(__name__)
class SocialLeadWebhookController(http.Controller):
@http.route('/api/social/webhook/lead', type='http', auth='public', methods=['POST'], csrf=False)
def receive_lead(self, **kw):
"""
Gelen Sosyal Medya Müşteri Fırsatı (Lead) Webhook'u.
Örnek Payload (JSON):
{
"name": "Ali Veli",
"email": "ali@example.com",
"phone": "+905554443322",
"campaign_name": "Anti-Haas Hedefli Kampanya",
"source": "Instagram Lead Ads",
"notes": "Makine fiyatı istiyor."
}
"""
try:
data = json.loads(request.httprequest.data)
_logger.info(f"Yeni Sosyal Fırsat (Lead) Alındı: {data}")
name = data.get('name', 'Bilinmeyen Müşteri')
email = data.get('email')
phone = data.get('phone')
campaign_name = data.get('campaign_name')
source = data.get('source', 'Sosyal Medya Formu')
notes = data.get('notes', '')
# Kampanya eşleştirme
campaign_id = False
if campaign_name:
campaign = request.env['utm.campaign'].sudo().search([('name', 'ilike', campaign_name)], limit=1)
if campaign:
campaign_id = campaign.id
else:
campaign = request.env['utm.campaign'].sudo().create({'name': campaign_name})
campaign_id = campaign.id
# Lead oluştur
lead_vals = {
'name': f"{source} - {name} İlgisi",
'contact_name': name,
'email_from': email,
'phone': phone,
'description': notes,
'type': 'lead',
}
if campaign_id:
lead_vals['campaign_id'] = campaign_id
# Try to map medium to 'Social' or 'Sosyal Medya' if exists
medium = request.env['utm.medium'].sudo().search([('name', 'ilike', 'Social')], limit=1)
if not medium:
medium = request.env['utm.medium'].sudo().create({'name': 'Sosyal Medya'})
lead_vals['medium_id'] = medium.id
lead = request.env['crm.lead'].sudo().create(lead_vals)
_logger.info(f"Odoo CRM Fırsatı başarıyla oluşturuldu: ID {lead.id}")
return request.make_response(json.dumps({'status': 'success', 'lead_id': lead.id}), headers=[('Content-Type', 'application/json')])
except Exception as e:
_logger.error(f"Lead Webhook işlenirken hata oluştu: {str(e)}")
return request.make_response(json.dumps({'error': str(e)}), status=500, headers=[('Content-Type', 'application/json')])

View File

@@ -10,6 +10,7 @@ from . import social_media
from . import social_auto_reply from . import social_auto_reply
from . import social_auto_reply_log from . import social_auto_reply_log
from . import social_trend from . import social_trend
from . import utm_campaign_ai
from . import social_competitor_history from . import social_competitor_history
from . import social_competitor_follower from . import social_competitor_follower
from . import social_competitor_ad from . import social_competitor_ad

View File

@@ -116,6 +116,13 @@ class SocialAccount(models.Model):
except Exception as e: except Exception as e:
# We raise the exception so the user can see exactly why Facebook is failing. # We raise the exception so the user can see exactly why Facebook is failing.
raise UserError(f"Instagram ID alınamadı: {str(e)}") raise UserError(f"Instagram ID alınamadı: {str(e)}")
elif self.platform == 'linkedin':
try:
profile = api_client.get_profile_info(self.access_token)
if profile and 'sub' in profile:
self.platform_account_id = profile.get('sub')
except Exception as e:
raise UserError(f"LinkedIn ID alınamadı: {str(e)}")
elif self.platform == 'facebook': elif self.platform == 'facebook':
# For facebook, get the first page ID and its Page Access Token # For facebook, get the first page ID and its Page Access Token
try: try:
@@ -140,16 +147,27 @@ class SocialAccount(models.Model):
'access_token': new_token, 'access_token': new_token,
'is_active': True, 'is_active': True,
}) })
# Odoo'nun standart success mesajı gösterilebilir
except Exception as e: except Exception as e:
raise UserError(f"Token yenilenirken hata oluştu: {str(e)}") raise UserError(f"Token yenilenirken hata oluştu: {str(e)}")
elif self.platform == 'youtube' and self.refresh_token:
try:
api_client = self._get_api_client()
resp = api_client.refresh_token(self.refresh_token)
new_token = resp.get('access_token')
if new_token:
self.write({
'access_token': new_token,
'is_active': True,
})
except Exception as e:
raise UserError(f"YouTube token yenilenirken hata oluştu: {str(e)}")
else: else:
raise UserError("Bu platform için otomatik token yenileme desteklenmiyor.") raise UserError("Bu platform için otomatik token yenileme desteklenmiyor veya Refresh Token eksik.")
@api.model @api.model
def _cron_refresh_tokens(self): def _cron_refresh_tokens(self):
# 60 gün dolmadan yenilenmesi gerekenleri bul (şimdilik tüm aktif facebook hesaplarını deneyelim) # Otomatik yenilenmesi gerekenleri bul
accounts = self.search([('is_active', '=', True), ('platform', '=', 'facebook')]) accounts = self.search([('is_active', '=', True), ('platform', 'in', ['facebook', 'youtube'])])
for acc in accounts: for acc in accounts:
try: try:
acc.action_refresh_token() acc.action_refresh_token()

View File

@@ -64,17 +64,143 @@ class SocialCompetitor(models.Model):
def action_scrape_data(self): def action_scrape_data(self):
import random import random
import requests
import json import json
from bs4 import BeautifulSoup import logging
_logger = logging.getLogger(__name__)
for record in self: for record in self:
# 1. Metrikleri Simüle Et (Gerçek senaryoda burada API çağrısı olur) new_followers = record.followers_count or 0
new_followers = record.followers_count + random.randint(-50, 500) if record.followers_count else random.randint(5000, 100000) new_engagement = record.engagement_rate or 0.0
new_engagement = round(random.uniform(1.5, 8.5), 2) real_content_sample = ""
api_success = False
if record.platform == 'instagram':
account = self.env['mymach.social.account'].search([
('platform', '=', 'instagram'),
('active', '=', True),
('access_token', '!=', False),
('access_token', '!=', 'DEMO_IG_TOKEN_999')
], limit=1)
if account and account.platform_account_id:
api_client = account._get_api_client()
try:
discovery = api_client.get_business_discovery(account.platform_account_id, account.access_token, record.username)
biz_data = discovery.get('business_discovery', {})
new_followers = biz_data.get('followers_count', new_followers)
media_data = biz_data.get('media', {}).get('data', [])
total_engagement = sum(m.get('like_count', 0) + m.get('comments_count', 0) for m in media_data)
if new_followers > 0 and media_data:
avg_eng = total_engagement / len(media_data)
new_engagement = round((avg_eng / new_followers) * 100, 2)
captions = [m.get('caption', '') for m in media_data if m.get('caption')]
if captions:
real_content_sample = "\n---\n".join(captions[:5])
api_success = True
_logger.info(f"Gerçek veri başarıyla çekildi: {record.username}")
except Exception as e:
_logger.warning(f"Business Discovery API hatası ({record.username}): {str(e)}")
if not api_success:
# Kullanıcının kendi Meta API'si yoksa veya hata verdiyse:
# Alternatif: Halka açık (Public) sayfadan takipçi sayısını çek (API'siz Scraping)
try:
import requests
from bs4 import BeautifulSoup
import re
# Yöntem 1: Instagram Gizli Web API (Kesin Sonuç)
try:
api_url = f"https://www.instagram.com/api/v1/users/web_profile_info/?username={record.username}"
api_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'X-IG-App-ID': '936619743392459',
'Accept-Language': 'en-US,en;q=0.9'
}
resp_api = requests.get(api_url, headers=api_headers, timeout=5)
if resp_api.status_code == 200:
data = resp_api.json()
user_data = data.get('data', {}).get('user', {})
if user_data and 'edge_followed_by' in user_data:
new_followers = int(user_data['edge_followed_by']['count'])
api_success = True
_logger.info(f"Web API ile {record.username} tam takipçi sayısı çekildi: {new_followers}")
except Exception as e:
pass
# Yöntem 2: SEO JSON-LD Veri Madenciliği (HTML'e gömülü tam rakam)
if not api_success:
url = f"https://www.instagram.com/{record.username}/"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept-Language': 'en-US,en;q=0.9,tr;q=0.8'
}
resp = requests.get(url, headers=headers, timeout=10)
if resp.status_code == 200:
# SEO için konulan 'FollowAction' istatistiğini bul
match_ld = re.search(r'"interactionType"\s*:\s*"https://schema\.org/FollowAction"[^}]*"userInteractionCount"\s*:\s*"?(\d+)"?', resp.text)
if not match_ld:
# Alternatif Hydration JSON noktaları
match_ld = re.search(r'"follower_count"\s*:\s*"?(\d+)"?', resp.text)
if not match_ld:
match_ld = re.search(r'"edge_followed_by"\s*:\s*\{\s*"count"\s*:\s*"?(\d+)"?', resp.text)
if match_ld:
new_followers = int(match_ld.group(1))
api_success = True
_logger.info(f"JSON-LD/Hydration ile {record.username} tam takipçi sayısı çekildi: {new_followers}")
else:
# Yöntem 3: Son Çare (og:description - Yuvarlanmış)
soup = BeautifulSoup(resp.text, 'html.parser')
meta = soup.find('meta', property='og:description')
if meta and meta.get('content'):
content = meta['content']
match = re.search(r'([\d\.,]+)([KMB]?)\s+(Followers|Takipçi)', content, re.IGNORECASE)
if match:
num_str = match.group(1).replace(',', '')
suffix = match.group(2).upper()
val = float(num_str)
if suffix in ['K', 'B']:
val *= 1000
elif suffix == 'M':
val *= 1000000
new_followers = int(val)
api_success = True
_logger.info(f"Meta Etiketi ile {record.username} (yuvarlanmış) takipçi sayısı çekildi: {new_followers}")
if not api_success:
# Yöntem 4: __a=1 JSON uç noktası
alt_url = f"https://www.instagram.com/{record.username}/?__a=1&__d=dis"
alt_resp = requests.get(alt_url, headers=headers, timeout=5)
if alt_resp.status_code == 200:
try:
import json
alt_data = alt_resp.json()
if 'graphql' in alt_data and 'user' in alt_data['graphql']:
new_followers = int(alt_data['graphql']['user']['edge_followed_by']['count'])
api_success = True
_logger.info(f"__a=1 JSON ile {record.username} tam takipçi sayısı çekildi: {new_followers}")
except Exception:
pass
except Exception as e:
_logger.warning(f"Public scraping hatası ({record.username}): {str(e)}")
if not api_success:
# Scraping failed (likely due to Instagram login wall/rate limit).
# Do not throw UserError to avoid blocking the user. Just keep the old value.
new_followers = record.followers_count
_logger.warning(f"'{record.username}' hesabı çekilemedi. Eski veri korundu.")
if not api_success and record.platform != 'instagram':
# Diğer platformlarda gerçek veri çekme özelliği eklene kadar manuel veriler korunur
# Simülasyon (random) tamamen kaldırıldı
pass
# Veritabanına history ekle # Veritabanına history ekle
self.env['social.competitor.history'].create({ self.env['social.competitor.history'].create({
'competitor_id': record.id, 'competitor_id': record.id,
@@ -88,13 +214,24 @@ class SocialCompetitor(models.Model):
'last_scraped': fields.Datetime.now() 'last_scraped': fields.Datetime.now()
}) })
# 2. Yapay Zeka ile (AI Manager) Strateji Analizi # Yapay Zeka ile (AI Manager) Strateji Analizi
try: try:
system_prompt = ( system_prompt = (
"Sen uzman bir sosyal medya stratejistisin. " "Sen uzman bir sosyal medya stratejistisin. "
f"Verilen rakibin ({record.name}) {record.platform} üzerindeki güncel durumunu, " f"Verilen rakibin ({record.name}) {record.platform} üzerindeki güncel durumunu, "
"son zamanlardaki pazarlama stratejilerini ve kullandıkları popüler " "son zamanlardaki pazarlama stratejilerini ve kullandıkları popüler "
"anahtar kelimeleri analiz et. Yanıtını aşağıdaki JSON formatında ver:\n" "anahtar kelimeleri analiz et. "
)
if real_content_sample:
system_prompt += (
f"\nÖNEMLİ: Rakibin son paylaşımlarından elde edilen GERÇEK içerikler şunlardır:\n"
f"{real_content_sample}\n"
"Lütfen analizini doğrudan bu gerçek paylaşımlara dayandır.\n"
)
system_prompt += (
"Yanıtını aşağıdaki JSON formatında ver:\n"
"{\n" "{\n"
" \"summary\": \"<p><b>Strateji Özeti:</b> ...</p><ul><li>...</li></ul>\",\n" " \"summary\": \"<p><b>Strateji Özeti:</b> ...</p><ul><li>...</li></ul>\",\n"
" \"keywords\": \"#hashtag1, #hashtag2, keyword1\"\n" " \"keywords\": \"#hashtag1, #hashtag2, keyword1\"\n"
@@ -107,7 +244,16 @@ class SocialCompetitor(models.Model):
response_format='json' response_format='json'
) )
parsed = json.loads(text_output) clean_text = text_output.strip()
if clean_text.startswith('```json'):
clean_text = clean_text[7:]
elif clean_text.startswith('```'):
clean_text = clean_text[3:]
if clean_text.endswith('```'):
clean_text = clean_text[:-3]
clean_text = clean_text.strip()
parsed = json.loads(clean_text)
record.ai_summary = parsed.get('summary', '<p>Özet bulunamadı.</p>') record.ai_summary = parsed.get('summary', '<p>Özet bulunamadı.</p>')
record.top_keywords = parsed.get('keywords', '') record.top_keywords = parsed.get('keywords', '')
except Exception as e: except Exception as e:
@@ -146,40 +292,16 @@ class SocialCompetitor(models.Model):
@api.model @api.model
def _cron_scrape_new_followers(self): def _cron_scrape_new_followers(self):
"""Saatte bir çalışan rakip takipçi tarama cron'u.""" # Yetki olmadığından dolayı takipçi listesi çekme cron'u kaldırıldı
import random pass
_logger.info("Cron: Rakiplerin yeni takipçileri taranıyor...")
competitors = self.search([])
sample_names = ["ahmet_yılmaz", "zeynep_muhendislik", "cnc_teknik_tr", "metal_isleme_34", "endustri_makine_tr", "mehmet_kaya_cnc", "ozlem_sanayi"]
for competitor in competitors:
num_new = random.randint(1, 3)
for _ in range(num_new):
user_name = random.choice(sample_names) + str(random.randint(10, 99))
existing = self.env['social.competitor.follower'].search([
('competitor_id', '=', competitor.id),
('follower_name', '=', user_name)
], limit=1)
if not existing:
self.env['social.competitor.follower'].create({
'competitor_id': competitor.id,
'follower_name': user_name,
'follower_profile_url': f"https://{competitor.platform}.com/{user_name}",
'follow_status': 'not_followed'
})
_logger.info(f"Yeni rakip takipçisi yakalandı: {user_name} ({competitor.name})")
@api.model_create_multi @api.model_create_multi
def create(self, vals_list): def create(self, vals_list):
records = super().create(vals_list) records = super().create(vals_list)
for record in records: for record in records:
try: try:
# 1. Metrikleri ve AI Stratejisini Çek # Sadece gerçek metrikleri ve AI stratejisini çek
record.action_scrape_data() record.action_scrape_data()
# 2. İlk Takipçileri Yakala
self._cron_scrape_new_followers()
except Exception as e: except Exception as e:
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@@ -255,3 +377,81 @@ class SocialCompetitor(models.Model):
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
_logger.warning(f"Ad Spy JSON Parsing failed, loaded mocks. Error: {e}") _logger.warning(f"Ad Spy JSON Parsing failed, loaded mocks. Error: {e}")
def action_generate_ad_campaign(self):
self.ensure_one()
system_instruction = (
"Sen uzman bir dijital pazarlama stratejisti ve Meta Ads hedef kitle uzmanısın. "
"Sana bir rakip analizi vereceğim. Bu rakibin müşterilerini yasal yollarla (Meta Ads) "
"kendi tarafımıza çekmek için bir reklam kampanyası hedeflemesi kurgulamanı istiyorum. "
"Lütfen çıktıyı tam olarak şu formatta JSON olarak ver:\n"
"{\n"
" \"ai_target_interests\": \"Meta Ads panelinde girilecek virgülle ayrılmış 5-6 İlgi Alanı\",\n"
" \"ai_demographics\": \"Önerilen Yaş Aralığı ve Cinsiyet (Örn: 25-45 Yaş, Tüm Cinsiyetler)\",\n"
" \"ai_ad_copy\": \"Sosyal medyada yayınlayacağımız, rakibin kitlesini etkileyecek profesyonel ve vurucu HTML reklam metni. <b>, <i>, <br> gibi etiketler kullan.\"\n"
"}"
)
user_prompt = f"Rakip Firma: {self.name} ({self.username}). Anahtar Kelimeleri: {self.top_keywords}. Yapay Zeka Özeti: {self.ai_summary}"
ai_target_interests = "Hedef Kitle Bulunamadı"
ai_demographics = "Belirlenemedi"
ai_ad_copy = "<p>Reklam metni oluşturulamadı.</p>"
try:
text_resp = self.env['mymach.social.ai.utils'].generate_text(
prompt=user_prompt,
system_prompt=system_instruction,
response_format='json'
)
import json
import re
# Clean JSON formatting artifacts
cleaned_text = re.sub(r'```(?:json)?\s*', '', text_resp.strip())
cleaned_text = cleaned_text.replace('```', '').strip()
generated_json = json.loads(cleaned_text)
ai_target_interests = generated_json.get('ai_target_interests', ai_target_interests)
ai_demographics = generated_json.get('ai_demographics', ai_demographics)
ai_ad_copy = generated_json.get('ai_ad_copy', ai_ad_copy)
except Exception as e:
import logging
_logger = logging.getLogger(__name__)
_logger.warning(f"Ad Campaign Strategy JSON Parsing failed: {e}")
ai_target_interests = f"Endüstriyel Üretim, {self.name}, Rakipler"
ai_demographics = "25-55 Yaş"
ai_ad_copy = f"<p><b>{self.name}</b> yerine daha kaliteli ve garantili hizmet! Hemen bizimle iletişime geçin.</p>"
from odoo.exceptions import UserError
try:
campaign = self.env['utm.campaign'].create({
'name': f"Anti-{self.name} Hedefli Kampanya",
'competitor_id': self.id,
'ai_target_interests': ai_target_interests,
'ai_demographics': ai_demographics,
'ai_ad_copy': ai_ad_copy,
})
except ValueError as e:
raise UserError(f"Veritabanı alanları henüz güncellenmemiş! Lütfen Odoo arayüzünden 'Uygulamalar' menüsüne gidip 'mymach_social_nextgen' modülünü 'Yükselt (Upgrade)' yapın.\nTeknik Hata: {e}")
except Exception as e:
raise UserError(f"Kampanya oluşturulurken beklenmeyen bir hata oluştu: {e}")
try:
view_id = self.env.ref('mymach_social_nextgen.view_utm_campaign_ai_form').id
except ValueError:
view_id = False
action = {
'name': 'Hedefli Reklam Kampanyası',
'type': 'ir.actions.act_window',
'res_model': 'utm.campaign',
'res_id': campaign.id,
'view_mode': 'form',
'target': 'current',
}
if view_id:
action['view_id'] = view_id
return action

View File

@@ -184,13 +184,17 @@ class SocialPost(models.Model):
for post in self: for post in self:
if not post.content: if not post.content:
continue continue
# Temel URL regex'i # URL regex'i: http://, https:// veya www. ile başlayanları yakalar
urls = re.findall(r'(https?://[^\s]+)', post.content) urls = re.findall(r'((?:https?://|www\.)[^\s]+)', post.content)
new_content = post.content new_content = post.content
for url in urls: for url in urls:
# Odoo'nun link.tracker modelini kullanarak UTM'li kısa link yarat # Odoo'nun link.tracker modelini kullanarak UTM'li kısa link yarat
tracker_url = url
if tracker_url.startswith('www.'):
tracker_url = 'https://' + tracker_url
tracker = self.env['link.tracker'].create({ tracker = self.env['link.tracker'].create({
'url': url, 'url': tracker_url,
'campaign_id': post.utm_campaign_id.id if post.utm_campaign_id else False, 'campaign_id': post.utm_campaign_id.id if post.utm_campaign_id else False,
'title': f"{post.name} Linki" 'title': f"{post.name} Linki"
}) })
@@ -290,74 +294,83 @@ class SocialPost(models.Model):
platform_post_id = False platform_post_id = False
# Hazırlık # Hazırlık
media = post.media_ids and post.media_ids[0] or None medias = post.media_ids
is_video = post.post_type == 'video' is_video = post.post_type == 'video'
is_image = post.post_type == 'image' is_image = post.post_type == 'image'
media_data = None media_data_list = []
mime_type = None if medias:
if media:
import base64 import base64
media_data = base64.b64decode(media.datas) for m in medias:
mime_type = media.mimetype media_data_list.append(base64.b64decode(m.datas))
if account.access_token == 'DEMO_IG_TOKEN_999': if account.access_token == 'DEMO_IG_TOKEN_999':
import random import random
platform_post_id = f"demo_post_{random.randint(1000,9999)}" platform_post_id = f"demo_post_{random.randint(1000,9999)}"
elif account.platform == 'facebook' and account.access_token and api_client: elif account.platform == 'facebook' and account.access_token and api_client:
page_id = account.platform_account_id or 'me' page_id = account.platform_account_id or 'me'
if is_video and media_data: if is_video and media_data_list:
resp = api_client.publish_video(page_id, account.access_token, post.name, post.content, media_data) resp = api_client.publish_video(page_id, account.access_token, post.name, post.content, media_data_list[0])
elif is_image and media_data: elif is_image and len(media_data_list) > 1:
resp = api_client.publish_photo(page_id, account.access_token, post.content, media_data) resp = api_client.publish_carousel(page_id, account.access_token, post.content, media_data_list)
elif is_image and media_data_list:
resp = api_client.publish_photo(page_id, account.access_token, post.content, media_data_list[0])
else: else:
resp = api_client.publish_post(page_id, account.access_token, post.content) resp = api_client.publish_post(page_id, account.access_token, post.content)
platform_post_id = resp.get('id', False) platform_post_id = resp.get('id', False)
elif account.platform == 'instagram' and account.access_token and api_client: elif account.platform == 'instagram' and account.access_token and api_client:
ig_account_id = account.platform_account_id ig_account_id = account.platform_account_id
if media: if medias:
# Generate Public URL # Generate Public URLs
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
public_media_url = f"{base_url}/social/media/{media.id}" url_list = [f"{base_url}/social/media/{m.id}" for m in medias]
resp = api_client.publish_instagram_media(ig_account_id, account.access_token, public_media_url, is_video, post.content) if is_image and len(url_list) > 1:
resp = api_client.publish_instagram_carousel(ig_account_id, account.access_token, url_list, post.content)
else:
resp = api_client.publish_instagram_media(ig_account_id, account.access_token, url_list[0], is_video, post.content)
platform_post_id = resp.get('id', False) platform_post_id = resp.get('id', False)
else: else:
raise Exception("Instagram sadece medya (görsel/video) ile paylaşım kabul eder.") raise Exception("Instagram sadece medya (görsel/video) ile paylaşım kabul eder.")
elif account.platform == 'linkedin' and account.access_token and api_client: elif account.platform == 'linkedin' and account.access_token and api_client:
person_id = account.platform_account_id or 'me' # Not: GerçekteURN alınmalı person_id = account.platform_account_id or 'me'
if media_data: if media_data_list:
asset_urn = api_client.upload_media(person_id, account.access_token, media_data, is_video) asset_urn_list = []
resp = api_client.publish_post(person_id, account.access_token, post.content, media_urn=asset_urn, is_video=is_video) for m_data in media_data_list:
asset_urn = api_client.upload_media(person_id, account.access_token, m_data, is_video)
asset_urn_list.append(asset_urn)
resp = api_client.publish_post(person_id, account.access_token, post.content, media_urn=asset_urn_list, is_video=is_video)
else: else:
resp = api_client.publish_post(person_id, account.access_token, post.content) resp = api_client.publish_post(person_id, account.access_token, post.content)
platform_post_id = resp.get('id', False) platform_post_id = resp.get('id', False)
elif account.platform == 'twitter' and account.access_token and api_client: elif account.platform == 'twitter' and account.access_token and api_client:
# Note: account.platform for X is 'twitter' in selection # Note: account.platform for X is 'twitter' in selection
media_urn = None media_ids = []
if media_data and is_image: if media_data_list and is_image:
media_urn = api_client.upload_media(account.access_token, media_data, mime_type) for m, m_data in zip(medias, media_data_list):
m_id = api_client.upload_media(account.access_token, m_data, m.mimetype)
media_ids.append(m_id)
elif is_video: elif is_video:
raise Exception("X (Twitter) için video paylaşımı henüz desteklenmiyor.") raise Exception("X (Twitter) için video paylaşımı henüz desteklenmiyor.")
media_list = [media_urn] if media_urn else None media_list = media_ids if media_ids else None
resp = api_client.create_tweet(account.access_token, post.content, media_ids=media_list) resp = api_client.create_tweet(account.access_token, post.content, media_ids=media_list)
platform_post_id = resp.get('data', {}).get('id', False) platform_post_id = resp.get('data', {}).get('id', False)
elif account.platform == 'youtube' and account.access_token and api_client: elif account.platform == 'youtube' and account.access_token and api_client:
if media_data and is_video: if media_data_list and is_video:
resp = api_client.upload_video(account.access_token, post.name, post.content, media_data) resp = api_client.upload_video(account.access_token, post.name, post.content, media_data_list[0])
platform_post_id = resp.get('id', False) platform_post_id = resp.get('id', False)
else: else:
raise Exception("YouTube sadece video gönderilerini kabul eder.") raise Exception("YouTube sadece video gönderilerini kabul eder.")
elif account.platform == 'tiktok' and account.access_token and api_client: elif account.platform == 'tiktok' and account.access_token and api_client:
if media and is_video: if medias and is_video:
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
public_media_url = f"{base_url}/social/media/{media.id}" public_media_url = f"{base_url}/social/media/{medias[0].id}"
resp = api_client.upload_video(account.access_token, post.content, public_media_url) resp = api_client.upload_video(account.access_token, post.content, public_media_url)
platform_post_id = resp.get('data', {}).get('publish_id', False) platform_post_id = resp.get('data', {}).get('publish_id', False)
else: else:

View File

@@ -0,0 +1,9 @@
from odoo import models, fields, api
class UtmCampaign(models.Model):
_inherit = 'utm.campaign'
competitor_id = fields.Many2one('social.competitor', string="Rakip Hedef (AI)")
ai_target_interests = fields.Text(string="İlgi Alanları (Meta Ads)")
ai_demographics = fields.Char(string="Demografik Hedefleme")
ai_ad_copy = fields.Html(string="Reklam Metni (Ad Copy)")

View File

@@ -39,7 +39,7 @@
<form string="Rakip Bilgisi"> <form string="Rakip Bilgisi">
<header> <header>
<button name="action_scrape_data" string="Yapay Zeka &amp; Metrik Güncelle" type="object" class="oe_highlight"/> <button name="action_scrape_data" string="Yapay Zeka &amp; Metrik Güncelle" type="object" class="oe_highlight"/>
<button name="action_spy_ads" string="Reklam Casusunu Çalıştır (Ad Spy AI)" type="object" class="btn-danger"/> <button name="action_generate_ad_campaign" string="Meta Reklam Kampanyası Üret (AI)" type="object" class="oe_highlight btn-success"/>
</header> </header>
<sheet> <sheet>
<div class="oe_title"> <div class="oe_title">
@@ -70,50 +70,6 @@
</div> </div>
<notebook> <notebook>
<page string="Yeni Takipçiler (Potansiyel Müşteriler)">
<div class="alert alert-warning" role="alert" invisible="today_followed_count &lt; daily_follow_limit">
<i class="fa fa-exclamation-triangle"/> Günlük güvenli takip limitine (30) ulaştınız. Lütfen spam riskine karşı yarına kadar yeni istek atmayınız.
</div>
<group>
<field name="daily_follow_limit" invisible="1"/>
<field name="today_followed_count" widget="progressbar" options="{'max_value': 'daily_follow_limit'}" string="Bugün Kullanılan Takip Limiti"/>
</group>
<field name="follower_ids">
<list>
<field name="follower_name"/>
<field name="follower_profile_url" widget="url"/>
<field name="detected_date"/>
<field name="follow_status" widget="badge"/>
<button name="action_send_follow_request" string="Takip Et / İstek At" type="object" class="btn-sm btn-success" invisible="follow_status in ('following', 'request_sent')"/>
</list>
</field>
</page>
<page string="Aktif Reklamlar (Ad Spy)">
<field name="ad_ids">
<list>
<field name="name"/>
<field name="detected_date"/>
<field name="status" widget="badge" decoration-success="status == 'active'" decoration-muted="status == 'inactive'"/>
</list>
<form>
<sheet>
<group>
<field name="name"/>
<field name="status" widget="radio"/>
<field name="detected_date"/>
</group>
<group>
<field name="ad_content"/>
</group>
<div class="alert alert-danger" role="alert" invisible="not ai_strategy_suggestion">
<h4><i class="fa fa-crosshairs"/> AI Karşı-Strateji Önerisi</h4>
<hr/>
<field name="ai_strategy_suggestion" widget="html"/>
</div>
</sheet>
</form>
</field>
</page>
<page string="Geçmiş Metrikler"> <page string="Geçmiş Metrikler">
<field name="history_ids" readonly="1"> <field name="history_ids" readonly="1">
<list> <list>
@@ -263,11 +219,7 @@
action="action_social_competitor" action="action_social_competitor"
sequence="1"/> sequence="1"/>
<menuitem id="menu_social_competitor_followers"
name="Yeni Rakip Takipçileri"
parent="menu_social_competitors_root"
action="action_social_competitor_followers"
sequence="2"/>
<menuitem id="menu_social_competitor_history" <menuitem id="menu_social_competitor_history"
name="Büyüme Analizi" name="Büyüme Analizi"

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Sosyal Fırsatlar (Social Leads) Action -->
<record id="action_social_leads" model="ir.actions.act_window">
<field name="name">Sosyal Medya Fırsatları</field>
<field name="res_model">crm.lead</field>
<field name="view_mode">kanban,list,form,calendar,pivot,graph</field>
<field name="domain">[('type','=','lead'), ('medium_id.name', 'ilike', 'Social')]</field>
<field name="context">{
'default_type': 'lead',
'search_default_type': 'lead'
}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Henüz Sosyal Medyadan Gelen Bir Fırsat (Lead) Yok
</p>
<p>
Sosyal medya reklamlarınızdan (Meta Lead Ads, LinkedIn Form) gelen form bilgileri doğrudan buraya düşecektir.
Webhook Endpoint: <b>/api/social/webhook/lead</b>
</p>
</field>
</record>
</odoo>

View File

@@ -114,6 +114,13 @@
action="action_social_visitor" action="action_social_visitor"
sequence="40"/> sequence="40"/>
<!-- Social Leads Menu -->
<menuitem id="menu_social_leads"
name="Sosyal Fırsatlar (Leads)"
parent="menu_social_root"
action="action_social_leads"
sequence="45"/>
<!-- Configuration Menu --> <!-- Configuration Menu -->
<menuitem id="menu_social_configuration" <menuitem id="menu_social_configuration"
name="Yapılandırma" name="Yapılandırma"

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_utm_campaign_ai_form" model="ir.ui.view">
<field name="name">utm.campaign.ai.form</field>
<field name="model">utm.campaign</field>
<field name="arch" type="xml">
<form string="Meta Reklam Hedef Kitlesi (Yapay Zeka)">
<sheet>
<div class="oe_title">
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group>
<field name="competitor_id" readonly="1" options="{'no_create': True, 'no_open': True}"/>
<field name="ai_demographics" readonly="1"/>
</group>
<group>
<field name="ai_target_interests" readonly="1"/>
</group>
</group>
<group string="Önerilen Reklam Metni (Ad Copy)">
<field name="ai_ad_copy" nolabel="1" readonly="1"/>
</group>
</sheet>
</form>
</field>
</record>
</data>
</odoo>