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_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': [
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from . import main
|
from . import main
|
||||||
from . import webhook_controller
|
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
|
||||||
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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -64,16 +64,142 @@ 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({
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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">
|
<form string="Rakip Bilgisi">
|
||||||
<header>
|
<header>
|
||||||
<button name="action_scrape_data" string="Yapay Zeka & Metrik Güncelle" type="object" class="oe_highlight"/>
|
<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>
|
</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 < 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"
|
||||||
|
|||||||
@@ -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"
|
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"
|
||||||
|
|||||||
@@ -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