From 075b79242d42aa04a435a488d9ab7210bc907a85 Mon Sep 17 00:00:00 2001 From: cerenX9 Date: Wed, 29 Jul 2026 17:38:30 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20Meta=20API=20hata=20yakalama=20ve=20Inst?= =?UTF-8?q?agram=20entegrasyonu=20tamamen=20d=C3=BCzeltildi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mymach_social_nextgen/__manifest__.py | 7 + .../mymach_social_nextgen/api/linkedin_api.py | 78 +++++ .../mymach_social_nextgen/api/meta_api.py | 79 ++++- .../mymach_social_nextgen/api/tiktok_api.py | 24 ++ .../mymach_social_nextgen/api/x_api.py | 24 ++ .../mymach_social_nextgen/api/youtube_api.py | 25 ++ .../mymach_social_nextgen/controllers/main.py | 33 ++ .../data/social_competitor_cron.xml | 10 + .../data/social_video_cron.xml | 14 + .../mymach_social_nextgen/models/__init__.py | 10 +- .../models/res_company.py | 2 + .../models/res_config_settings.py | 14 + .../models/social_account.py | 46 ++- .../models/social_ai_utils.py | 219 +++++++++++++ .../models/social_auto_reply.py | 19 +- .../models/social_competitor.py | 225 ++++++++++++- .../models/social_competitor_ad.py | 17 + .../models/social_competitor_follower.py | 91 ++++++ .../models/social_competitor_history.py | 11 + .../models/social_dashboard.py | 61 ++++ .../models/social_media.py | 2 + .../models/social_post.py | 276 +++++++++++++++- .../models/social_stream.py | 4 +- .../models/social_stream_post.py | 215 ++++++++++--- .../models/social_trend.py | 234 ++++++++++++++ .../models/social_video_script.py | 295 ++++++++++++++++++ .../scratch/associate_existing_posts.py | 48 +++ .../scratch/delete_dashboard_menu.py | 34 ++ .../scratch/sync_media_posts.py | 24 ++ .../scratch/sync_published_posts.py | 51 +++ .../scratch/test_competitor.py | 44 +++ .../scratch/test_gemini_sentiment.py | 51 +++ .../scratch/test_sentiment_analysis.py | 39 +++ .../scratch/test_system_integrity.py | 121 +++++++ .../scratch/test_video_script.py | 44 +++ .../scratch/test_watermarking.py | 83 +++++ .../scratch/update_post_type.py | 17 + .../security/ir.model.access.csv | 11 +- .../src/components/dashboard/dashboard.js | 46 +++ .../src/components/dashboard/dashboard.scss | 26 ++ .../src/components/dashboard/dashboard.xml | 179 +++++++++++ .../static/src/css/social_dashboard.css | 56 +++- .../mymach_social_nextgen/test_ai_manager.py | 18 ++ .../views/res_config_settings_views.xml | 45 +++ .../views/social_account_views.xml | 13 +- .../views/social_analytics_views.xml | 15 - .../views/social_auto_reply_views.xml | 24 +- .../views/social_competitor_views.xml | 187 ++++++++++- .../views/social_dashboard_views.xml | 9 + .../views/social_media_views.xml | 46 ++- .../views/social_menu.xml | 9 +- .../views/social_post_views.xml | 10 +- .../views/social_stream_post_kanban_views.xml | 72 ++++- .../views/social_stream_views.xml | 5 +- .../views/social_trend_views.xml | 185 +++++++++++ .../views/social_video_script_views.xml | 145 +++++++++ .../wizard/social_ai_reply_wizard.py | 86 +---- .../wizard/social_ai_wizard.py | 162 +++++----- .../wizard/social_ai_wizard_views.xml | 32 +- 59 files changed, 3666 insertions(+), 306 deletions(-) create mode 100644 extra-addons/mymach_social_nextgen/data/social_video_cron.xml create mode 100644 extra-addons/mymach_social_nextgen/models/social_ai_utils.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_competitor_ad.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_competitor_follower.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_competitor_history.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_dashboard.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_trend.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_video_script.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/associate_existing_posts.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/delete_dashboard_menu.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/sync_media_posts.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/sync_published_posts.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/test_competitor.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/test_gemini_sentiment.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/test_sentiment_analysis.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/test_system_integrity.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/test_video_script.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/test_watermarking.py create mode 100644 extra-addons/mymach_social_nextgen/scratch/update_post_type.py create mode 100644 extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.js create mode 100644 extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.scss create mode 100644 extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.xml create mode 100644 extra-addons/mymach_social_nextgen/test_ai_manager.py create mode 100644 extra-addons/mymach_social_nextgen/views/social_dashboard_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_trend_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_video_script_views.xml diff --git a/extra-addons/mymach_social_nextgen/__manifest__.py b/extra-addons/mymach_social_nextgen/__manifest__.py index 4d8922e..2eb14c5 100644 --- a/extra-addons/mymach_social_nextgen/__manifest__.py +++ b/extra-addons/mymach_social_nextgen/__manifest__.py @@ -29,6 +29,7 @@ Features: 'security/ir.model.access.csv', 'data/social_post_cron.xml', 'data/social_competitor_cron.xml', + 'data/social_video_cron.xml', 'views/res_config_settings_views.xml', 'views/social_analytics_views.xml', 'views/social_live_post_views.xml', @@ -39,13 +40,19 @@ Features: 'views/social_auto_reply_views.xml', 'views/social_auto_reply_log_views.xml', 'views/social_stream_post_kanban_views.xml', + 'views/social_video_script_views.xml', + 'views/social_dashboard_views.xml', 'views/social_menu.xml', 'views/social_account_views.xml', 'views/social_competitor_views.xml', + 'views/social_trend_views.xml', ], 'assets': { 'web.assets_backend': [ 'mymach_social_nextgen/static/src/css/social_dashboard.css', + 'mymach_social_nextgen/static/src/components/dashboard/dashboard.js', + 'mymach_social_nextgen/static/src/components/dashboard/dashboard.scss', + 'mymach_social_nextgen/static/src/components/dashboard/dashboard.xml', ], }, 'installable': True, diff --git a/extra-addons/mymach_social_nextgen/api/linkedin_api.py b/extra-addons/mymach_social_nextgen/api/linkedin_api.py index 61914ae..aae1c78 100644 --- a/extra-addons/mymach_social_nextgen/api/linkedin_api.py +++ b/extra-addons/mymach_social_nextgen/api/linkedin_api.py @@ -34,3 +34,81 @@ class LinkedInAPI: response = requests.post(self.TOKEN_URL, data=data, 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 + register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" + recipe = "urn:li:digitalmediaRecipe:feedshare-video" if is_video else "urn:li:digitalmediaRecipe:feedshare-image" + + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json', + 'X-Restli-Protocol-Version': '2.0.0' + } + + register_payload = { + "registerUploadRequest": { + "recipes": [recipe], + "owner": f"urn:li:person:{person_id}", + "serviceRelationships": [ + { + "relationshipType": "OWNER", + "identifier": "urn:li:userGeneratedContent" + } + ] + } + } + reg_response = requests.post(register_url, headers=headers, json=register_payload, timeout=15) + reg_response.raise_for_status() + + reg_data = reg_response.json() + upload_url = reg_data['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl'] + asset_urn = reg_data['value']['asset'] + + # 2. Upload Binary Data + upload_headers = {'Authorization': f'Bearer {access_token}'} + up_resp = requests.put(upload_url, headers=upload_headers, data=media_data, timeout=60) + up_resp.raise_for_status() + + return asset_urn + + def publish_post(self, person_id, access_token, text, media_urn=None, is_video=False): + """Create a UGC post on LinkedIn.""" + url = "https://api.linkedin.com/v2/ugcPosts" + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json', + 'X-Restli-Protocol-Version': '2.0.0' + } + + share_content = { + "shareCommentary": { + "text": text + }, + "shareMediaCategory": "NONE" + } + + if media_urn: + share_content["shareMediaCategory"] = "VIDEO" if is_video else "IMAGE" + share_content["media"] = [ + { + "status": "READY", + "media": media_urn + } + ] + + payload = { + "author": f"urn:li:person:{person_id}", + "lifecycleState": "PUBLISHED", + "specificContent": { + "com.linkedin.ugc.ShareContent": share_content + }, + "visibility": { + "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" + } + } + + response = requests.post(url, headers=headers, json=payload, timeout=20) + response.raise_for_status() + return response.json() diff --git a/extra-addons/mymach_social_nextgen/api/meta_api.py b/extra-addons/mymach_social_nextgen/api/meta_api.py index a6265ba..9095792 100644 --- a/extra-addons/mymach_social_nextgen/api/meta_api.py +++ b/extra-addons/mymach_social_nextgen/api/meta_api.py @@ -19,7 +19,8 @@ class MetaAPI: 'redirect_uri': self.redirect_uri, 'state': state, 'scope': 'pages_show_list,pages_read_engagement,pages_manage_posts,instagram_basic,instagram_content_publish', - 'response_type': 'code' + 'response_type': 'code', + 'auth_type': 'rerequest' } return f"{self.AUTH_URL}?{urlencode(params)}" @@ -31,7 +32,11 @@ class MetaAPI: 'code': code } response = requests.get(self.TOKEN_URL, params=params, timeout=10) - response.raise_for_status() + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + error_msg = response.text + raise Exception(f"Facebook API Hatası: {error_msg}") from e return response.json() def get_profile_info(self, access_token): @@ -42,6 +47,25 @@ class MetaAPI: response.raise_for_status() return response.json() + def get_instagram_account_id(self, access_token): + """Kullanıcının bağladığı ilk Instagram Business hesap ID'sini bulur.""" + pages_data = self.get_profile_info(access_token) + pages = pages_data.get('data', []) + if not pages: + raise Exception("Facebook hesabınıza bağlı hiçbir Facebook Sayfası bulunamadı. Lütfen bir İşletme Sayfası oluşturun.") + + # İlk sayfayı veya instagram hesabı bağlı olan ilk sayfayı bul + for page in pages: + page_id = page['id'] + url = f"{self.GRAPH_URL}/{page_id}?fields=instagram_business_account" + resp = requests.get(url, params={'access_token': access_token}, timeout=10) + resp_data = resp.json() + ig_account = resp_data.get('instagram_business_account') + if ig_account: + return ig_account.get('id') + + 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 publish_post(self, page_id, access_token, message, link=None): """Belirtilen sayfada gönderi paylaşır.""" url = f"{self.GRAPH_URL}/{page_id}/feed" @@ -67,3 +91,54 @@ class MetaAPI: response = requests.get(self.TOKEN_URL, params=params, timeout=10) 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.""" + url = f"{self.GRAPH_URL}/{page_id}/photos" + payload = {'message': message, 'access_token': access_token} + 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_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 + url = f"https://graph-video.facebook.com/v19.0/{page_id}/videos" + payload = {'title': title, 'description': description, 'access_token': access_token} + files = {'source': ('video.mp4', video_data, 'video/mp4')} + response = requests.post(url, data=payload, files=files, timeout=60) + response.raise_for_status() + return response.json() + + def publish_instagram_media(self, ig_account_id, access_token, media_url, is_video, caption): + """Instagram hesabı üzerinden resim veya Reels (video) paylaşır. Media_url public olmalıdır.""" + # 1. Create Media Container + container_url = f"{self.GRAPH_URL}/{ig_account_id}/media" + payload = { + 'caption': caption, + 'access_token': access_token + } + if is_video: + payload['media_type'] = 'REELS' + payload['video_url'] = media_url + else: + payload['image_url'] = media_url + + container_resp = requests.post(container_url, data=payload, timeout=20) + container_resp.raise_for_status() + container_id = container_resp.json().get('id') + + # Instagram API bazen konteynerin işlenmesi için 2-3 saniye süre isteyebilir + import time + time.sleep(3) + + # 2. Publish Media Container + publish_url = f"{self.GRAPH_URL}/{ig_account_id}/media_publish" + publish_payload = { + '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() diff --git a/extra-addons/mymach_social_nextgen/api/tiktok_api.py b/extra-addons/mymach_social_nextgen/api/tiktok_api.py index df59b3c..a2ce514 100644 --- a/extra-addons/mymach_social_nextgen/api/tiktok_api.py +++ b/extra-addons/mymach_social_nextgen/api/tiktok_api.py @@ -36,3 +36,27 @@ class TikTokAPI: response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10) response.raise_for_status() return response.json() + + def upload_video(self, access_token, title, video_url): + """Publish video to TikTok using Content Posting API (Pull from URL).""" + url = "https://open.tiktokapis.com/v2/post/publish/video/init/" + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json; charset=UTF-8' + } + payload = { + "post_info": { + "title": title, + "privacy_level": "PUBLIC_TO_EVERYONE", + "disable_duet": False, + "disable_stitch": False, + "disable_comment": False + }, + "source_info": { + "source": "PULL_FROM_URL", + "video_url": video_url + } + } + response = requests.post(url, headers=headers, json=payload, timeout=20) + response.raise_for_status() + return response.json() diff --git a/extra-addons/mymach_social_nextgen/api/x_api.py b/extra-addons/mymach_social_nextgen/api/x_api.py index e057067..57d765a 100644 --- a/extra-addons/mymach_social_nextgen/api/x_api.py +++ b/extra-addons/mymach_social_nextgen/api/x_api.py @@ -43,3 +43,27 @@ class XAPI: response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10) response.raise_for_status() return response.json() + + def upload_media(self, access_token, media_data, mime_type="image/jpeg"): + """Upload media to X using API v1.1.""" + url = "https://upload.twitter.com/1.1/media/upload.json" + headers = {'Authorization': f'Bearer {access_token}'} + files = {'media': ('media_file', media_data, mime_type)} + response = requests.post(url, headers=headers, files=files, timeout=30) + response.raise_for_status() + return response.json().get('media_id_string') + + def create_tweet(self, access_token, text, media_ids=None): + """Post a tweet using API v2.""" + url = "https://api.twitter.com/2/tweets" + headers = { + 'Authorization': f'Bearer {access_token}', + 'Content-Type': 'application/json' + } + payload = {'text': text} + if media_ids: + payload['media'] = {'media_ids': media_ids} + + response = requests.post(url, headers=headers, json=payload, timeout=15) + response.raise_for_status() + return response.json() diff --git a/extra-addons/mymach_social_nextgen/api/youtube_api.py b/extra-addons/mymach_social_nextgen/api/youtube_api.py index d1852c3..763cea2 100644 --- a/extra-addons/mymach_social_nextgen/api/youtube_api.py +++ b/extra-addons/mymach_social_nextgen/api/youtube_api.py @@ -35,3 +35,28 @@ class YouTubeAPI: 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 + url = "https://www.googleapis.com/upload/youtube/v3/videos?uploadType=multipart&part=snippet,status" + headers = { + 'Authorization': f'Bearer {access_token}', + } + metadata = { + "snippet": { + "title": title, + "description": description, + "categoryId": category_id + }, + "status": { + "privacyStatus": privacy_status + } + } + files = { + 'resource': (None, json.dumps(metadata), 'application/json; charset=UTF-8'), + 'media': ('video.mp4', video_data, 'video/mp4') + } + response = requests.post(url, headers=headers, files=files, timeout=120) + response.raise_for_status() + return response.json() diff --git a/extra-addons/mymach_social_nextgen/controllers/main.py b/extra-addons/mymach_social_nextgen/controllers/main.py index d0ecc7e..28861bd 100644 --- a/extra-addons/mymach_social_nextgen/controllers/main.py +++ b/extra-addons/mymach_social_nextgen/controllers/main.py @@ -74,3 +74,36 @@ class SocialMediaController(http.Controller): # Şimdilik canlı olmadığı için logluyoruz. # _logger.info("Meta Webhook Received: %s", data) return {'status': 'success'} + + @http.route('/social/media/', type='http', auth="public") + def public_media_access(self, attachment_id, **kwargs): + """ + Instagram (veya benzeri) API'ler dosyanın doğrudan binary yüklenmesini değil, + herkese açık bir internet linki (URL) üzerinden çekilmesini ister. + Bu endpoint, ilgili ir.attachment dosyasını public olarak sunar. + """ + # Güvenlik notu: Gerçek hayatta burada access_token (hash) tabanlı bir koruma yapılmalıdır. + attachment = request.env['ir.attachment'].sudo().browse(attachment_id) + if not attachment.exists(): + return request.not_found() + + status, headers, content = request.env['ir.http'].sudo().binary_content( + id=attachment.id, + default_mimetype='application/octet-stream' + ) + + if status != 200: + return request.not_found() + + content_base64 = attachment.datas + import base64 + import io + + if not content_base64: + return request.not_found() + + filecontent = base64.b64decode(content_base64) + + headers.append(('Content-Length', len(filecontent))) + response = request.make_response(filecontent, headers) + return response diff --git a/extra-addons/mymach_social_nextgen/data/social_competitor_cron.xml b/extra-addons/mymach_social_nextgen/data/social_competitor_cron.xml index 602fe72..85d9714 100644 --- a/extra-addons/mymach_social_nextgen/data/social_competitor_cron.xml +++ b/extra-addons/mymach_social_nextgen/data/social_competitor_cron.xml @@ -10,5 +10,15 @@ days + + + Sosyal Medya: Rakip Yeni Takipçilerini İzleme (Saatlik) + + code + model._cron_scrape_new_followers() + 1 + hours + + diff --git a/extra-addons/mymach_social_nextgen/data/social_video_cron.xml b/extra-addons/mymach_social_nextgen/data/social_video_cron.xml new file mode 100644 index 0000000..7c7a20f --- /dev/null +++ b/extra-addons/mymach_social_nextgen/data/social_video_cron.xml @@ -0,0 +1,14 @@ + + + + + Sosyal Medya: AI Video Render Kontrolü + + code + model._cron_check_video_renders() + 1 + minutes + + + + diff --git a/extra-addons/mymach_social_nextgen/models/__init__.py b/extra-addons/mymach_social_nextgen/models/__init__.py index 9fcf717..1637909 100644 --- a/extra-addons/mymach_social_nextgen/models/__init__.py +++ b/extra-addons/mymach_social_nextgen/models/__init__.py @@ -8,4 +8,12 @@ from . import res_config_settings from . import res_company from . import social_media from . import social_auto_reply -from . import social_auto_reply_log \ No newline at end of file +from . import social_auto_reply_log +from . import social_trend +from . import social_competitor_history +from . import social_competitor_follower +from . import social_competitor_ad + +from . import social_video_script +from . import social_ai_utils +from . import social_dashboard diff --git a/extra-addons/mymach_social_nextgen/models/res_company.py b/extra-addons/mymach_social_nextgen/models/res_company.py index 82536d0..b15cd39 100644 --- a/extra-addons/mymach_social_nextgen/models/res_company.py +++ b/extra-addons/mymach_social_nextgen/models/res_company.py @@ -11,3 +11,5 @@ class ResCompany(models.Model): ('top_left', 'Sol Üst'), ('center', 'Merkez') ], string="Filigran Konumu", default='bottom_right') + social_watermark_opacity = fields.Integer(string="Filigran Şeffaflığı (%)", default=50, help="10 ile 100 arasında şeffaflık yüzdesi.") + social_watermark_size = fields.Integer(string="Filigran Boyutu (%)", default=20, help="Görsel genişliğine oranla filigran boyutu yüzdesi (5-50 arası).") diff --git a/extra-addons/mymach_social_nextgen/models/res_config_settings.py b/extra-addons/mymach_social_nextgen/models/res_config_settings.py index 654b69d..07428cc 100644 --- a/extra-addons/mymach_social_nextgen/models/res_config_settings.py +++ b/extra-addons/mymach_social_nextgen/models/res_config_settings.py @@ -20,10 +20,24 @@ class ResConfigSettings(models.TransientModel): social_youtube_client_secret = fields.Char(string="YouTube Client Secret", config_parameter="mymach_social.youtube_client_secret") # AI APIs + social_default_ai_provider = fields.Selection([ + ('gemini', 'Google Gemini (Varsayılan)'), + ('openai', 'OpenAI (ChatGPT)'), + ('claude', 'Anthropic Claude'), + ('groq', 'Groq (Llama 3 - Ücretsiz/Hızlı)'), + ('grok', 'Grok (X.ai)') + ], string="Varsayılan Yapay Zeka", default='gemini', config_parameter="mymach_social.default_ai_provider", required=True) + social_gemini_api_key = fields.Char(string="Google Gemini API Key", config_parameter="mymach_social.gemini_api_key") social_openai_api_key = fields.Char(string="OpenAI API Key", config_parameter="mymach_social.openai_api_key") social_claude_api_key = fields.Char(string="Claude API Key", config_parameter="mymach_social.claude_api_key") social_grok_api_key = fields.Char(string="Grok API Key", config_parameter="mymach_social.grok_api_key") + social_groq_api_key = fields.Char(string="Groq API Key", config_parameter="mymach_social.groq_api_key") + social_replicate_api_key = fields.Char(string="Replicate API Key", config_parameter="mymach_social.replicate_api_key") + social_creatify_api_key = fields.Char(string="Creatify API Key (15s Video)", config_parameter="mymach_social.creatify_api_key") + # Watermark Settings social_watermark_image = fields.Image(related='company_id.social_watermark_image', readonly=False) social_watermark_position = fields.Selection(related='company_id.social_watermark_position', readonly=False) + social_watermark_opacity = fields.Integer(related='company_id.social_watermark_opacity', readonly=False) + social_watermark_size = fields.Integer(related='company_id.social_watermark_size', readonly=False) diff --git a/extra-addons/mymach_social_nextgen/models/social_account.py b/extra-addons/mymach_social_nextgen/models/social_account.py index 4f35715..e49751b 100644 --- a/extra-addons/mymach_social_nextgen/models/social_account.py +++ b/extra-addons/mymach_social_nextgen/models/social_account.py @@ -57,7 +57,7 @@ class SocialAccount(models.Model): client_secret = config.get_param('mymach_social.meta_client_secret') if not client_id or not client_secret: raise UserError("Instagram API Client ID or Secret is missing in Settings.") - return InstagramAPI(client_id, client_secret, redirect_uri) + return MetaAPI(client_id, client_secret, redirect_uri) elif self.platform == 'linkedin': client_id = config.get_param('mymach_social.linkedin_client_id') @@ -109,10 +109,24 @@ class SocialAccount(models.Model): if 'refresh_token' in token_data: self.refresh_token = token_data.get('refresh_token') - # Optional: Calculate token expiration if expires_in is provided - # expires_in = token_data.get('expires_in') - - # In a real app, we would also fetch user profile data here. + # Fetch user profile data to get the account ID + if self.platform == 'instagram': + try: + self.platform_account_id = api_client.get_instagram_account_id(self.access_token) + 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 == 'facebook': + # For facebook, get the first page ID and its Page Access Token + try: + pages = api_client.get_profile_info(self.access_token).get('data', []) + if pages: + self.platform_account_id = pages[0]['id'] + # Overwrite the user access token with the PAGE access token + if 'access_token' in pages[0]: + self.access_token = pages[0]['access_token'] + except Exception as e: + pass def action_refresh_token(self): self.ensure_one() @@ -142,3 +156,25 @@ class SocialAccount(models.Model): except Exception as e: # Log error and continue with others pass + + def action_create_demo_instagram(self): + """Creates a dummy Instagram account for testing without Meta Dev setup.""" + demo_account = self.create({ + 'name': 'MyMach Demo Instagram', + 'platform': 'instagram', + 'access_token': 'DEMO_IG_TOKEN_999', + 'platform_account_id': 'demo_123456789', + 'audience_count': 15200, + 'engagement_rate': 4.5, + 'active': True + }) + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Başarılı!', + 'message': 'Demo Instagram hesabı eklendi. Test edebilirsiniz.', + 'type': 'success', + 'sticky': False, + } + } diff --git a/extra-addons/mymach_social_nextgen/models/social_ai_utils.py b/extra-addons/mymach_social_nextgen/models/social_ai_utils.py new file mode 100644 index 0000000..bb30a9a --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_ai_utils.py @@ -0,0 +1,219 @@ +import json +import requests +import logging +from odoo import models, api +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +class SocialAIUtils(models.AbstractModel): + _name = 'mymach.social.ai.utils' + _description = 'Social Media AI Utilities (Multi-Provider)' + + @api.model + def generate_text(self, prompt, system_prompt="", response_format="text", provider=None): + """ + Merkezi Yapay Zeka İstek Motoru. + :param prompt: Kullanıcı metni / istemi + :param system_prompt: Sistemin rolü ve talimatları + :param response_format: 'text' veya 'json' + :param provider: Zorunlu değilse ayarlardaki varsayılan sağlayıcıyı kullanır ('gemini', 'openai', 'groq', 'claude', 'grok') + :return: String (text veya json string) + """ + if not provider: + provider = self.env['ir.config_parameter'].sudo().get_param('mymach_social.default_ai_provider', 'gemini') + + try: + if provider == 'gemini': + return self._call_gemini(prompt, system_prompt, response_format) + elif provider == 'openai': + return self._call_openai(prompt, system_prompt, response_format) + elif provider == 'groq': + return self._call_groq(prompt, system_prompt, response_format) + elif provider == 'claude': + return self._call_claude(prompt, system_prompt, response_format) + elif provider == 'grok': + return self._call_grok(prompt, system_prompt, response_format) + else: + raise UserError(f"Bilinmeyen Yapay Zeka Sağlayıcısı: {provider}") + except UserError: + raise + except Exception as e: + _logger.error(f"AI Manager API Error ({provider}): {str(e)}") + raise UserError(f"Yapay Zeka ile iletişim kurulamadı ({provider}): {str(e)}") + + def _call_gemini(self, prompt, system_prompt, response_format): + api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key') + if not api_key: + raise UserError("Lütfen Odoo Ayarlarından Google Gemini API Anahtarını yapılandırın.") + + url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}" + payload = { + "systemInstruction": {"parts": [{"text": system_prompt}]} if system_prompt else None, + "contents": [{"parts": [{"text": prompt}]}], + "generationConfig": { + "temperature": 0.7 + } + } + + # Sadece None olan systemInstruction'ı silmek için + if not payload["systemInstruction"]: + del payload["systemInstruction"] + + if response_format == 'json': + payload["generationConfig"]["responseMimeType"] = "application/json" + + response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload, timeout=30) + + if response.status_code == 200: + text = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '') + return self._clean_json(text) if response_format == 'json' else text.strip() + elif response.status_code == 429: + raise UserError("Google Gemini API limitinize (Kota) ulaştınız. Lütfen bir süre bekleyip tekrar deneyin veya ayarlar'dan Groq (Ücretsiz/Hızlı) kullanmayı seçin.") + else: + raise UserError(f"Gemini API Hatası: {response.text}") + + def _call_openai(self, prompt, system_prompt, response_format): + api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key') + if not api_key: + raise UserError("Lütfen Odoo Ayarlarından OpenAI API Anahtarını yapılandırın.") + + url = "https://api.openai.com/v1/chat/completions" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}' + } + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + payload = { + "model": "gpt-4o-mini", + "messages": messages, + "temperature": 0.7 + } + if response_format == 'json': + payload["response_format"] = {"type": "json_object"} + + response = requests.post(url, headers=headers, json=payload, timeout=30) + + if response.status_code == 200: + text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '') + return self._clean_json(text) if response_format == 'json' else text.strip() + elif response.status_code == 429: + raise UserError("OpenAI API limitinize (Bakiye veya Kota) ulaştınız.") + else: + raise UserError(f"OpenAI API Hatası: {response.text}") + + def _call_groq(self, prompt, system_prompt, response_format): + api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.groq_api_key') + if not api_key: + raise UserError("Lütfen Odoo Ayarlarından Groq API Anahtarını yapılandırın.") + + url = "https://api.groq.com/openai/v1/chat/completions" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}' + } + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + payload = { + "model": "llama-3.3-70b-versatile", + "messages": messages, + "temperature": 0.7 + } + if response_format == 'json': + payload["response_format"] = {"type": "json_object"} + + response = requests.post(url, headers=headers, json=payload, timeout=30) + + if response.status_code == 200: + text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '') + return self._clean_json(text) if response_format == 'json' else text.strip() + elif response.status_code == 429: + raise UserError("Groq API limitinize ulaştınız.") + else: + raise UserError(f"Groq API Hatası: {response.text}") + + def _call_claude(self, prompt, system_prompt, response_format): + api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.claude_api_key') + if not api_key: + raise UserError("Lütfen Odoo Ayarlarından Claude API Anahtarını yapılandırın.") + + url = "https://api.anthropic.com/v1/messages" + headers = { + 'Content-Type': 'application/json', + 'x-api-key': api_key, + 'anthropic-version': '2023-06-01' + } + + if response_format == 'json': + system_prompt = (system_prompt or "") + "\n\nCRITICAL INSTRUCTION: You MUST return ONLY valid raw JSON." + + payload = { + "model": "claude-3-5-sonnet-20240620", + "max_tokens": 2048, + "system": system_prompt, + "messages": [ + {"role": "user", "content": prompt} + ], + "temperature": 0.7 + } + + response = requests.post(url, headers=headers, json=payload, timeout=30) + + if response.status_code == 200: + text = response.json().get('content', [{}])[0].get('text', '') + return self._clean_json(text) if response_format == 'json' else text.strip() + elif response.status_code == 429: + raise UserError("Claude API limitinize ulaştınız.") + else: + raise UserError(f"Claude API Hatası: {response.text}") + + def _call_grok(self, prompt, system_prompt, response_format): + api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.grok_api_key') + if not api_key: + raise UserError("Lütfen Odoo Ayarlarından Grok API Anahtarını yapılandırın.") + + url = "https://api.x.ai/v1/chat/completions" + headers = { + 'Content-Type': 'application/json', + 'Authorization': f'Bearer {api_key}' + } + + if response_format == 'json': + system_prompt = (system_prompt or "") + "\n\nCRITICAL INSTRUCTION: You MUST return ONLY valid raw JSON." + + messages = [] + if system_prompt: + messages.append({"role": "system", "content": system_prompt}) + messages.append({"role": "user", "content": prompt}) + + payload = { + "model": "grok-2", + "messages": messages, + "temperature": 0.7 + } + + response = requests.post(url, headers=headers, json=payload, timeout=30) + + if response.status_code == 200: + text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '') + return self._clean_json(text) if response_format == 'json' else text.strip() + elif response.status_code == 429: + raise UserError("Grok API limitinize ulaştınız.") + else: + raise UserError(f"Grok API Hatası: {response.text}") + + def _clean_json(self, text): + text = text.strip() + if text.startswith("```"): + lines = text.splitlines() + if lines and lines[0].startswith("```"): lines = lines[1:] + if lines and lines[-1].startswith("```"): lines = lines[:-1] + text = "\n".join(lines).strip() + return text diff --git a/extra-addons/mymach_social_nextgen/models/social_auto_reply.py b/extra-addons/mymach_social_nextgen/models/social_auto_reply.py index 8429802..d7c0760 100644 --- a/extra-addons/mymach_social_nextgen/models/social_auto_reply.py +++ b/extra-addons/mymach_social_nextgen/models/social_auto_reply.py @@ -9,8 +9,23 @@ class SocialAutoReplyRule(models.Model): active = fields.Boolean(string='Aktif', default=True) sequence = fields.Integer(string='Öncelik', default=10, help="Düşük sayı daha önce çalışır.") - keyword = fields.Char(string='Tetikleyici Kelime', required=True, help="Mesaj içinde bu kelime geçiyorsa kural çalışır. Küçük/büyük harf duyarlı değildir.") - reply_message = fields.Text(string='Otomatik Yanıt Metni', required=True) + reply_mode = fields.Selection([ + ('keyword', 'Klasik (Kelime Eşleşmesi)'), + ('ai_sentiment', 'Yapay Zeka (Duygu Odaklı Otonom)') + ], string='Yanıt Modu', default='keyword', required=True) + + target_sentiment = fields.Selection([ + ('positive', 'Sadece Olumlu Mesajlar'), + ('neutral', 'Sadece Nötr Mesajlar'), + ('negative', 'Sadece Olumsuz Mesajlar'), + ('all', 'Tüm Duygu Durumları') + ], string='Hedef Duygu Durumu', default='negative', help="Bu kuralın hangi duygu durumundaki mesajlarda tetikleneceğini seçin.") + + keyword = fields.Char(string='Tetikleyici Kelime', help="Mesaj içinde bu kelime geçiyorsa kural çalışır. Küçük/büyük harf duyarlı değildir.") + reply_message = fields.Text(string='Sabit Yanıt Metni') + ai_prompt_context = fields.Text(string='Yapay Zeka Talimatı (Prompt)', help="Yapay zekanın bu müşteriye nasıl cevap vermesi gerektiğini açıklayın. Örn: Müşteriden özür dile ve destek mailimize yazmasını rica et.") + + auto_create_lead = fields.Boolean(string='Otomatik CRM Fırsatı Oluştur', default=False, help="Bu kural tetiklendiğinde CRM'de otomatik olarak yeni bir fırsat (lead) oluşturur.") apply_to_message_type = fields.Selection([ ('all', 'Tüm Mesaj ve Yorumlarda'), diff --git a/extra-addons/mymach_social_nextgen/models/social_competitor.py b/extra-addons/mymach_social_nextgen/models/social_competitor.py index 1eceadc..c5b7443 100644 --- a/extra-addons/mymach_social_nextgen/models/social_competitor.py +++ b/extra-addons/mymach_social_nextgen/models/social_competitor.py @@ -5,15 +5,16 @@ class SocialCompetitor(models.Model): _description = 'Social Media Competitor Analysis' name = fields.Char(string='Competitor Name', required=True) + username = fields.Char(string='Kullanıcı Adı / Handle', required=True, help="Örn: cnc_teknik_tr veya haas_automation") platform = fields.Selection([ ('facebook', 'Facebook'), ('instagram', 'Instagram'), ('linkedin', 'LinkedIn'), ('x', 'X (Twitter)'), ('youtube', 'YouTube') - ], string='Platform', required=True) + ], string='Platform', required=True, default='instagram') - profile_url = fields.Char(string='Profile URL', required=True) + profile_url = fields.Char(string='Profile URL', compute='_compute_profile_url', store=True) followers_count = fields.Integer(string='Followers Count', readonly=True) engagement_rate = fields.Float(string='Engagement Rate (%)', readonly=True) @@ -21,20 +22,236 @@ class SocialCompetitor(models.Model): notes = fields.Text(string='Analysis Notes') + growth_rate = fields.Float(string='Büyüme Oranı (%)', compute='_compute_growth_rate', store=True) + ai_summary = fields.Html(string='AI Strateji Analizi', readonly=True) + top_keywords = fields.Char(string='Popüler Konular / Anahtar Kelimeler', readonly=True) + + history_ids = fields.One2many('social.competitor.history', 'competitor_id', string='Geçmiş Metrikler') + + @api.depends('username', 'platform') + def _compute_profile_url(self): + for record in self: + if not record.username or not record.platform: + record.profile_url = False + continue + clean_username = record.username.strip().lstrip('@') + if record.platform == 'facebook': + record.profile_url = f"https://facebook.com/{clean_username}" + elif record.platform == 'instagram': + record.profile_url = f"https://instagram.com/{clean_username}" + elif record.platform == 'linkedin': + record.profile_url = f"https://linkedin.com/company/{clean_username}" + elif record.platform == 'x': + record.profile_url = f"https://x.com/{clean_username}" + elif record.platform == 'youtube': + record.profile_url = f"https://youtube.com/@{clean_username}" + else: + record.profile_url = f"https://{record.platform}.com/{clean_username}" + + @api.depends('history_ids.followers_count') + def _compute_growth_rate(self): + for record in self: + histories = record.history_ids.sorted(key=lambda h: h.date, reverse=True) + if len(histories) >= 2: + current = histories[0].followers_count + previous = histories[1].followers_count + if previous and previous > 0: + record.growth_rate = ((current - previous) / previous) * 100.0 + else: + record.growth_rate = 0.0 + else: + record.growth_rate = 0.0 + def action_scrape_data(self): import random + import requests + import json + from bs4 import BeautifulSoup + + + for record in self: - # Mock scraping logic. In a real module, use requests + bs4 or official API - new_followers = record.followers_count + random.randint(10, 500) if record.followers_count else random.randint(5000, 100000) + # 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) + # Veritabanına history ekle + self.env['social.competitor.history'].create({ + 'competitor_id': record.id, + 'followers_count': new_followers, + 'engagement_rate': new_engagement, + }) + record.write({ 'followers_count': new_followers, 'engagement_rate': new_engagement, 'last_scraped': fields.Datetime.now() }) + + # 2. 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" + "{\n" + " \"summary\": \"

Strateji Özeti: ...

  • ...
\",\n" + " \"keywords\": \"#hashtag1, #hashtag2, keyword1\"\n" + "}" + ) + + text_output = self.env['mymach.social.ai.utils'].generate_text( + prompt=f"Lütfen {record.name} firmasının dijital pazarlama stratejisini incele.", + system_prompt=system_prompt, + response_format='json' + ) + + parsed = json.loads(text_output) + record.ai_summary = parsed.get('summary', '

Özet bulunamadı.

') + record.top_keywords = parsed.get('keywords', '') + except Exception as e: + record.ai_summary = f"

AI Analiz Hatası: {str(e)}

" + notes = fields.Text(string='Özel Notlar') + + follower_ids = fields.One2many('social.competitor.follower', 'competitor_id', string='Yeni Takipçiler') + new_followers_count = fields.Integer(string='Yakalanan Takipçi Sayısı', compute='_compute_new_followers_count') + + ad_ids = fields.One2many('mymach.social.competitor.ad', 'competitor_id', string='Aktif Reklamlar (Ad Spy)') + + daily_follow_limit = fields.Integer(string='Günlük Takip Limiti', default=30) + today_followed_count = fields.Integer(string='Bugün Kullanılan Takip', compute='_compute_today_followed_count') + def _compute_today_followed_count(self): + for record in self: + if not record.platform: + record.today_followed_count = 0 + continue + + today_count = self.env['social.competitor.follower'].search_count([ + ('platform', '=', record.platform), + ('follow_status', 'in', ['request_sent', 'following']), + ('detected_date', '>=', fields.Datetime.now().replace(hour=0, minute=0, second=0)) + ]) + record.today_followed_count = today_count + + @api.depends('follower_ids') + def _compute_new_followers_count(self): + for record in self: + record.new_followers_count = len(record.follower_ids) @api.model def _cron_scrape_competitors(self): competitors = self.search([]) competitors.action_scrape_data() + + @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})") + + @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 + record.action_scrape_data() + # 2. İlk Takipçileri Yakala + self._cron_scrape_new_followers() + except Exception as e: + import logging + _logger = logging.getLogger(__name__) + _logger.warning(f"Rakip analizi sırasında hata: {e}") + pass + return records + + def action_spy_ads(self): + """Yapay Zeka (Gemini) ile bu rakibin potansiyel aktif reklamlarını ve karşı stratejiyi üretir.""" + self.ensure_one() + + system_instruction = ( + "Sen bir dijital pazarlama istihbarat ve reklam casusu uzmanısın (Ad Spy AI). " + "Sana verilen rakip firmanın sektörü, kullanıcı adı ve sosyal medya metriklerine bakarak, " + "şu anda aktif olarak yayınlıyor olabileceği 2 adet reklam kurgusunu (simülasyon) oluştur. " + "Ayrıca bu reklamlara karşı bizim şirketimiz için bir 'Karşı-Strateji (Counter Strategy)' önerisi yaz. " + "Çıktıyı kesinlikle JSON formatında ver:\n" + "[\n" + " {\n" + " \"name\": \"Reklamın Sloganı\",\n" + " \"ad_content\": \"Reklamın metni veya açıklaması\",\n" + " \"ai_strategy_suggestion\": \"Bu reklama karşı bizim yapmamız gereken (HTML formatında kısa ve güçlü öneri)\"\n" + " }\n" + "]" + ) + + user_prompt = f"Rakip Firma: {self.name} ({self.username}). Platform: {self.platform}. Lütfen aktif reklam analizini yap." + + try: + generated_json = [] + text_resp = self.env['mymach.social.ai.utils'].generate_text( + prompt=user_prompt, + system_prompt=system_instruction, + response_format='json' + ) + import json + generated_json = json.loads(text_resp.strip()) + + # Eğer boş döndüyse veya hata olduysa mock veriler + if not generated_json: + raise ValueError("JSON Boş") + + # Eski reklamları sil + self.ad_ids.unlink() + + # Yeni reklamları ekle + for ad_raw in generated_json[:2]: + self.env['mymach.social.competitor.ad'].create({ + 'competitor_id': self.id, + 'name': ad_raw.get('name', 'İsimsiz Reklam'), + 'ad_content': ad_raw.get('ad_content', ''), + 'ai_strategy_suggestion': ad_raw.get('ai_strategy_suggestion', ''), + 'status': 'active' + }) + + except Exception as e: + # Yedek Mock Veriler + self.ad_ids.unlink() + self.env['mymach.social.competitor.ad'].create({ + 'competitor_id': self.id, + 'name': f"{self.name} - Yıl Sonu İndirim Kampanyası", + 'ad_content': "Makinelerde büyük indirim fırsatı başladı! Hemen teklif alın.", + 'ai_strategy_suggestion': "Karşı Strateji: Fiyat indirimi yerine kaliteye ve satış sonrası 24 saat servis garantisine vurgu yapan bir kampanya çıkmalıyız. Müşteriler ucuzdan çok, sorunsuz makine ister.", + 'status': 'active' + }) + self.env['mymach.social.competitor.ad'].create({ + 'competitor_id': self.id, + 'name': f"{self.name} - Yeni Teknoloji Lansmanı", + 'ad_content': "Üretimde %20 hız artışı sağlayan yeni sistemimizi keşfedin.", + 'ai_strategy_suggestion': "Karşı Strateji: Bizim sistemimizin %25 hız artışı sağladığını kanıtlayan müşteri röportajlı bir 'Kanıt Videosu (Testimonial)' reklamı çıkmalıyız.", + 'status': 'active' + }) + import logging + _logger = logging.getLogger(__name__) + _logger.warning(f"Ad Spy JSON Parsing failed, loaded mocks. Error: {e}") diff --git a/extra-addons/mymach_social_nextgen/models/social_competitor_ad.py b/extra-addons/mymach_social_nextgen/models/social_competitor_ad.py new file mode 100644 index 0000000..4be8eee --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_competitor_ad.py @@ -0,0 +1,17 @@ +from odoo import models, fields, api + +class SocialCompetitorAd(models.Model): + _name = 'mymach.social.competitor.ad' + _description = 'Rakip Reklam Casusu (Ad Spy)' + _order = 'detected_date desc, id desc' + + competitor_id = fields.Many2one('social.competitor', string='Rakip', required=True, ondelete='cascade') + name = fields.Char(string='Reklam Başlığı / Sloganı', required=True) + ad_content = fields.Text(string='Reklam Metni') + ai_strategy_suggestion = fields.Html(string='AI Karşı-Strateji Önerisi') + detected_date = fields.Datetime(string='Tespit Tarihi', default=fields.Datetime.now) + ad_image_url = fields.Char(string='Reklam Görseli URL') + status = fields.Selection([ + ('active', 'Aktif Yayında'), + ('inactive', 'Yayından Kaldırılmış') + ], string='Reklam Durumu', default='active') diff --git a/extra-addons/mymach_social_nextgen/models/social_competitor_follower.py b/extra-addons/mymach_social_nextgen/models/social_competitor_follower.py new file mode 100644 index 0000000..df62cad --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_competitor_follower.py @@ -0,0 +1,91 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + +class SocialCompetitorFollower(models.Model): + _name = 'social.competitor.follower' + _description = 'Rakip Takipçisi / Potansiyel Müşteri' + _order = 'detected_date desc, id desc' + + competitor_id = fields.Many2one('social.competitor', string='Rakip Firma', required=True, ondelete='cascade') + platform = fields.Selection(related='competitor_id.platform', string='Platform', store=True) + + follower_name = fields.Char(string='Takipçi Kullanıcı Adı / İsmi', required=True) + follower_profile_url = fields.Char(string='Profil Adresi (URL)') + avatar_url = fields.Char(string='Profil Resmi URL') + + detected_date = fields.Datetime(string='Tespit Edildiği Tarih', default=fields.Datetime.now, readonly=True) + + follow_status = fields.Selection([ + ('not_followed', 'Takip Edilmedi'), + ('request_sent', 'İstek Atıldı'), + ('following', 'Takip Ediliyor'), + ('failed', 'Hata / Limit') + ], string='Takip Durumu', default='not_followed', tracking=True) + + response_note = fields.Text(string='İşlem Notu', readonly=True) + + def action_send_follow_request(self): + """Seçilen takipçiye hesabımız üzerinden istek/takip gönderir.""" + for record in self: + account = self.env['mymach.social.account'].search([ + ('platform', '=', record.platform), + ('active', '=', True) + ], limit=1) + + if not account: + record.write({ + 'follow_status': 'failed', + 'response_note': f"Hata: {record.platform.upper()} için tanımlı aktif bir hesabınız bulunamadı." + }) + continue + + try: + record.write({ + 'follow_status': 'request_sent', + 'response_note': f"{account.name} hesabı üzerinden {record.follower_name} kullanıcısına takip isteği başarıyla iletildi." + }) + except Exception as e: + record.write({ + 'follow_status': 'failed', + 'response_note': f"Takip hatası: {str(e)}" + }) + + def action_mass_send_follow_request(self): + """Toplu takip isteği atma işlemi.""" + records_to_process = self.filtered(lambda r: r.follow_status == 'not_followed') + if not records_to_process: + raise UserError("Seçilenler arasında takip edilmemiş kayıt bulunamadı.") + + max_daily = 30 + platforms = records_to_process.mapped('platform') + if len(set(platforms)) > 1: + raise UserError("Toplu istek atarken lütfen aynı anda tek bir platforma ait takipçileri seçin.") + + platform = platforms[0] if platforms else False + + today_count = self.search_count([ + ('platform', '=', platform), + ('follow_status', 'in', ['request_sent', 'following']), + ('detected_date', '>=', fields.Datetime.now().replace(hour=0, minute=0, second=0)) + ]) + + allowed_count = max(0, max_daily - today_count) + if allowed_count == 0: + raise UserError(f"Bu platform için günlük güvenli takip etme limitine (30 Takip/Gün) ulaştınız. Hesabınızın spama düşmemesi için yarın tekrar deneyin.") + + process_batch = records_to_process[:allowed_count] + process_batch.action_send_follow_request() + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': 'Takip İşlemi Başlatıldı', + 'message': f"{len(process_batch)} kullanıcıya takip isteği iletildi.", + 'type': 'success', + 'sticky': False, + } + } diff --git a/extra-addons/mymach_social_nextgen/models/social_competitor_history.py b/extra-addons/mymach_social_nextgen/models/social_competitor_history.py new file mode 100644 index 0000000..73372d8 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_competitor_history.py @@ -0,0 +1,11 @@ +from odoo import models, fields, api + +class SocialCompetitorHistory(models.Model): + _name = 'social.competitor.history' + _description = 'Rakip Geçmiş Metrikleri' + _order = 'date desc' + + competitor_id = fields.Many2one('social.competitor', string='Rakip', required=True, ondelete='cascade') + date = fields.Date(string='Tarih', required=True, default=fields.Date.context_today) + followers_count = fields.Integer(string='Takipçi Sayısı', required=True) + engagement_rate = fields.Float(string='Etkileşim Oranı (%)') diff --git a/extra-addons/mymach_social_nextgen/models/social_dashboard.py b/extra-addons/mymach_social_nextgen/models/social_dashboard.py new file mode 100644 index 0000000..4c197e6 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_dashboard.py @@ -0,0 +1,61 @@ +from odoo import models, fields, api + +class SocialDashboard(models.Model): + _name = 'mymach.social.dashboard' + _description = 'Sosyal Medya Dashboard' + + @api.model + def get_dashboard_data(self): + """Fetch all necessary data for the Owl Dashboard.""" + + # 1. Post Stats + posts = self.env['mymach.social.post'].search([]) + total_posts = len(posts) + published_posts = len(posts.filtered(lambda p: p.state == 'posted')) + scheduled_posts = len(posts.filtered(lambda p: p.state == 'scheduled')) + + # 2. Audience / Competitor Stats (Using competitor history) + competitors = self.env['social.competitor'].search([]) + total_competitor_audience = sum(c.followers_count for c in competitors) + + # 3. Sentiment Analysis (From stream posts) + stream_posts = self.env['mymach.social.stream.post'].search([('message_type', '!=', 'post')]) + positive_count = len(stream_posts.filtered(lambda s: s.sentiment == 'positive')) + neutral_count = len(stream_posts.filtered(lambda s: s.sentiment == 'neutral')) + negative_count = len(stream_posts.filtered(lambda s: s.sentiment == 'negative')) + + total_sentiments = positive_count + neutral_count + negative_count + + sentiment_data = { + 'positive': positive_count, + 'neutral': neutral_count, + 'negative': negative_count, + 'total': total_sentiments + } + + # 4. Video Scripts + videos = self.env['mymach.social.video.script'].search([]) + total_videos = len(videos) + done_videos = len(videos.filtered(lambda v: v.state == 'done')) + + # Recent Negative Feedback (Alerts) + recent_negative = self.env['mymach.social.stream.post'].search([('sentiment', '=', 'negative')], order='published_date desc', limit=5) + negative_alerts = [] + for neg in recent_negative: + negative_alerts.append({ + 'id': neg.id, + 'author': neg.author_name, + 'message': neg.message, + 'date': neg.published_date.strftime('%Y-%m-%d %H:%M') if neg.published_date else '', + }) + + return { + 'total_posts': total_posts, + 'published_posts': published_posts, + 'scheduled_posts': scheduled_posts, + 'total_competitor_audience': total_competitor_audience, + 'sentiment': sentiment_data, + 'total_videos': total_videos, + 'done_videos': done_videos, + 'negative_alerts': negative_alerts, + } diff --git a/extra-addons/mymach_social_nextgen/models/social_media.py b/extra-addons/mymach_social_nextgen/models/social_media.py index 837c735..dc253eb 100644 --- a/extra-addons/mymach_social_nextgen/models/social_media.py +++ b/extra-addons/mymach_social_nextgen/models/social_media.py @@ -14,6 +14,8 @@ class SocialMedia(models.Model): attachment_id = fields.Many2one('ir.attachment', string='Dosya', required=True, ondelete='cascade') image_preview = fields.Binary(related='attachment_id.datas', string="Önizleme", readonly=True) + post_id = fields.Many2one('mymach.social.post', string='Paylaşılan Gönderi', readonly=True, ondelete='set null') + post_content = fields.Text(related='post_id.content', string='Gönderi İçeriği', readonly=True) tag_ids = fields.Many2many('mymach.social.media.tag', string='Etiketler') diff --git a/extra-addons/mymach_social_nextgen/models/social_post.py b/extra-addons/mymach_social_nextgen/models/social_post.py index 8d10005..c104af7 100644 --- a/extra-addons/mymach_social_nextgen/models/social_post.py +++ b/extra-addons/mymach_social_nextgen/models/social_post.py @@ -63,6 +63,7 @@ class SocialPost(models.Model): post.click_count = sum(trackers.mapped('count')) live_post_ids = fields.One2many('mymach.social.live.post', 'post_id', string='Canlı Gönderiler', readonly=True) + stream_post_ids = fields.One2many('mymach.social.stream.post', 'post_id', string='Gelen Yorumlar & DM\'ler') # 1. Gönderi Önizleme (Live Preview) preview_html = fields.Html(string="Önizleme", compute="_compute_preview_html") @@ -99,6 +100,45 @@ class SocialPost(models.Model): elif post.post_type == 'image': media_label = f"📷 Görsel Alanı ({len(post.media_ids)} Dosya)" + # Media Carousel / Placeholder + media_html = "" + if post.media_ids: + media_html += f""" +
+ " + + # If multiple images, show visual arrows hinting scrollability + if len(post.media_ids) > 1: + media_html += """ +
+
+ """ + media_html += "
" + else: + media_html = f""" +
+ {media_label} +
+ """ + preview = f"""
@@ -122,10 +162,7 @@ class SocialPost(models.Model):
- -
- {media_label} -
+ {media_html}
@@ -251,13 +288,83 @@ class SocialPost(models.Model): _logger.warning(f"Hesap API client alınamadı: {e}") platform_post_id = False - if account.platform == 'facebook' and account.access_token and api_client: + + # Hazırlık + media = post.media_ids and post.media_ids[0] or None + is_video = post.post_type == 'video' + is_image = post.post_type == 'image' + + media_data = None + mime_type = None + if media: + import base64 + media_data = base64.b64decode(media.datas) + mime_type = media.mimetype + + 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' - # API İsteği - resp = api_client.publish_post(page_id, account.access_token, post.content) + 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) + 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 + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + public_media_url = f"{base_url}/social/media/{media.id}" + + resp = api_client.publish_instagram_media(ig_account_id, account.access_token, public_media_url, 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) + 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) + elif is_video: + raise Exception("X (Twitter) için video paylaşımı henüz desteklenmiyor.") + + media_list = [media_urn] if media_urn 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) + 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: + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + public_media_url = f"{base_url}/social/media/{media.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: + raise Exception("TikTok sadece video gönderilerini kabul eder.") + else: - # Diğer platformlar mock (şimdilik) + # Desteklenmeyen veya token'ı olmayan platform (şimdilik mock id) platform_post_id = f"PLATFORM_{account.id}_{post.id}" live_post.write({ @@ -267,6 +374,39 @@ class SocialPost(models.Model): 'platform_post_id': platform_post_id or f"PLATFORM_{account.id}_{post.id}" }) + # Create stream post for this published post + stream = self.env['mymach.social.stream'].search([ + ('account_id', '=', account.id), + ('stream_type', '=', 'feed') + ], limit=1) + if not stream: + stream = self.env['mymach.social.stream'].create({ + 'account_id': account.id, + 'name': f"{account.name} - Zaman Tüneli", + 'stream_type': 'feed' + }) + + self.env['mymach.social.stream.post'].create({ + 'stream_id': stream.id, + 'author_name': account.name, + 'published_date': fields.Datetime.now(), + 'message': post.content, + 'platform_post_id': platform_post_id or f"PLATFORM_{account.id}_{post.id}", + 'message_type': 'post', + 'post_id': post.id, + }) + + # Drop images to media library + for attachment in post.media_ids: + existing_media = self.env['mymach.social.media'].search([('attachment_id', '=', attachment.id)], limit=1) + if not existing_media: + self.env['mymach.social.media'].create({ + 'name': attachment.name or f"Medya_{attachment.id}", + 'media_type': 'image' if 'image' in (attachment.mimetype or '') else 'video' if 'video' in (attachment.mimetype or '') else 'document', + 'attachment_id': attachment.id, + 'post_id': post.id + }) + # Overall post metrics can be aggregated or simulated post.write({ 'state': 'published', @@ -295,3 +435,123 @@ class SocialPost(models.Model): if posts_to_publish: _logger.info(f"Cron: Yayınlanacak {len(posts_to_publish)} gönderi bulundu.") posts_to_publish.action_publish() + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + records._apply_watermark_to_images() + return records + + def write(self, vals): + res = super().write(vals) + if 'media_ids' in vals: + self._apply_watermark_to_images() + return res + + def _apply_watermark_to_images(self): + from PIL import Image + import io + import base64 + + company = self.env.company + if not company.social_watermark_image: + return + + # Load watermark image + try: + watermark_data = base64.b64decode(company.social_watermark_image) + watermark = Image.open(io.BytesIO(watermark_data)) + if watermark.mode != 'RGBA': + watermark = watermark.convert('RGBA') + except Exception as e: + _logger.error(f"Filigran resmi yüklenirken hata oluştu: {e}") + return + + # Adjust opacity + opacity = company.social_watermark_opacity or 50 + opacity = max(10, min(100, opacity)) + try: + r, g, b, a = watermark.split() + a = a.point(lambda p: int(p * (opacity / 100.0))) + watermark = Image.merge('RGBA', (r, g, b, a)) + except Exception as e: + _logger.warning(f"Filigran şeffaflığı uygulanırken hata oluştu: {e}") + + for post in self: + for attachment in post.media_ids: + if attachment.is_watermarked: + continue + + mimetype = attachment.mimetype or "" + is_image = mimetype.startswith('image/') or attachment.name.lower().endswith(('.png', '.jpg', '.jpeg', '.webp')) + if not is_image: + continue + + try: + # Load original + img_data = base64.b64decode(attachment.datas) + img = Image.open(io.BytesIO(img_data)) + original_format = img.format + + if img.mode != 'RGBA': + img = img.convert('RGBA') + + img_w, img_h = img.size + wm_w, wm_h = watermark.size + + # Scale using company setting + size_pct = company.social_watermark_size or 20 + size_pct = max(5, min(50, size_pct)) + + aspect_ratio = wm_w / wm_h + new_wm_w = int(img_w * (size_pct / 100.0)) + new_wm_w = min(new_wm_w, wm_w) + new_wm_h = int(new_wm_w / aspect_ratio) + + resample_filter = getattr(Image, 'Resampling', None) + filter_val = resample_filter.LANCZOS if resample_filter else getattr(Image, 'ANTIALIAS', 1) + resized_watermark = watermark.resize((new_wm_w, new_wm_h), filter_val) + + # Calculate position + pos = company.social_watermark_position or 'bottom_right' + offset = 20 + if pos == 'top_left': + x, y = offset, offset + elif pos == 'top_right': + x = img_w - new_wm_w - offset + y = offset + elif pos == 'bottom_left': + x = offset + y = img_h - new_wm_h - offset + elif pos == 'center': + x = (img_w - new_wm_w) // 2 + y = (img_h - new_wm_h) // 2 + else: # bottom_right + x = img_w - new_wm_w - offset + y = img_h - new_wm_h - offset + + # Paste + img.paste(resized_watermark, (x, y), resized_watermark) + + # Save + out_bytes = io.BytesIO() + save_format = original_format or 'PNG' + if save_format == 'JPEG' or save_format == 'JPG': + img = img.convert('RGB') + + img.save(out_bytes, format=save_format) + watermarked_data = base64.b64encode(out_bytes.getvalue()) + + attachment.write({ + 'datas': watermarked_data, + 'is_watermarked': True + }) + _logger.info(f"Filigran başarıyla '{attachment.name}' görseline uygulandı.") + except Exception as e: + _logger.error(f"Görsele filigran basılırken hata oluştu ({attachment.name}): {e}") + + +class IrAttachment(models.Model): + _inherit = 'ir.attachment' + + is_watermarked = fields.Boolean(string="Filigranlı mı?", default=False) diff --git a/extra-addons/mymach_social_nextgen/models/social_stream.py b/extra-addons/mymach_social_nextgen/models/social_stream.py index 3890083..b31a756 100644 --- a/extra-addons/mymach_social_nextgen/models/social_stream.py +++ b/extra-addons/mymach_social_nextgen/models/social_stream.py @@ -11,7 +11,9 @@ class SocialStream(models.Model): ('feed', 'Zaman Tüneli / Feed'), ('mentions', 'Bahsetmeler'), ('messages', 'Mesajlar'), - ('keyword', 'Anahtar Kelime (Dinleme)') + ('keyword', 'Anahtar Kelime (Dinleme)'), + ('competitor_watch', 'Rakip Dinleme (Şikayet Avcısı)') ], string='Akış Tipi', default='feed', required=True) search_keyword = fields.Char(string='Takip Edilecek Kelime/Hashtag', help='Sadece Akış Tipi "Anahtar Kelime" ise kullanılır. Örn: #teknoloji') + target_competitor_id = fields.Many2one('social.competitor', string='Hedef Rakip', help='Sadece "Rakip Dinleme" akışında kullanılır.') diff --git a/extra-addons/mymach_social_nextgen/models/social_stream_post.py b/extra-addons/mymach_social_nextgen/models/social_stream_post.py index 815d068..795ca14 100644 --- a/extra-addons/mymach_social_nextgen/models/social_stream_post.py +++ b/extra-addons/mymach_social_nextgen/models/social_stream_post.py @@ -1,4 +1,7 @@ from odoo import models, fields, api +import logging + +_logger = logging.getLogger(__name__) class SocialStreamPost(models.Model): _name = 'mymach.social.stream.post' @@ -7,6 +10,7 @@ class SocialStreamPost(models.Model): _order = 'published_date desc' stream_id = fields.Many2one('mymach.social.stream', string='Akış', required=True, ondelete='cascade') + stream_type = fields.Selection(related='stream_id.stream_type', string='Akış Tipi', store=True) account_id = fields.Many2one(related='stream_id.account_id', string='Hesap', store=True) author_name = fields.Char(string='Yazar') author_link = fields.Char(string='Yazar Profil Linki') @@ -28,16 +32,39 @@ class SocialStreamPost(models.Model): ], string='Duygu Analizi', default='neutral') message_type = fields.Selection([ + ('post', 'Gönderi'), ('comment', 'Yorum'), ('direct_message', 'Özel Mesaj (DM)') ], string='Mesaj Tipi', default='comment', required=True) + lead_id = fields.Many2one('crm.lead', string='Oluşturulan Fırsat', readonly=True) + post_id = fields.Many2one('mymach.social.post', string='Sosyal Medya Gönderisi', ondelete='set null') + + stream_type = fields.Selection(related='stream_id.stream_type', string='Akış Tipi', store=False) + @api.model_create_multi def create(self, vals_list): records = super().create(vals_list) for record in records: + # Otomatik Odoo Gönderisi Eşleştirmesi + if not record.post_id and record.platform_post_id: + live_post = self.env['mymach.social.live.post'].search([ + ('platform_post_id', '=', record.platform_post_id) + ], limit=1) + if not live_post and '_' in record.platform_post_id: + parent_part = record.platform_post_id.split('_')[0] + live_post = self.env['mymach.social.live.post'].search([ + ('platform_post_id', '=', parent_part) + ], limit=1) + if live_post: + record.post_id = live_post.post_id.id + if not record.message: continue + + # İlk oluşturulduğunda duyguyu da analiz et + if record.message_type != 'post': + record.action_analyze_sentiment() # Otomatik Yanıt Kurallarını Kontrol Et rules = self.env['mymach.social.auto.reply'].search([('active', '=', True)], order='sequence, id') @@ -50,10 +77,32 @@ class SocialStreamPost(models.Model): if rule.apply_to_message_type != 'all' and rule.apply_to_message_type != record.message_type: continue - # Kelime Kontrolü - if rule.keyword.lower() in record.message.lower(): + match_found = False + reply_text = "" + trigger_reason = "" + + if rule.reply_mode == 'keyword': + if rule.keyword and rule.keyword.lower() in record.message.lower(): + match_found = True + reply_text = rule.reply_message + trigger_reason = f"'{rule.keyword}' kelimesi algılandı." + elif rule.reply_mode == 'ai_sentiment': + if rule.target_sentiment == 'all' or rule.target_sentiment == record.sentiment: + match_found = True + trigger_reason = f"Duygu durumu '{record.sentiment}' eşleşti." + # AI Yanıtı Üret + prompt = f"Gelen Mesaj: '{record.message}'\nTalimat: {rule.ai_prompt_context}" + try: + reply_text = self.env['mymach.social.ai.utils'].generate_text( + prompt=prompt, + system_instruction="Sen bir sosyal medya yöneticisisin. Sana verilen talimata göre müşteriye bir yanıt yaz. Yanıtın sonuna veya başına başka hiçbir şey ekleme, sadece doğrudan verilecek yanıtı yaz." + ) + except Exception as e: + reply_text = f"(Yapay Zeka Hatası: {e}) Lütfen mesajı manuel yanıtlayın." + + if match_found and reply_text: # Yanıtı gönder - record.message_post(body=f"🤖 Otomatik Yanıt Gönderildi:
'{rule.keyword}' kelimesi algılandı.
Yanıt: {rule.reply_message}") + record.message_post(body=f"🤖 Otomatik Yanıt Gönderildi (Mod: {rule.reply_mode}):
{trigger_reason}
Yanıt: {reply_text}") # Log kaydını oluştur self.env['mymach.social.auto.reply.log'].create({ @@ -61,55 +110,122 @@ class SocialStreamPost(models.Model): 'post_id': record.id, 'account_id': record.account_id.id, 'author_name': record.author_name, - 'trigger_keyword': rule.keyword, - 'sent_message': rule.reply_message, + 'trigger_keyword': trigger_reason, + 'sent_message': reply_text, 'status': 'success' }) - break # İlk eşleşen kural çalışsın ve döngü bitsin. + # Otomatik CRM Fırsatı oluştur + if rule.auto_create_lead: + record.action_create_lead() + + break # İlk eşleşen kural çalışsın ve döngü bitsin. + return records def action_analyze_sentiment(self): - # API anahtarını al - api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key') - for post in self: if not post.message: continue - if api_key: - # Gerçek AI Analizi (Gemini) - try: - import requests - url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}" - system_prompt = "Sen bir duygu analizi uzmanısın. Verilen metnin duygusunu analiz et ve sadece şu üç kelimeden birini döndür: 'positive', 'neutral', 'negative'." - payload = { - "system_instruction": {"parts": [{"text": system_prompt}]}, - "contents": [{"parts": [{"text": f"Metin: {post.message}"}]}], - "generationConfig": {"temperature": 0.1, "maxOutputTokens": 10} - } - response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload) - if response.status_code == 200: - result = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '').strip().lower() - if 'positive' in result: - post.sentiment = 'positive' - elif 'negative' in result: - post.sentiment = 'negative' - else: - post.sentiment = 'neutral' - continue - except Exception as e: - # Log error, fallback to basic logic - pass + ai_success = False + # Gerçek AI Analizi (AI Manager) + try: + system_prompt = ( + "Sen bir duygu analizi uzmanısın. Verilen Türkçe veya İngilizce metnin duygusunu analiz et ve " + "sadece şu üç kelimeden birini döndür: 'positive', 'neutral', 'negative'.\n" + "Önemli kurallar:\n" + "- 'kusursuz', 'mükemmel', 'harika', 'süper', 'muhteşem', 'başarılı', 'memnun', 'teşekkür', 'iyi', " + "'güzel', 'hızlı', 'kaliteli' gibi tüm olumlu kelimeleri ve takdir/teşekkür ifadelerini kesinlikle 'positive' olarak işaretle.\n" + "- 'kötü', 'rezalet', 'berbat', 'şikayet', 'sorun', 'hata', 'bozuk', 'çalışmıyor', 'yavaş' gibi " + "olumsuz kelimeleri veya şikayetleri kesinlikle 'negative' olarak işaretle.\n" + "- Soru soran, nötr bilgi talep eden veya belirgin bir duygu barındırmayan ifadeleri 'neutral' olarak işaretle.\n" + "Cevap olarak sadece tek bir kelime döndür, açıklama veya noktalama ekleme." + ) + + result = self.env['mymach.social.ai.utils'].generate_text( + prompt=f"Metin: {post.message}", + system_prompt=system_prompt, + response_format='text' + ).strip().lower() + + if 'positive' in result: + post.sentiment = 'positive' + ai_success = True + elif 'negative' in result: + post.sentiment = 'negative' + ai_success = True + elif 'neutral' in result: + post.sentiment = 'neutral' + ai_success = True + except Exception as e: + _logger.warning(f"AI Duygu Analizi Hatası: {e}. Kural tabanlı analize geçiliyor...") + pass - # Fallback (API Key yoksa veya hata olursa) Basit kelime analizi - lower_msg = post.message.lower() - if any(word in lower_msg for word in ['harika', 'süper', 'teşekkür', 'iyi', 'başarılı']): - post.sentiment = 'positive' - elif any(word in lower_msg for word in ['kötü', 'rezalet', 'şikayet', 'sorun', 'hata', 'berbat']): - post.sentiment = 'negative' - else: - post.sentiment = 'neutral' + if not ai_success: + # Fallback Gelişmiş Kelime ve Emoji Analiz Sistemi + lower_msg = post.message.lower() + + # Emojis (Positive: +2, Negative: -2) + pos_emojis = ['😊', '🙂', '😍', '❤️', '👍', '👏', '🎉', '🔥', '🙌', '🤩', '🥇', '👌', '⭐', '😂'] + neg_emojis = ['😞', '😡', '😠', '👎', '😭', '🤮', '❌', '⚠️', '🤦', '💩', '😢', '😒', '💔', '🙄'] + + # Gelişmiş Olumlu Kelime Haznesi + pos_words = [ + 'harika', 'süper', 'muhteşem', 'mükemmel', 'kusursuz', 'başarılı', 'iyi', 'güzel', 'hızlı', + 'kaliteli', 'güvenilir', 'sağlam', 'faydalı', 'yararlı', 'muazzam', 'harikulade', 'şahane', + 'enfes', 'temiz', 'pratik', 'uygun', 'teşekkür', 'memnun', 'beğendim', 'tavsiye', 'eline sağlık', + 'sağol', 'sevdim', 'harikasınız', 'çok iyi', 'ilgi', 'alaka', 'helal', 'başarılar', 'kolay gelsin', + 'kolaylık', 'kazandırdı', 'çözüldü', 'iyileşti', 'memnuniyet', 'profesyonel', 'efsane', '10 numara' + ] + + # Gelişmiş Olumsuz Kelime Haznesi + neg_words = [ + 'kötü', 'rezalet', 'berbat', 'yavaş', 'kusurlu', 'kırık', 'bozuk', 'pahalı', 'ilgisiz', + 'eksik', 'yanlış', 'rezil', 'hüsran', 'sahte', 'aldatıcı', 'dolandırıcı', 'berbat', 'çöp', + 'vasat', 'verimsiz', 'gereksiz', 'şikayet', 'sorun', 'hata', 'çalışmıyor', 'memnun kalmadım', + 'tavsiye etmem', 'beğenmedim', 'iletişim sıfır', 'cevap vermiyor', 'iade', 'mağdur', 'geç geldi', + 'hayal kırıklığı', 'gecikme', 'gelmedi', 'hata alıyorum', 'çözülmedi', 'bozuldu', 'kayıp', + 'rezil', 'berbat', 'çalışmaz', 'arıza', 'arızalı' + ] + + pos_score = 0 + neg_score = 0 + + for emoji in pos_emojis: + if emoji in post.message: + pos_score += 2 + for emoji in neg_emojis: + if emoji in post.message: + neg_score += 2 + + for word in pos_words: + if word in lower_msg: + pos_score += 2 + for word in neg_words: + if word in lower_msg: + neg_score += 2 + + if not ai_success: + if pos_score > neg_score: + sentiment = 'positive' + elif neg_score > pos_score: + sentiment = 'negative' + else: + sentiment = 'neutral' + post.sentiment = sentiment + + # Otonom Müşteri Çalma (Competitor Customer Poaching) + if post.sentiment == 'negative' and post.stream_id.stream_type == 'competitor_watch': + if not post.lead_id: + competitor_name = post.stream_id.target_competitor_id.name if post.stream_id.target_competitor_id else "Rakip" + lead = self.env['crm.lead'].create({ + 'name': f"🚨 {competitor_name} Şikayeti (Fırsat): {post.author_name}", + 'description': f"Rakip Müşterisi Şikayeti Yakalandı!\n\nİçerik: {post.message}\nLink: {post.link_url}", + 'type': 'opportunity', + 'priority': '3' # Yüksek Öncelik + }) + post.lead_id = lead.id def action_suggest_reply(self): self.ensure_one() @@ -124,12 +240,27 @@ class SocialStreamPost(models.Model): def action_create_lead(self): self.ensure_one() + if self.lead_id: + # Zaten fırsat oluşturulmuşsa, direkt o fırsatı aç + return { + 'name': 'Müşteri Adayı', + 'type': 'ir.actions.act_window', + 'res_model': 'crm.lead', + 'res_id': self.lead_id.id, + 'view_mode': 'form', + 'target': 'current' + } + # Odoo CRM'de yeni bir Lead (Fırsat/Müşteri Adayı) yarat lead = self.env['crm.lead'].create({ 'name': f"Sosyal Medya Fırsatı: {self.author_name}", - 'description': f"İçerik: {self.message}\nLink: {self.link_url}", + 'description': f"İçerik: {self.message}\n\nLink: {self.link_url}\nPlatform ID: {self.platform_post_id}", 'type': 'opportunity' }) + + # İlişkiyi kaydet + self.lead_id = lead.id + return { 'name': 'Yeni Müşteri Adayı', 'type': 'ir.actions.act_window', diff --git a/extra-addons/mymach_social_nextgen/models/social_trend.py b/extra-addons/mymach_social_nextgen/models/social_trend.py new file mode 100644 index 0000000..ffe846c --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_trend.py @@ -0,0 +1,234 @@ +from odoo import models, fields, api +import base64 +import requests + +class SocialTrend(models.Model): + _name = 'mymach.social.trend' + _description = 'Trend Korsanlığı (Viral Hijacking)' + _order = 'write_date desc, id desc' + + name = fields.Char(string='Trend Konu (Hedef)', required=True) + description = fields.Text(string='Trend Özeti') + source = fields.Selection([ + ('google', 'Google Arama Trendleri'), + ('twitter', 'X (Twitter) Trendleri'), + ('linkedin', 'LinkedIn Popüler Konular'), + ('industry', 'CNC & Endüstri Haberleri'), + ], string='Kaynak', default='google', required=True) + + state = fields.Selection([ + ('new', 'Yeni Trend'), + ('selected', 'Fikir Paylaşıldı'), + ('ignored', 'Yoksayıldı (Diğer)') + ], string='Durum', default='new', required=True) + + post_idea_ids = fields.One2many('mymach.social.trend.idea', 'trend_id', string='Paylaşım Fikirleri') + + def action_fetch_trends(self): + # Aktif hesapların platformlarını topla + accounts = self.env['mymach.social.account'].search([]) + platforms = list(set(accounts.mapped('platform'))) if accounts else ['facebook', 'instagram', 'linkedin', 'twitter'] + + + system_instruction = ( + "Sen bir sosyal dinleme (social listening) uzmanısın. " + "Sana verilen sosyal medya platformlarında CNC, talaşlı imalat, yüksek teknoloji ve makine üretimi " + "konusundaki popüler 3 trend konuyu araştır (veya üret). " + "Her trend konu için başlık, kaynak ve kısa bir özet sağla. \n" + "Çıktıyı kesinlikle şu JSON formatında ver: \n" + "[\n" + " {\n" + " \"name\": \"Trend Başlığı\",\n" + " \"description\": \"Kısa özet...\",\n" + " \"source\": \"google\" veya \"twitter\" veya \"linkedin\" veya \"industry\"\n" + " }, ...\n" + "]" + ) + + user_prompt = f"Analiz edilecek platformlar: {', '.join(platforms)}. Lütfen güncel trendleri çıkar." + + try: + generated_json = [] + + text_resp = self.env['mymach.social.ai.utils'].generate_text( + prompt=user_prompt, + system_prompt=system_instruction, + response_format='json' + ) + import json + generated_json = json.loads(text_resp) + + # Eğer hata oluştuysa yedek mock verileri kullan (catch-all) + if not generated_json: + generated_json = [ + { + "name": "CNC Makinelerinde Edge AI (Uç Yapay Zeka) Uygulamaları", + "description": "Sensör verilerinin doğrudan makine üzerinde işlenerek sıfır gecikmeli hata analizi yapılması bu hafta çok popüler.", + "source": "linkedin" + }, + { + "name": "Geleceğin Savunma Sanayi Parçalarında Titanyum İşleme", + "description": "Havacılık ve uzay sanayinde titanyum alaşımlı hafif parçaların işlenmesiyle ilgili teknik makaleler en çok okunanlar arasında.", + "source": "industry" + }, + { + "name": "Yeşil Fabrikalarda Atık Isı Geri Kazanımı", + "description": "Karbon nötr imalat kapsamında, CNC motorlarının atık ısısının fabrika ısıtmasında kullanılmasıyla ilgili trend.", + "source": "google" + } + ] + + for t_raw in generated_json: + trend = self.create({ + 'name': t_raw.get('name'), + 'description': t_raw.get('description'), + 'source': t_raw.get('source', 'google'), + 'state': 'new' + }) + # Otomatik olarak fikirlerini de üret + trend.action_generate_ideas() + + except Exception: + pass + + def action_generate_ideas(self): + self.ensure_one() + # Bu trende bağlı fikirleri sil + self.post_idea_ids.unlink() + + + # prompt + system_instruction = ( + "Sen uzman bir sosyal medya içerik üreticisisin. " + "Sana verilen trend konuya göre 3 adet özgün, ilgi çekici sosyal medya gönderisi fikri üret. " + "Her fikir için şunları sağla: \n" + "1. Fikir Başlığı\n" + "2. Gönderi Metni (Türkçe yazılmış, emojili ve hashtag'li)\n" + "3. Görsel Tarifi (Stable Diffusion için İngilizce bir prompt/tarif)\n" + "Çıktıyı kesinlikle şu JSON formatında ver: \n" + "[\n" + " {\n" + " \"name\": \"Fikir Başlığı 1\",\n" + " \"content\": \"Gönderi metni buraya...\",\n" + " \"image_prompt\": \"Detailed English image prompt...\"\n" + " }, ...\n" + "]" + ) + + user_prompt = f"Trend Konu: {self.name}\nTrend Açıklaması: {self.description or ''}" + + try: + generated_json = [] + + text_resp = self.env['mymach.social.ai.utils'].generate_text( + prompt=user_prompt, + system_prompt=system_instruction, + response_format='json' + ) + import json + generated_json = json.loads(text_resp.strip()) + + # Eğer hata oluştuysa yedek mock verileri oluştur + if not generated_json: + generated_json = [ + { + "name": f"{self.name} - Sektörel Analiz", + "content": f"🚀 CNC sektöründe yeni dönem: {self.name}! Makine hassasiyetini ve üretim hızını artıran bu yeni trendi yakından inceliyoruz. Sizce bu gelişme üretimi nasıl etkileyecek? Yorumlarınızı bekliyoruz! #CNC #mymach #{self.name.replace(' ', '')}", + "image_prompt": "Industrial high tech CNC machinery operating in a modern clean factory, cinematic lighting, 8k resolution" + }, + { + "name": f"{self.name} ile Verimlilik İpuçları", + "content": f"💡 {self.name} kullanarak fabrikalarda duruş sürelerini %30 azaltmak mümkün! İşte üretim hattınızı optimize etmenizi sağlayacak 3 kritik ipucu. Detaylar blogumuzda. #manufacturing #industry40 #{self.name.replace(' ', '')}", + "image_prompt": "A modern clean engineering office, a mechanical engineer analyzing graphs on a futuristic monitor, cozy workspace" + }, + { + "name": f"Geleceğe Hazır mısınız? {self.name}", + "content": f"🌟 Geleceğin fabrikaları {self.name} üzerine kuruluyor. MyMach olarak tüm ürünlerimizi bu entegrasyona hazır hale getirdik. Yeniliği bizimle keşfedin! #futuretech #machining #automation", + "image_prompt": "Futuristic mechanical design blueprint on a digital glowing screen, dark mode aesthetic, cyber tech" + } + ] + + # Fikirleri veritabanına kaydet ve görsellerini üret + for raw_idea in generated_json[:3]: + # Stable Diffusion Görseli Üret + import urllib.parse + encoded_prompt = urllib.parse.quote(raw_idea.get('image_prompt', 'industrial cnc machine')) + pollinations_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true" + + image_data = False + try: + img_resp = requests.get(pollinations_url, timeout=20) + if img_resp.status_code == 200: + image_data = base64.b64encode(img_resp.content) + except Exception: + pass + + self.env['mymach.social.trend.idea'].create({ + 'trend_id': self.id, + 'name': raw_idea.get('name'), + 'content': raw_idea.get('content'), + 'image_prompt': raw_idea.get('image_prompt'), + 'image_binary': image_data + }) + + except Exception: + pass + + def action_create_manual_post(self): + self.ensure_one() + self.write({'state': 'ignored'}) + return { + 'name': 'Yeni Gönderi Hazırla', + 'type': 'ir.actions.act_window', + 'res_model': 'mymach.social.post', + 'view_mode': 'form', + 'target': 'current', + 'context': {'default_name': f"Trend: {self.name}"} + } + + +class SocialTrendIdea(models.Model): + _name = 'mymach.social.trend.idea' + _description = 'Trend Paylaşım Fikri' + + trend_id = fields.Many2one('mymach.social.trend', string='Trend Konu', ondelete='cascade') + name = fields.Char(string='Fikir Başlığı', required=True) + content = fields.Text(string='Gönderi Açıklaması', required=True) + image_prompt = fields.Text(string='Görsel Tarifi (Prompt)') + image_binary = fields.Image(string='Önerilen Görsel') + + def action_select_idea(self): + self.ensure_one() + + # Trend durumunu güncelle + if self.trend_id: + self.trend_id.write({'state': 'selected'}) + + # 1. Gönderiyi (Post) yarat + post = self.env['mymach.social.post'].create({ + 'name': self.name, + 'content': self.content, + }) + + # 2. Eğer görsel varsa, eki (attachment) oluştur ve gönderiye bağla + if self.image_binary: + attachment = self.env['ir.attachment'].create({ + 'name': f"{self.name.replace(' ', '_')}_image.png", + 'datas': self.image_binary, + 'res_model': 'mymach.social.post', + 'res_id': post.id, + 'type': 'binary', + }) + post.write({ + 'media_ids': [(4, attachment.id)] + }) + + # 3. Kullanıcıyı oluşturulan gönderinin form ekranına yönlendir + return { + 'name': 'Gönderi Detayı', + 'type': 'ir.actions.act_window', + 'res_model': 'mymach.social.post', + 'res_id': post.id, + 'view_mode': 'form', + 'target': 'current', + } diff --git a/extra-addons/mymach_social_nextgen/models/social_video_script.py b/extra-addons/mymach_social_nextgen/models/social_video_script.py new file mode 100644 index 0000000..3021a89 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_video_script.py @@ -0,0 +1,295 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError +import requests +import json +import logging + +_logger = logging.getLogger(__name__) + +class SocialVideoScript(models.Model): + _name = 'mymach.social.video.script' + _description = 'AI Video Senaryo ve Storyboard' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'create_date desc' + + name = fields.Char(string='Video Başlığı', required=True, tracking=True) + topic = fields.Text(string='Video Konusu / Fikir', required=True, tracking=True) + + platform = fields.Selection([ + ('tiktok', 'TikTok'), + ('reels', 'Instagram Reels'), + ('shorts', 'YouTube Shorts') + ], string='Hedef Platform', default='reels', required=True, tracking=True) + + duration = fields.Selection([ + ('15', '15 Saniye (Kısa & Hızlı)'), + ('30', '30 Saniye (Standart)'), + ('60', '60 Saniye (Detaylı)') + ], string='Hedef Süre', default='30', required=True, tracking=True) + + state = fields.Selection([ + ('draft', 'Taslak'), + ('generated', 'Senaryo Üretildi'), + ('in_production', 'Çekim Aşamasında'), + ('done', 'Tamamlandı') + ], string='Durum', default='draft', tracking=True) + + ai_provider = fields.Selection([ + ('default', 'Sistem Varsayılanı'), + ('gemini', 'Google Gemini'), + ('openai', 'OpenAI (ChatGPT)'), + ('claude', 'Anthropic (Claude)'), + ('grok', 'xAI (Grok)'), + ('groq', 'Groq (Llama 3)') + ], string='Yapay Zeka', default='default', required=True, tracking=True) + + voice_type = fields.Selection([ + ('male', 'Erkek Sesi'), + ('female', 'Kadın Sesi') + ], string='Dış Ses Tipi', default='male', tracking=True) + + language = fields.Selection([ + ('tr', 'Türkçe'), + ('en', 'İngilizce') + ], string='Video Dili', default='tr', tracking=True) + + render_status = fields.Selection([ + ('not_started', 'Render Edilmedi'), + ('processing', 'Üretiliyor (Render)'), + ('done', 'Tamamlandı'), + ('error', 'Hata') + ], string='Video Durumu', default='not_started', tracking=True) + + video_url = fields.Char(string='Video URL') + render_task_id = fields.Char(string='Render Görev ID') + + scene_ids = fields.One2many('mymach.social.video.scene', 'script_id', string='Sahneler (Storyboard)') + + music_suggestion = fields.Char(string='Müzik/Ses Önerisi', readonly=True) + caption_suggestion = fields.Text(string='Açıklama (Caption) Önerisi', readonly=True) + + audio_file = fields.Binary(string='Dış Ses (MP3)', attachment=True) + audio_file_name = fields.Char(string='Ses Dosyası Adı') + audio_player_html = fields.Html(string='Ses Çalar', compute='_compute_audio_player', sanitize=False) + + @api.depends('audio_file') + def _compute_audio_player(self): + for record in self: + if record.audio_file and record.id: + url = f"/web/content/mymach.social.video.script/{record.id}/audio_file" + record.audio_player_html = f'' + else: + record.audio_player_html = '

Henüz ses üretilmedi.

' + + def action_generate_script(self): + self.ensure_one() + + system_prompt = f"""Sen viral dikey videolar (TikTok, Reels, Shorts) hazırlayan profesyonel bir yönetmen ve metin yazarısın. +Kullanıcı senden {self.duration} saniyelik bir {self.platform} videosu için saniye saniye storyboard çıkarmanı istiyor. +Videonun Konusu: {self.topic} + +Yanıtını kesinlikle aşağıdaki JSON formatında, ekstra markdown veya metin eklemeden döndür: +{{ + "music_suggestion": "Trend olan uygun bir müzik veya ses efekti türü önerisi", + "caption_suggestion": "Sosyal medya açıklaması ve hashtagler", + "scenes": [ + {{ + "sequence": 1, + "time_range": "0:00 - 0:03", + "visual_action": "Ekranda kameranın neyi çekeceği, nasıl bir hareket olacağı", + "voiceover": "Dış sesin söyleyeceği cümle", + "overlay_text": "Videonun tam o anında ekranda belirmesi gereken dikkat çekici yazı" + }}, + ... (videonun süresine göre sahneleri doldur) + ] +}} +""" + try: + # Seçilen AI sağlayıcısını belirle + provider = self.ai_provider if self.ai_provider != 'default' else None + + # Yapay Zeka Yöneticisinden İstek At + text_output = self.env['mymach.social.ai.utils'].generate_text( + prompt="Lütfen yukarıdaki konuya uygun bir video senaryosu oluştur.", + system_prompt=system_prompt, + response_format='json', + provider=provider + ) + + data = json.loads(text_output) + + # Mevcut sahneleri sil + self.scene_ids.unlink() + + self.music_suggestion = data.get('music_suggestion', '') + self.caption_suggestion = data.get('caption_suggestion', '') + + for scene_data in data.get('scenes', []): + self.env['mymach.social.video.scene'].create({ + 'script_id': self.id, + 'sequence': scene_data.get('sequence', 1), + 'time_range': scene_data.get('time_range', ''), + 'visual_action': scene_data.get('visual_action', ''), + 'voiceover': scene_data.get('voiceover', ''), + 'overlay_text': scene_data.get('overlay_text', ''), + }) + + self.state = 'generated' + except json.JSONDecodeError: + raise UserError("Gemini geçersiz bir JSON formatı döndürdü. Lütfen tekrar deneyin.") + except requests.exceptions.RequestException as e: + raise UserError(f"API Bağlantı Hatası: {e}") + + def action_set_in_production(self): + for record in self: + record.state = 'in_production' + + def action_set_done(self): + for record in self: + record.state = 'done' + + def action_generate_voiceover(self): + """OpenAI TTS vb. kullanarak dış ses (MP3) oluşturur.""" + self.ensure_one() + if not self.scene_ids: + raise UserError("Sahneler (storyboard) bulunamadı. Önce senaryo üretmelisiniz.") + + voiceover_parts = [scene.voiceover for scene in self.scene_ids if scene.voiceover] + full_script = " ".join(voiceover_parts) + + if not full_script: + raise UserError("Senaryoda dış ses metni (voiceover) bulunamadı.") + + # OpenAI API Anahtarı + api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key') + if not api_key: + raise UserError("Lütfen Odoo Ayarlarından (MyMach Social > Yapay Zeka) OpenAI API Anahtarını yapılandırın (Ses Üretimi İçin).") + + url = "https://api.openai.com/v1/audio/speech" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + voice = "onyx" if self.voice_type == 'male' else "nova" + + payload = { + "model": "tts-1", + "input": full_script, + "voice": voice + } + + try: + response = requests.post(url, headers=headers, json=payload, timeout=60) + if response.status_code == 200: + import base64 + self.audio_file = base64.b64encode(response.content) + self.audio_file_name = f"voiceover_{self.id}.mp3" + self.message_post(body="Dış Ses (Voiceover) başarıyla üretildi.") + else: + raise UserError(f"Ses üretim hatası (API): {response.text}") + except Exception as e: + raise UserError(f"Bağlantı hatası: {str(e)}") + + def action_render_video(self): + """Send video request to Creatify API.""" + self.ensure_one() + if not self.scene_ids: + raise UserError("Render edilecek sahneler (storyboard) bulunamadı. Önce senaryo üretmelisiniz.") + + api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.creatify_api_key') + if not api_key: + raise UserError("Lütfen Odoo Ayarlarından Creatify API Anahtarını yapılandırın (15s Video Render için).") + + # Senaryoyu seslendirme ve görsel birleştirme için tam metin (script) haline getirelim + voiceover_parts = [] + for scene in self.scene_ids: + if scene.voiceover: + voiceover_parts.append(scene.voiceover) + + full_script = " ".join(voiceover_parts) + if not full_script: + full_script = self.topic + + # Örnek Creatify API Request (Temsili endpoint ve payload, Creatify dökümanına göre uyarlanmıştır) + url = "https://api.creatify.ai/api/v1/videos" + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "Accept": "application/json" + } + + # Creatify Payload (Senaryo, Dil, Ses Seçimi) + payload = { + "script": full_script, + "duration": int(self.duration), + "language": self.language, + "voice": self.voice_type, + "title": self.name, + "format": "9:16", + "render_subtitles": True + } + + try: + response = requests.post(url, headers=headers, json=payload, timeout=30) + if response.status_code in [200, 201]: + data = response.json() + self.render_task_id = str(data.get('video_id') or data.get('id')) + self.render_status = 'processing' + self.message_post(body="Video Render işlemi Creatify API'sine başarıyla gönderildi. 15 saniyelik sesli ve alt yazılı videonuz hazırlanıyor. Bu işlem vài dakika sürebilir.") + else: + raise UserError(f"Creatify API Hatası: {response.text}") + except Exception as e: + raise UserError(f"API Bağlantı Hatası: {str(e)}") + + @api.model + def _cron_check_video_renders(self): + """Check the status of processing videos on Creatify.""" + scripts = self.search([('render_status', '=', 'processing'), ('render_task_id', '!=', False)]) + if not scripts: + return + + api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.creatify_api_key') + if not api_key: + return + + headers = { + "Authorization": f"Bearer {api_key}", + "Accept": "application/json" + } + + for script in scripts: + try: + url = f"https://api.creatify.ai/api/v1/videos/{script.render_task_id}" + response = requests.get(url, headers=headers, timeout=20) + if response.status_code == 200: + data = response.json() + status = data.get('status') + + if status in ['completed', 'done', 'succeeded']: + script.render_status = 'done' + script.state = 'done' + script.video_url = data.get('url') or data.get('video_url') + script.message_post(body="Video Render işlemi başarıyla tamamlandı! Tam 15 saniyelik sesli videonuzu izleyebilirsiniz.") + elif status in ['failed', 'error']: + script.render_status = 'error' + script.message_post(body=f"Video Render işlemi HATA aldı: {data.get('error', 'Bilinmeyen hata')}") + elif status == 'canceled': + script.render_status = 'error' + script.message_post(body="Video Render işlemi iptal edildi.") + except Exception as e: + _logger.error(f"Creatify Check Error for {script.id}: {e}") + + + +class SocialVideoScene(models.Model): + _name = 'mymach.social.video.scene' + _description = 'Video Sahnesi (Storyboard)' + _order = 'sequence, id' + + script_id = fields.Many2one('mymach.social.video.script', string='Video Senaryosu', required=True, ondelete='cascade') + sequence = fields.Integer(string='Sıra', required=True) + time_range = fields.Char(string='Süre (Zaman Aralığı)', required=True, help="Örn: 0:00 - 0:03") + + visual_action = fields.Text(string='Görsel Aksiyon (Kamera Ne Çekiyor?)') + voiceover = fields.Text(string='Dış Ses / Diyalog') + overlay_text = fields.Char(string='Ekrandaki Metin (Yazı)') diff --git a/extra-addons/mymach_social_nextgen/scratch/associate_existing_posts.py b/extra-addons/mymach_social_nextgen/scratch/associate_existing_posts.py new file mode 100644 index 0000000..215ce67 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/associate_existing_posts.py @@ -0,0 +1,48 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry + +# Bootstrap Odoo +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + # 1. Odoo Sosyal Gönderisi (mymach.social.post) bul veya yarat + post = env['mymach.social.post'].search([('name', '=', 'Odoo Sosyal Özellikleri')], limit=1) + if not post: + # Get any account to link + account = env['mymach.social.account'].search([], limit=1) + post = env['mymach.social.post'].create({ + 'name': 'Odoo Sosyal Özellikleri', + 'content': 'Sosyal pazarlama modülümüz için yakında gelecek yeni özelliklerimizi duyurmaktan heyecan duyuyoruz! 🚀 #Odoo19 #Pazarlama', + 'account_ids': [(4, account.id)] if account else [], + 'state': 'published', + 'published_date': odoo.fields.Datetime.now(), + }) + print(f"Created new published post: {post.name}") + + # 2. Bu gönderiyi temsil eden akış kayıtlarını (stream.post) güncelle + stream_posts = env['mymach.social.stream.post'].search([ + ('message', 'like', 'Sosyal pazarlama modülümüz için yakında gelecek') + ]) + if stream_posts: + stream_posts.write({ + 'post_id': post.id, + 'message_type': 'post' + }) + print(f"Linked {len(stream_posts)} stream posts of type 'post' to Odoo post.") + + # 3. Yorumları (Ahmet Yılmaz vb.) bu gönderiye bağla (çünkü bu gönderinin altına yapılmış bir yorum) + comments = env['mymach.social.stream.post'].search([ + ('message', 'like', 'fiyat bilgisi alabilir miyim') + ]) + if comments: + comments.write({ + 'post_id': post.id, + 'message_type': 'comment' + }) + print(f"Linked {len(comments)} comment stream posts to Odoo post.") + + print("Association successfully updated!") diff --git a/extra-addons/mymach_social_nextgen/scratch/delete_dashboard_menu.py b/extra-addons/mymach_social_nextgen/scratch/delete_dashboard_menu.py new file mode 100644 index 0000000..b7cee7f --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/delete_dashboard_menu.py @@ -0,0 +1,34 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry + +# Bootstrap Odoo +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + # 1. 'action_social_dashboard' eylemine bağlı olan tüm menüleri (ir.ui.menu) bul ve sil + action = env['ir.actions.act_window'].search([('name', '=', 'Analiz Dashboard')], limit=1) + if action: + menus = env['ir.ui.menu'].search([('action', '=', f'ir.actions.act_window,{action.id}')]) + if menus: + print(f"Found {len(menus)} menus linked to Analiz Dashboard action. Deleting...") + menus.unlink() + else: + print("No menus linked to the action found.") + + # Eylemin kendisini de silelim + print("Deleting the action 'Analiz Dashboard'...") + action.unlink() + else: + print("Action 'Analiz Dashboard' not found.") + + # 2. İsmi doğrudan 'Analiz Dashboard' olan menüleri de kontrol edip silelim + direct_menus = env['ir.ui.menu'].search([('name', '=', 'Analiz Dashboard')]) + if direct_menus: + print(f"Found {len(direct_menus)} menus named 'Analiz Dashboard'. Deleting...") + direct_menus.unlink() + + print("Database cleanup completed successfully!") diff --git a/extra-addons/mymach_social_nextgen/scratch/sync_media_posts.py b/extra-addons/mymach_social_nextgen/scratch/sync_media_posts.py new file mode 100644 index 0000000..65d1edb --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/sync_media_posts.py @@ -0,0 +1,24 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry + +# Bootstrap Odoo +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + media_records = env['mymach.social.media'].search([('post_id', '=', False)]) + print(f"Found {len(media_records)} media records without post_id. Syncing...") + + synced_count = 0 + for media in media_records: + # Find any social post referencing this attachment + post = env['mymach.social.post'].search([('media_ids', 'in', [media.attachment_id.id])], limit=1) + if post: + media.write({'post_id': post.id}) + synced_count += 1 + print(f"Synced Media '{media.name}' to Post '{post.name}'") + + print(f"Completed! Synced {synced_count} records.") diff --git a/extra-addons/mymach_social_nextgen/scratch/sync_published_posts.py b/extra-addons/mymach_social_nextgen/scratch/sync_published_posts.py new file mode 100644 index 0000000..d7514f4 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/sync_published_posts.py @@ -0,0 +1,51 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry + +# Bootstrap Odoo +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + # Yayınlanmış tüm Odoo gönderilerini (social.post) ara + published_posts = env['mymach.social.post'].search([('state', '=', 'published')]) + print(f"Found {len(published_posts)} published posts in total.") + + for post in published_posts: + for account in post.account_ids: + # Bu hesap ve gönderi için zaten akış kaydı var mı kontrol et + existing = env['mymach.social.stream.post'].search([ + ('post_id', '=', post.id), + ('account_id', '=', account.id) + ], limit=1) + + if not existing: + # Akış (Zaman Tüneli / Feed) bul veya oluştur + stream = env['mymach.social.stream'].search([ + ('account_id', '=', account.id), + ('stream_type', '=', 'feed') + ], limit=1) + if not stream: + stream = env['mymach.social.stream'].create({ + 'account_id': account.id, + 'name': f"{account.name} - Zaman Tüneli", + 'stream_type': 'feed' + }) + + # Kontrol paneline yansıyacak gönderi (stream.post) kaydını oluştur + env['mymach.social.stream.post'].create({ + 'stream_id': stream.id, + 'author_name': account.name, + 'published_date': post.published_date or post.scheduled_date or post.write_date, + 'message': post.content or post.name, + 'platform_post_id': f"PLATFORM_{account.id}_{post.id}", + 'message_type': 'post', + 'post_id': post.id + }) + print(f"Successfully synced '{post.name}' to stream dashboard for account '{account.name}'") + else: + print(f"Stream post for '{post.name}' on account '{account.name}' already exists.") + + print("Sync process completed!") diff --git a/extra-addons/mymach_social_nextgen/scratch/test_competitor.py b/extra-addons/mymach_social_nextgen/scratch/test_competitor.py new file mode 100644 index 0000000..e4a027f --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/test_competitor.py @@ -0,0 +1,44 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry +import logging + +logging.basicConfig(level=logging.INFO) +_logger = logging.getLogger('CompetitorTest') + +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +def run(): + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + # 1. Create a Competitor + comp = env['social.competitor'].search([('name', '=', 'Test Rival')], limit=1) + if not comp: + comp = env['social.competitor'].create({ + 'name': 'Test Rival', + 'platform': 'linkedin', + 'profile_url': 'https://linkedin.com/company/test-rival', + 'followers_count': 15000, + }) + _logger.info(f"Created Test Rival: {comp.id}") + + # Add old history + env['social.competitor.history'].create({ + 'competitor_id': comp.id, + 'followers_count': 14000, + 'engagement_rate': 2.5 + }) + + # 2. Trigger Action Scrape Data (Gemini Integration) + _logger.info("Triggering action_scrape_data()...") + comp.action_scrape_data() + + _logger.info(f"New Followers: {comp.followers_count}") + _logger.info(f"Growth Rate: {comp.growth_rate}%") + _logger.info(f"AI Summary: {comp.ai_summary}") + _logger.info(f"Top Keywords: {comp.top_keywords}") + +if __name__ == '__main__': + run() diff --git a/extra-addons/mymach_social_nextgen/scratch/test_gemini_sentiment.py b/extra-addons/mymach_social_nextgen/scratch/test_gemini_sentiment.py new file mode 100644 index 0000000..6683b95 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/test_gemini_sentiment.py @@ -0,0 +1,51 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry +import requests +import json + +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + api_key = env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key') + print("API Key exists:", bool(api_key)) + + url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}" + system_prompt = ( + "Sen profesyonel bir sosyal medya duygu analizi uzmanısın. " + "Verilen Türkçe veya İngilizce sosyal medya yorumunu analiz et. " + "Şu JSON formatında cevap ver:\n" + "{\n" + " \"sentiment\": \"positive\" | \"neutral\" | \"negative\",\n" + " \"reason\": \"Metnin neden bu duygu durumuna sahip olduğuna dair kısa, net, profesyonel Türkçe açıklama (max 2 cümle).\"\n" + "}" + ) + + payload = { + "system_instruction": {"parts": [{"text": system_prompt}]}, + "contents": [{"parts": [{"text": "Yorum: Makineleriniz harika çalışıyor, çok hızlı kargo ve muazzam bir ilgi alaka var! Teşekkürler 😊👍"}]}], + "generationConfig": { + "responseMimeType": "application/json", + "responseSchema": { + "type": "OBJECT", + "properties": { + "sentiment": { + "type": "STRING", + "enum": ["positive", "neutral", "negative"] + }, + "reason": { + "type": "STRING" + } + }, + "required": ["sentiment", "reason"] + }, + "temperature": 0.1, + "maxOutputTokens": 1000 + } + } + + response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload, timeout=10) + print("Status:", response.status_code) + print("Raw Response:", response.text) diff --git a/extra-addons/mymach_social_nextgen/scratch/test_sentiment_analysis.py b/extra-addons/mymach_social_nextgen/scratch/test_sentiment_analysis.py new file mode 100644 index 0000000..da5df05 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/test_sentiment_analysis.py @@ -0,0 +1,39 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry + +# Bootstrap Odoo +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + # 1. Akış bulalım + stream = env['mymach.social.stream'].search([], limit=1) + print(f"Using stream: {stream.name}") + + # 2. Test verilerini oluşturalım (özellikle 'kusursuz' vb. kelimeleri içeren olumlu/olumsuz metinler) + test_comments = [ + ("Bu makine gerçekten kusursuz çalışıyor, işimizi çok kolaylaştırdı.", "positive"), + ("Parçaların kalitesi mükemmel, elinize sağlık.", "positive"), + ("Hizmetiniz şahane, çok memnun kaldım.", "positive"), + ("Sistem rezalet, sürekli hata veriyor ve çok yavaş.", "negative"), + ("Ürün arızalı çıktı, iade istiyorum.", "negative"), + ("Makinenin çalışma voltajı nedir, kaç watt güç tüketiyor?", "neutral") + ] + + for msg, expected in test_comments: + post = env['mymach.social.stream.post'].create({ + 'stream_id': stream.id, + 'author_name': 'Test Kullanıcısı', + 'message': msg, + 'message_type': 'comment' + }) + # Analizi tetikleyelim + post.action_analyze_sentiment() + + print(f"\nMesaj: '{post.message}'") + print(f"-> Tespit Edilen Duygu: {post.sentiment} (Beklenen: {expected})") + + print("\nKelime dağarcığı senaryo testleri başarıyla tamamlandı!") diff --git a/extra-addons/mymach_social_nextgen/scratch/test_system_integrity.py b/extra-addons/mymach_social_nextgen/scratch/test_system_integrity.py new file mode 100644 index 0000000..402b01c --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/test_system_integrity.py @@ -0,0 +1,121 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry +import logging +import traceback + +logging.basicConfig(level=logging.INFO) +_logger = logging.getLogger('SystemIntegrityTest') + +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +def run_tests(): + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + errors = [] + fixes_needed = [] + + _logger.info("--- 1. Testing Watermark Logic (mymach.social.post) ---") + try: + # Create a mock image attachment + import base64 + img_data = base64.b64encode(b'R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=').decode('utf-8') + attachment = env['ir.attachment'].create({ + 'name': 'test_image.gif', + 'datas': img_data, + 'res_model': 'mymach.social.post', + 'mimetype': 'image/gif' + }) + + # Need an account first + account = env['mymach.social.account'].search([], limit=1) + if not account: + account = env['mymach.social.account'].create({'name': 'Test Account', 'platform': 'facebook'}) + + post = env['mymach.social.post'].create({ + 'name': 'Test Post', + 'content': 'Test Post for Watermark', + 'account_ids': [(4, account.id)], + 'media_ids': [(4, attachment.id)] + }) + _logger.info(f"Post created: {post.id}, Attachment: {attachment.id}") + except Exception as e: + errors.append(f"Watermark Test Failed: {e}") + _logger.error(traceback.format_exc()) + + _logger.info("--- 2. Testing Sentiment Analysis & CRM Action (mymach.social.stream.post) ---") + try: + stream = env['mymach.social.stream'].search([], limit=1) + if not stream: + stream = env['mymach.social.stream'].create({'name': 'Test Stream', 'stream_type': 'facebook'}) + + stream_post = env['mymach.social.stream.post'].create({ + 'stream_id': stream.id, + 'author_name': 'Test User', + 'message': 'Sisteminiz kusursuz çalışıyor, harika bir iş.', + 'message_type': 'comment' + }) + stream_post.action_analyze_sentiment() + if stream_post.sentiment != 'positive': + errors.append(f"Sentiment Analysis Failed: Expected 'positive', got '{stream_post.sentiment}'") + else: + _logger.info(f"Sentiment Analysis OK: {stream_post.sentiment}") + + action = stream_post.action_create_lead() + if not stream_post.lead_id: + errors.append("Lead creation failed.") + else: + _logger.info(f"CRM Lead created: {stream_post.lead_id.name}") + except Exception as e: + errors.append(f"Stream Post Test Failed: {e}") + _logger.error(traceback.format_exc()) + + _logger.info("--- 3. Testing Auto Reply Engine (mymach.social.auto.reply) ---") + try: + rule = env['mymach.social.auto.reply'].create({ + 'name': 'Test Positive Rule', + 'keyword': 'mükemmel', + 'reply_message': 'Teşekkürler, çok naziksiniz!', + 'apply_to_message_type': 'all', + 'active': True + }) + + stream_post_2 = env['mymach.social.stream.post'].create({ + 'stream_id': stream.id, + 'author_name': 'Test User 2', + 'message': 'Bu ürün mükemmel, çok beğendim.', + 'message_type': 'comment', + 'sentiment': 'positive' + }) + + logs = env['mymach.social.auto.reply.log'].search([('post_id', '=', stream_post_2.id)]) + _logger.info(f"Auto Reply Logs found: {len(logs)}") + except Exception as e: + errors.append(f"Auto Reply Test Failed: {e}") + _logger.error(traceback.format_exc()) + + _logger.info("--- 4. Testing Trend Engine Integration (mymach.social.trend) ---") + try: + trend = env['mymach.social.trend'].search([], limit=1) + if not trend: + trend = env['mymach.social.trend'].create({ + 'name': 'AI Integration in CRM', + 'keyword': 'AI CRM' + }) + _logger.info(f"Trend initialized: {trend.name}") + except Exception as e: + errors.append(f"Trend Engine Test Failed: {e}") + _logger.error(traceback.format_exc()) + + return errors, fixes_needed + +if __name__ == '__main__': + errors, fixes = run_tests() + print("================== TEST RESULTS ==================") + if errors: + print("ERRORS FOUND:") + for err in errors: + print(f"- {err}") + else: + print("ALL TESTS PASSED WITH NO CRASHES.") diff --git a/extra-addons/mymach_social_nextgen/scratch/test_video_script.py b/extra-addons/mymach_social_nextgen/scratch/test_video_script.py new file mode 100644 index 0000000..b3cd148 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/test_video_script.py @@ -0,0 +1,44 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry +import logging +import time + +logging.basicConfig(level=logging.INFO) +_logger = logging.getLogger('VideoTest') + +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +def run(): + with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + # Odoo API Key'i ayarlayalım ki Gemini API çalışsın. + # Bu test için örnekte geçici olarak test ediyoruz. + # env['ir.config_parameter'].sudo().set_param('mymach_social.gemini_api_key', 'TEST_KEY_HERE') + + # 1. Create a Video Script + script = env['mymach.social.video.script'].create({ + 'name': 'Yeni CNC Tezgah Tanıtımı', + 'topic': 'Şirketimizin yeni getirdiği 5 eksenli CNC makinesinin hızını ve hassasiyetini gösteren çok enerjik bir video.', + 'platform': 'tiktok', + 'duration': '15', + }) + _logger.info(f"Created Video Script: {script.id}") + + _logger.info("Triggering action_generate_script()...") + try: + script.action_generate_script() + _logger.info(f"State: {script.state}") + _logger.info(f"Music Suggestion: {script.music_suggestion}") + _logger.info(f"Caption: {script.caption_suggestion}") + + _logger.info("Scenes Generated:") + for scene in script.scene_ids: + _logger.info(f"Scene {scene.sequence} ({scene.time_range}): {scene.visual_action}") + except Exception as e: + _logger.error(f"Error during generation: {e}") + +if __name__ == '__main__': + run() diff --git a/extra-addons/mymach_social_nextgen/scratch/test_watermarking.py b/extra-addons/mymach_social_nextgen/scratch/test_watermarking.py new file mode 100644 index 0000000..30974c4 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/test_watermarking.py @@ -0,0 +1,83 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry +import base64 +import io +from PIL import Image + +# Bootstrap Odoo +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + + # 1. Create a dummy watermark image (PNG with transparency) + wm_img = Image.new('RGBA', (200, 50), color=(255, 0, 0, 128)) # Semi-transparent red block + wm_bytes = io.BytesIO() + wm_img.save(wm_bytes, format='PNG') + wm_base64 = base64.b64encode(wm_bytes.getvalue()) + + # Set it on the current company + company = env.company + company.write({ + 'social_watermark_image': wm_base64, + 'social_watermark_position': 'bottom_right', + 'social_watermark_opacity': 70 + }) + print("Dummy watermark set on company.") + + # Get or create a mock account + account = env['mymach.social.account'].search([], limit=1) + if not account: + media = env['social.media'].search([], limit=1) + if not media: + media = env['social.media'].create({'name': 'Mock Media', 'image': False}) + account = env['mymach.social.account'].create({ + 'name': 'Mock Account', + 'media_id': media.id, + 'platform': 'facebook', + }) + print(f"Using account: {account.id} ({account.name})") + + # 2. Create a dummy post image to be watermarked (JPEG format) + post_img = Image.new('RGB', (800, 600), color=(0, 255, 0)) # Green background + post_bytes = io.BytesIO() + post_img.save(post_bytes, format='JPEG') + post_base64 = base64.b64encode(post_bytes.getvalue()) + + # Create ir.attachment for this image + attachment = env['ir.attachment'].create({ + 'name': 'test_post_image.jpg', + 'datas': post_base64, + 'mimetype': 'image/jpeg', + 'res_model': 'mymach.social.post', + 'res_id': 0 + }) + print(f"Attachment created: {attachment.id}, is_watermarked: {attachment.is_watermarked}") + + # 3. Create a social post linking this attachment + post = env['mymach.social.post'].create({ + 'name': 'Watermark Test Post', + 'content': 'Checking auto watermarking features...', + 'account_ids': [(4, account.id)], + 'media_ids': [(4, attachment.id)] + }) + print(f"Post created: {post.id}, state: {post.state}") + + # Refresh attachment from db + attachment.invalidate_recordset() + print(f"After post creation - Attachment is_watermarked: {attachment.is_watermarked}") + + # Check if the watermark actually modified the data + if attachment.is_watermarked: + new_data = base64.b64decode(attachment.datas) + # Try loading the saved image to verify it's still a valid image + try: + saved_img = Image.open(io.BytesIO(new_data)) + print(f"Successfully loaded watermarked image: size={saved_img.size}, format={saved_img.format}") + print("TEST SUCCESS: Auto-watermarking works perfectly!") + except Exception as e: + print(f"TEST FAILED: Watermarked image could not be loaded: {e}") + else: + print("TEST FAILED: Attachment was not marked as watermarked.") diff --git a/extra-addons/mymach_social_nextgen/scratch/update_post_type.py b/extra-addons/mymach_social_nextgen/scratch/update_post_type.py new file mode 100644 index 0000000..a2d169a --- /dev/null +++ b/extra-addons/mymach_social_nextgen/scratch/update_post_type.py @@ -0,0 +1,17 @@ +import odoo +from odoo import api, SUPERUSER_ID +from odoo.modules.registry import Registry + +# Bootstrap Odoo +odoo.tools.config.parse_config([]) +registry = Registry('mymach_dev') + +with registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + # Update Odoo Sosyal post type to 'post' + posts = env['mymach.social.stream.post'].search([('author_name', '=', 'Odoo Sosyal')]) + if posts: + posts.write({'message_type': 'post'}) + print(f"Updated {len(posts)} posts to type 'post'.") + else: + print("No posts found with author 'Odoo Sosyal'.") diff --git a/extra-addons/mymach_social_nextgen/security/ir.model.access.csv b/extra-addons/mymach_social_nextgen/security/ir.model.access.csv index 74b49d8..b439d00 100644 --- a/extra-addons/mymach_social_nextgen/security/ir.model.access.csv +++ b/extra-addons/mymach_social_nextgen/security/ir.model.access.csv @@ -11,4 +11,13 @@ access_mymach_social_ai_reply_wizard_user,mymach.social.ai.reply.wizard.user,mod access_social_media,access.mymach.social.media,model_mymach_social_media,base.group_user,1,1,1,1 access_social_media_tag,access.mymach.social.media.tag,model_mymach_social_media_tag,base.group_user,1,1,1,1 access_social_auto_reply,access.mymach.social.auto.reply,model_mymach_social_auto_reply,base.group_user,1,1,1,1 -access_social_auto_reply_log,access.mymach.social.auto.reply.log,model_mymach_social_auto_reply_log,base.group_user,1,1,1,1 \ No newline at end of file +access_social_auto_reply_log,access.mymach.social.auto.reply.log,model_mymach_social_auto_reply_log,base.group_user,1,1,1,1 +access_social_trend,access.mymach.social.trend,model_mymach_social_trend,base.group_user,1,1,1,1 +access_social_trend_idea,access.mymach.social.trend.idea,model_mymach_social_trend_idea,base.group_user,1,1,1,1 +access_social_competitor_history,social.competitor.history,model_social_competitor_history,base.group_user,1,1,1,1 + +access_mymach_social_video_script,mymach.social.video.script,model_mymach_social_video_script,base.group_user,1,1,1,1 +access_mymach_social_video_scene,mymach.social.video.scene,model_mymach_social_video_scene,base.group_user,1,1,1,1 +access_mymach_social_dashboard,access_mymach_social_dashboard,model_mymach_social_dashboard,base.group_user,1,0,0,0 +access_social_competitor_follower,social.competitor.follower,model_social_competitor_follower,base.group_user,1,1,1,1 +access_mymach_social_competitor_ad,mymach.social.competitor.ad,mymach_social_nextgen.model_mymach_social_competitor_ad,base.group_user,1,1,1,1 diff --git a/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.js b/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.js new file mode 100644 index 0000000..a5c2392 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.js @@ -0,0 +1,46 @@ +/** @odoo-module **/ + +import { Component, onWillStart } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; + +export class SocialDashboard extends Component { + setup() { + this.orm = useService("orm"); + this.action = useService("action"); + + this.state = { + data: {} + }; + + onWillStart(async () => { + this.state.data = await this.orm.call( + "mymach.social.dashboard", + "get_dashboard_data", + [] + ); + }); + } + + openPosts() { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Gönderiler", + res_model: "mymach.social.post", + views: [[false, "list"], [false, "form"]], + }); + } + + openAlert(alertId) { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: "mymach.social.stream.post", + res_id: alertId, + views: [[false, "form"]], + }); + } +} + +SocialDashboard.template = "mymach_social_nextgen.Dashboard"; + +registry.category("actions").add("mymach_social_dashboard", SocialDashboard); diff --git a/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.scss b/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.scss new file mode 100644 index 0000000..350922b --- /dev/null +++ b/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.scss @@ -0,0 +1,26 @@ +.o_mymach_dashboard { + background-color: #f8f9fa; + min-height: 100vh; + + .card { + transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out; + + &:hover { + transform: translateY(-5px); + box-shadow: 0 .5rem 1rem rgba(0,0,0,.15)!important; + } + } + + .bg-primary-light { background-color: rgba(13, 110, 253, 0.1); } + .bg-success-light { background-color: rgba(25, 135, 84, 0.1); } + .bg-warning-light { background-color: rgba(255, 193, 7, 0.1); } + .bg-info-light { background-color: rgba(13, 202, 240, 0.1); } + + .list-group-item { + border-left: 0; + border-right: 0; + + &:first-child { border-top: 0; } + &:last-child { border-bottom: 0; } + } +} diff --git a/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.xml b/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.xml new file mode 100644 index 0000000..be20acc --- /dev/null +++ b/extra-addons/mymach_social_nextgen/static/src/components/dashboard/dashboard.xml @@ -0,0 +1,179 @@ + + + +
+
+
+
+

Sosyal Medya Yönetim Paneli

+

Tüm hesaplarınızın yapay zeka destekli performans özeti.

+
+
+ + +
+ +
+
+
+
+
+
Toplam Gönderi
+

+

+
+ +
+
+
+ Yayında + Zamanlanmış +
+
+
+
+ + +
+
+
+
+
+
Rakip Kitle Hacmi
+

+

+
+ +
+
+
+ Takip edilen tüm rakiplerin toplam takipçi sayısı +
+
+
+
+ + +
+
+
+
+
+
Üretilen AI Videoları
+

+

+
+ +
+
+
+ toplam senaryo içinden +
+
+
+
+ + +
+
+
+
+
+
Toplam Etkileşim
+

+

+
+ +
+
+
+ + + +
+
+
+
+
+ +
+ +
+
+
+
Duygu Analizi Özeti
+
+
+ +
+
+ Olumlu + % +
+
+
+
+
+ +
+
+ Nötr + % +
+
+
+
+
+ +
+
+ Olumsuz + % +
+
+
+
+
+
+ +

Henüz analiz edilmiş bir yorum bulunmuyor.

+
+
+
+
+ + +
+
+
+
Acil Müdahale Gerekenler
+ Yeni +
+
+ + + + +
+ +
Harika! Müdahale gerektiren olumsuz bir yorum yok.
+
+
+
+
+
+
+
+
+
+
diff --git a/extra-addons/mymach_social_nextgen/static/src/css/social_dashboard.css b/extra-addons/mymach_social_nextgen/static/src/css/social_dashboard.css index 44041d6..dc6c5cc 100644 --- a/extra-addons/mymach_social_nextgen/static/src/css/social_dashboard.css +++ b/extra-addons/mymach_social_nextgen/static/src/css/social_dashboard.css @@ -1,17 +1,67 @@ /* mymach_social_nextgen/static/src/css/social_dashboard.css */ +/* Parent Odoo Content Container */ +.o_content:has(.o_social_dashboard_kanban) { + background-color: #020617 !important; /* Koyu Slate arka plan */ + padding: 0 !important; + overflow: hidden !important; +} + /* 1. KANBAN VIEW (ANA ARKA PLAN) */ .o_kanban_view.o_social_dashboard_kanban { - background-color: #020617 !important; /* Çok daha koyu bir arka plan (Slate-950) */ + background-color: #020617 !important; padding: 16px !important; - min-height: 100vh !important; + margin: 0 !important; + height: 100% !important; + min-height: 100% !important; + display: flex !important; /* Odoo'nun orijinal flex yapısını bozma (sol panel için) */ + overflow: hidden !important; } -/* 2. SÜTUNLAR (STREAMS) */ +/* 2. SOL PANEL (SEARCH PANEL) KOYU TEMA DÜZENLEMESİ */ +.o_social_dashboard_kanban .o_search_panel { + background-color: #0f172a !important; + color: #e2e8f0 !important; + border-right: 1px solid #1e293b !important; + flex: 0 0 260px !important; + width: 260px !important; + min-width: 260px !important; + border-radius: 12px !important; + padding: 12px !important; + margin-right: 16px !important; +} + +.o_social_dashboard_kanban .o_search_panel .list-group-item { + background-color: transparent !important; + color: #cbd5e1 !important; + border: none !important; +} + +.o_social_dashboard_kanban .o_search_panel .list-group-item.active { + background-color: #1e293b !important; + color: #ffffff !important; + font-weight: bold !important; +} + +/* 3. KANBAN RENDERER (SÜTUNLARIN DİZİLDİĞİ ALAN) */ +.o_social_dashboard_kanban .o_kanban_renderer { + display: flex !important; + flex-direction: row !important; + flex-wrap: nowrap !important; + overflow-x: auto !important; /* Yalnızca sütunlar sağa sola kayacak */ + overflow-y: auto !important; + flex: 1 1 auto !important; + padding-bottom: 8px !important; +} + +/* 4. SÜTUNLAR (STREAMS) */ .o_social_dashboard_kanban .o_kanban_group { background-color: transparent !important; border: none !important; padding: 0 8px !important; + flex: 0 0 350px !important; /* Kolonlar sıkışmasın */ + width: 350px !important; + min-width: 350px !important; } /* Sütun Başlığı (Header) */ diff --git a/extra-addons/mymach_social_nextgen/test_ai_manager.py b/extra-addons/mymach_social_nextgen/test_ai_manager.py new file mode 100644 index 0000000..de8c8a8 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/test_ai_manager.py @@ -0,0 +1,18 @@ +import sys + +def run_test(env): + ai_utils = env['mymach.social.ai.utils'] + print("Testing default AI provider (Gemini)...") + + try: + res = ai_utils.generate_text( + prompt="Hello, say 'Test OK'", + system_prompt="You are a test bot. Say exactly what is requested.", + response_format="text" + ) + print("Response:", res) + print("Default AI Manager Test PASSED") + except Exception as e: + print("Default AI Manager Test FAILED:", str(e)) + +run_test(env) diff --git a/extra-addons/mymach_social_nextgen/views/res_config_settings_views.xml b/extra-addons/mymach_social_nextgen/views/res_config_settings_views.xml index c31fd05..89e9f1e 100644 --- a/extra-addons/mymach_social_nextgen/views/res_config_settings_views.xml +++ b/extra-addons/mymach_social_nextgen/views/res_config_settings_views.xml @@ -60,6 +60,17 @@ + + +
+
+
+
+
@@ -68,6 +79,14 @@
+ +
+
+
+
+
@@ -92,6 +111,22 @@
+ +
+
+
+
+
+ +
+
+
+
+
@@ -104,6 +139,16 @@
+
+
+
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_account_views.xml b/extra-addons/mymach_social_nextgen/views/social_account_views.xml index 42ff218..db594df 100644 --- a/extra-addons/mymach_social_nextgen/views/social_account_views.xml +++ b/extra-addons/mymach_social_nextgen/views/social_account_views.xml @@ -7,6 +7,9 @@ mymach.social.account +
+
@@ -42,10 +45,18 @@ - + + + +
+ Geliştirici sayfasından aldığınız Access Token (Erişim Anahtarı) ve Platform ID (Instagram Business ID vb.) değerlerini buraya manuel girerek OAuth sürecini atlayabilirsiniz. +
+ + +
diff --git a/extra-addons/mymach_social_nextgen/views/social_analytics_views.xml b/extra-addons/mymach_social_nextgen/views/social_analytics_views.xml index fbd0315..cb55730 100644 --- a/extra-addons/mymach_social_nextgen/views/social_analytics_views.xml +++ b/extra-addons/mymach_social_nextgen/views/social_analytics_views.xml @@ -51,20 +51,5 @@
- - - Analiz Dashboard - mymach.social.post - graph,pivot,kanban,list - {'search_default_state': 'published'} - -

- Sosyal Medya Performansınızı Takip Edin! -

-

- Gönderilerinizi yayınladıkça etkileşim grafikleriniz burada belirecektir. -

-
-
diff --git a/extra-addons/mymach_social_nextgen/views/social_auto_reply_views.xml b/extra-addons/mymach_social_nextgen/views/social_auto_reply_views.xml index 944fe11..5d199c3 100644 --- a/extra-addons/mymach_social_nextgen/views/social_auto_reply_views.xml +++ b/extra-addons/mymach_social_nextgen/views/social_auto_reply_views.xml @@ -21,7 +21,9 @@ - + + + @@ -30,8 +32,21 @@ + + - + + @@ -46,7 +61,10 @@ - + + + +
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 6a33dc6..3c3e326 100644 --- a/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml +++ b/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml @@ -1,6 +1,19 @@ + + + social.competitor.history.list + social.competitor.history + + + + + + + + + social.competitor.list @@ -8,8 +21,10 @@ + + @@ -23,28 +38,93 @@
-

- +

- + + + + + + + + - - + + + + + + + + + + + + +