diff --git a/extra-addons/mymach_social_nextgen/__manifest__.py b/extra-addons/mymach_social_nextgen/__manifest__.py index 2eb14c5..255d5ce 100644 --- a/extra-addons/mymach_social_nextgen/__manifest__.py +++ b/extra-addons/mymach_social_nextgen/__manifest__.py @@ -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': [ diff --git a/extra-addons/mymach_social_nextgen/api/linkedin_api.py b/extra-addons/mymach_social_nextgen/api/linkedin_api.py index aae1c78..665aae9 100644 --- a/extra-addons/mymach_social_nextgen/api/linkedin_api.py +++ b/extra-addons/mymach_social_nextgen/api/linkedin_api.py @@ -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}", diff --git a/extra-addons/mymach_social_nextgen/api/meta_api.py b/extra-addons/mymach_social_nextgen/api/meta_api.py index 9095792..d971ae0 100644 --- a/extra-addons/mymach_social_nextgen/api/meta_api.py +++ b/extra-addons/mymach_social_nextgen/api/meta_api.py @@ -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() diff --git a/extra-addons/mymach_social_nextgen/api/youtube_api.py b/extra-addons/mymach_social_nextgen/api/youtube_api.py index 763cea2..31bd7c6 100644 --- a/extra-addons/mymach_social_nextgen/api/youtube_api.py +++ b/extra-addons/mymach_social_nextgen/api/youtube_api.py @@ -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 diff --git a/extra-addons/mymach_social_nextgen/controllers/__init__.py b/extra-addons/mymach_social_nextgen/controllers/__init__.py index d19a30a..4dd5921 100644 --- a/extra-addons/mymach_social_nextgen/controllers/__init__.py +++ b/extra-addons/mymach_social_nextgen/controllers/__init__.py @@ -1,2 +1,3 @@ from . import main from . import webhook_controller +from . import lead_webhook diff --git a/extra-addons/mymach_social_nextgen/controllers/lead_webhook.py b/extra-addons/mymach_social_nextgen/controllers/lead_webhook.py new file mode 100644 index 0000000..a1b9e6c --- /dev/null +++ b/extra-addons/mymach_social_nextgen/controllers/lead_webhook.py @@ -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')]) diff --git a/extra-addons/mymach_social_nextgen/models/__init__.py b/extra-addons/mymach_social_nextgen/models/__init__.py index 1637909..3246c56 100644 --- a/extra-addons/mymach_social_nextgen/models/__init__.py +++ b/extra-addons/mymach_social_nextgen/models/__init__.py @@ -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 diff --git a/extra-addons/mymach_social_nextgen/models/social_account.py b/extra-addons/mymach_social_nextgen/models/social_account.py index e49751b..a6a036f 100644 --- a/extra-addons/mymach_social_nextgen/models/social_account.py +++ b/extra-addons/mymach_social_nextgen/models/social_account.py @@ -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() diff --git a/extra-addons/mymach_social_nextgen/models/social_competitor.py b/extra-addons/mymach_social_nextgen/models/social_competitor.py index c5b7443..75c5699 100644 --- a/extra-addons/mymach_social_nextgen/models/social_competitor.py +++ b/extra-addons/mymach_social_nextgen/models/social_competitor.py @@ -64,17 +64,143 @@ 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({ 'competitor_id': record.id, @@ -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\": \"
Strateji Özeti: ...
Özet bulunamadı.
') 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. , ,Reklam metni oluşturulamadı.
" + + 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"{self.name} yerine daha kaliteli ve garantili hizmet! Hemen bizimle iletişime geçin.
" + + 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 diff --git a/extra-addons/mymach_social_nextgen/models/social_post.py b/extra-addons/mymach_social_nextgen/models/social_post.py index c104af7..46b3f2f 100644 --- a/extra-addons/mymach_social_nextgen/models/social_post.py +++ b/extra-addons/mymach_social_nextgen/models/social_post.py @@ -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: diff --git a/extra-addons/mymach_social_nextgen/models/utm_campaign_ai.py b/extra-addons/mymach_social_nextgen/models/utm_campaign_ai.py new file mode 100644 index 0000000..b6a3c32 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/utm_campaign_ai.py @@ -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)") diff --git a/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml b/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml index 3c3e326..386f26f 100644 --- a/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml +++ b/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml @@ -39,7 +39,7 @@