Feat: Add Social Lead Catcher webhook and CRM integration
This commit is contained in:
@@ -42,10 +42,12 @@ Features:
|
||||
'views/social_stream_post_kanban_views.xml',
|
||||
'views/social_video_script_views.xml',
|
||||
'views/social_dashboard_views.xml',
|
||||
'views/social_lead_views.xml',
|
||||
'views/social_menu.xml',
|
||||
'views/social_account_views.xml',
|
||||
'views/social_competitor_views.xml',
|
||||
'views/social_trend_views.xml',
|
||||
'views/utm_campaign_ai_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
|
||||
@@ -18,7 +18,7 @@ class LinkedInAPI:
|
||||
'client_id': self.client_id,
|
||||
'redirect_uri': self.redirect_uri,
|
||||
'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)}"
|
||||
|
||||
@@ -35,6 +35,14 @@ class LinkedInAPI:
|
||||
response.raise_for_status()
|
||||
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):
|
||||
"""Register and upload media to LinkedIn."""
|
||||
# 1. Register Upload
|
||||
@@ -91,12 +99,10 @@ class LinkedInAPI:
|
||||
|
||||
if media_urn:
|
||||
share_content["shareMediaCategory"] = "VIDEO" if is_video else "IMAGE"
|
||||
share_content["media"] = [
|
||||
{
|
||||
"status": "READY",
|
||||
"media": media_urn
|
||||
}
|
||||
]
|
||||
if isinstance(media_urn, list):
|
||||
share_content["media"] = [{"status": "READY", "media": urn} for urn in media_urn[:9]]
|
||||
else:
|
||||
share_content["media"] = [{"status": "READY", "media": media_urn}]
|
||||
|
||||
payload = {
|
||||
"author": f"urn:li:person:{person_id}",
|
||||
|
||||
@@ -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.")
|
||||
|
||||
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):
|
||||
"""Belirtilen sayfada gönderi paylaşır."""
|
||||
url = f"{self.GRAPH_URL}/{page_id}/feed"
|
||||
@@ -92,15 +107,33 @@ class MetaAPI:
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
def publish_photo(self, page_id, access_token, message, image_data):
|
||||
"""Facebook sayfasına görsel paylaşır."""
|
||||
def publish_photo(self, page_id, access_token, message, image_data, published=True):
|
||||
"""Facebook sayfasına görsel paylaşır veya gizli yükler."""
|
||||
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')}
|
||||
response = requests.post(url, data=payload, files=files, timeout=30)
|
||||
response.raise_for_status()
|
||||
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):
|
||||
"""Facebook sayfasına video yükler."""
|
||||
# Note: Video upload endpoint is graph-video.facebook.com
|
||||
@@ -139,6 +172,48 @@ class MetaAPI:
|
||||
'creation_id': container_id,
|
||||
'access_token': access_token
|
||||
}
|
||||
pub_resp = requests.post(publish_url, data=publish_payload, timeout=20)
|
||||
pub_resp.raise_for_status()
|
||||
return pub_resp.json()
|
||||
publish_resp = requests.post(publish_url, data=publish_payload, timeout=20)
|
||||
publish_resp.raise_for_status()
|
||||
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()
|
||||
|
||||
@@ -36,6 +36,18 @@ class YouTubeAPI:
|
||||
response.raise_for_status()
|
||||
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"):
|
||||
"""Upload video to YouTube via Data API v3 multipart upload."""
|
||||
import json
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
from . import main
|
||||
from . import webhook_controller
|
||||
from . import lead_webhook
|
||||
|
||||
@@ -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')])
|
||||
@@ -10,6 +10,7 @@ from . import social_media
|
||||
from . import social_auto_reply
|
||||
from . import social_auto_reply_log
|
||||
from . import social_trend
|
||||
from . import utm_campaign_ai
|
||||
from . import social_competitor_history
|
||||
from . import social_competitor_follower
|
||||
from . import social_competitor_ad
|
||||
|
||||
@@ -116,6 +116,13 @@ class SocialAccount(models.Model):
|
||||
except Exception as e:
|
||||
# We raise the exception so the user can see exactly why Facebook is failing.
|
||||
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':
|
||||
# For facebook, get the first page ID and its Page Access Token
|
||||
try:
|
||||
@@ -140,16 +147,27 @@ class SocialAccount(models.Model):
|
||||
'access_token': new_token,
|
||||
'is_active': True,
|
||||
})
|
||||
# Odoo'nun standart success mesajı gösterilebilir
|
||||
except Exception as 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:
|
||||
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
|
||||
def _cron_refresh_tokens(self):
|
||||
# 60 gün dolmadan yenilenmesi gerekenleri bul (şimdilik tüm aktif facebook hesaplarını deneyelim)
|
||||
accounts = self.search([('is_active', '=', True), ('platform', '=', 'facebook')])
|
||||
# Otomatik yenilenmesi gerekenleri bul
|
||||
accounts = self.search([('is_active', '=', True), ('platform', 'in', ['facebook', 'youtube'])])
|
||||
for acc in accounts:
|
||||
try:
|
||||
acc.action_refresh_token()
|
||||
|
||||
@@ -64,16 +64,142 @@ class SocialCompetitor(models.Model):
|
||||
|
||||
def action_scrape_data(self):
|
||||
import random
|
||||
import requests
|
||||
import json
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
for record in self:
|
||||
# 1. Metrikleri Simüle Et (Gerçek senaryoda burada API çağrısı olur)
|
||||
new_followers = record.followers_count + random.randint(-50, 500) if record.followers_count else random.randint(5000, 100000)
|
||||
new_engagement = round(random.uniform(1.5, 8.5), 2)
|
||||
new_followers = record.followers_count or 0
|
||||
new_engagement = record.engagement_rate or 0.0
|
||||
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
|
||||
self.env['social.competitor.history'].create({
|
||||
@@ -88,13 +214,24 @@ class SocialCompetitor(models.Model):
|
||||
'last_scraped': fields.Datetime.now()
|
||||
})
|
||||
|
||||
# 2. Yapay Zeka ile (AI Manager) Strateji Analizi
|
||||
# Yapay Zeka ile (AI Manager) Strateji Analizi
|
||||
try:
|
||||
system_prompt = (
|
||||
"Sen uzman bir sosyal medya stratejistisin. "
|
||||
f"Verilen rakibin ({record.name}) {record.platform} üzerindeki güncel durumunu, "
|
||||
"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"
|
||||
" \"summary\": \"<p><b>Strateji Özeti:</b> ...</p><ul><li>...</li></ul>\",\n"
|
||||
" \"keywords\": \"#hashtag1, #hashtag2, keyword1\"\n"
|
||||
@@ -107,7 +244,16 @@ class SocialCompetitor(models.Model):
|
||||
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.top_keywords = parsed.get('keywords', '')
|
||||
except Exception as e:
|
||||
@@ -146,40 +292,16 @@ class SocialCompetitor(models.Model):
|
||||
|
||||
@api.model
|
||||
def _cron_scrape_new_followers(self):
|
||||
"""Saatte bir çalışan rakip takipçi tarama cron'u."""
|
||||
import random
|
||||
_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})")
|
||||
# Yetki olmadığından dolayı takipçi listesi çekme cron'u kaldırıldı
|
||||
pass
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
records = super().create(vals_list)
|
||||
for record in records:
|
||||
try:
|
||||
# 1. Metrikleri ve AI Stratejisini Çek
|
||||
# Sadece gerçek metrikleri ve AI stratejisini çek
|
||||
record.action_scrape_data()
|
||||
# 2. İlk Takipçileri Yakala
|
||||
self._cron_scrape_new_followers()
|
||||
except Exception as e:
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -255,3 +377,81 @@ class SocialCompetitor(models.Model):
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_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
|
||||
|
||||
@@ -184,13 +184,17 @@ class SocialPost(models.Model):
|
||||
for post in self:
|
||||
if not post.content:
|
||||
continue
|
||||
# Temel URL regex'i
|
||||
urls = re.findall(r'(https?://[^\s]+)', post.content)
|
||||
# URL regex'i: http://, https:// veya www. ile başlayanları yakalar
|
||||
urls = re.findall(r'((?:https?://|www\.)[^\s]+)', post.content)
|
||||
new_content = post.content
|
||||
for url in urls:
|
||||
# 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({
|
||||
'url': url,
|
||||
'url': tracker_url,
|
||||
'campaign_id': post.utm_campaign_id.id if post.utm_campaign_id else False,
|
||||
'title': f"{post.name} Linki"
|
||||
})
|
||||
@@ -290,74 +294,83 @@ class SocialPost(models.Model):
|
||||
platform_post_id = False
|
||||
|
||||
# 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_image = post.post_type == 'image'
|
||||
|
||||
media_data = None
|
||||
mime_type = None
|
||||
if media:
|
||||
media_data_list = []
|
||||
if medias:
|
||||
import base64
|
||||
media_data = base64.b64decode(media.datas)
|
||||
mime_type = media.mimetype
|
||||
for m in medias:
|
||||
media_data_list.append(base64.b64decode(m.datas))
|
||||
|
||||
if account.access_token == 'DEMO_IG_TOKEN_999':
|
||||
import random
|
||||
platform_post_id = f"demo_post_{random.randint(1000,9999)}"
|
||||
elif account.platform == 'facebook' and account.access_token and api_client:
|
||||
page_id = account.platform_account_id or 'me'
|
||||
if is_video and media_data:
|
||||
resp = api_client.publish_video(page_id, account.access_token, post.name, post.content, media_data)
|
||||
elif is_image and media_data:
|
||||
resp = api_client.publish_photo(page_id, account.access_token, post.content, 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_list[0])
|
||||
elif is_image and len(media_data_list) > 1:
|
||||
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:
|
||||
resp = api_client.publish_post(page_id, account.access_token, post.content)
|
||||
platform_post_id = resp.get('id', False)
|
||||
|
||||
elif account.platform == 'instagram' and account.access_token and api_client:
|
||||
ig_account_id = account.platform_account_id
|
||||
if media:
|
||||
# Generate Public URL
|
||||
if medias:
|
||||
# Generate Public URLs
|
||||
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)
|
||||
else:
|
||||
raise Exception("Instagram sadece medya (görsel/video) ile paylaşım kabul eder.")
|
||||
|
||||
elif account.platform == 'linkedin' and account.access_token and api_client:
|
||||
person_id = account.platform_account_id or 'me' # Not: GerçekteURN alınmalı
|
||||
if media_data:
|
||||
asset_urn = api_client.upload_media(person_id, account.access_token, media_data, is_video)
|
||||
resp = api_client.publish_post(person_id, account.access_token, post.content, media_urn=asset_urn, is_video=is_video)
|
||||
person_id = account.platform_account_id or 'me'
|
||||
if media_data_list:
|
||||
asset_urn_list = []
|
||||
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:
|
||||
resp = api_client.publish_post(person_id, account.access_token, post.content)
|
||||
platform_post_id = resp.get('id', False)
|
||||
|
||||
elif account.platform == 'twitter' and account.access_token and api_client:
|
||||
# Note: account.platform for X is 'twitter' in selection
|
||||
media_urn = None
|
||||
if media_data and is_image:
|
||||
media_urn = api_client.upload_media(account.access_token, media_data, mime_type)
|
||||
media_ids = []
|
||||
if media_data_list and is_image:
|
||||
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:
|
||||
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)
|
||||
platform_post_id = resp.get('data', {}).get('id', False)
|
||||
|
||||
elif account.platform == 'youtube' and account.access_token and api_client:
|
||||
if media_data and is_video:
|
||||
resp = api_client.upload_video(account.access_token, post.name, post.content, media_data)
|
||||
if media_data_list and is_video:
|
||||
resp = api_client.upload_video(account.access_token, post.name, post.content, media_data_list[0])
|
||||
platform_post_id = resp.get('id', False)
|
||||
else:
|
||||
raise Exception("YouTube sadece video gönderilerini kabul eder.")
|
||||
|
||||
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')
|
||||
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)
|
||||
platform_post_id = resp.get('data', {}).get('publish_id', False)
|
||||
else:
|
||||
|
||||
@@ -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)")
|
||||
@@ -39,7 +39,7 @@
|
||||
<form string="Rakip Bilgisi">
|
||||
<header>
|
||||
<button name="action_scrape_data" string="Yapay Zeka & 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>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
@@ -70,50 +70,6 @@
|
||||
</div>
|
||||
|
||||
<notebook>
|
||||
<page string="Yeni Takipçiler (Potansiyel Müşteriler)">
|
||||
<div class="alert alert-warning" role="alert" invisible="today_followed_count < 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">
|
||||
<field name="history_ids" readonly="1">
|
||||
<list>
|
||||
@@ -263,11 +219,7 @@
|
||||
action="action_social_competitor"
|
||||
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"
|
||||
name="Büyüme Analizi"
|
||||
|
||||
@@ -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>
|
||||
@@ -114,6 +114,13 @@
|
||||
action="action_social_visitor"
|
||||
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 -->
|
||||
<menuitem id="menu_social_configuration"
|
||||
name="Yapılandırma"
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user