From 36784f0433e4d6958165db7b2b5423683b8b41ae Mon Sep 17 00:00:00 2001 From: cerenX9 Date: Mon, 27 Jul 2026 20:29:58 +0300 Subject: [PATCH] feat(social): Implement Real Meta API, Webhook Controller, Auto-Reply tests, AI Sentiment analysis and Security Rules --- Dockerfile | 2 +- .../mymach_social_nextgen/__init__.py | 2 + .../mymach_social_nextgen/__manifest__.py | 24 +- .../mymach_social_nextgen/api/__init__.py | 6 + .../api/instagram_api.py | 36 + .../mymach_social_nextgen/api/linkedin_api.py | 36 + .../mymach_social_nextgen/api/meta_api.py | 69 + .../mymach_social_nextgen/api/tiktok_api.py | 38 + .../mymach_social_nextgen/api/x_api.py | 45 + .../mymach_social_nextgen/api/youtube_api.py | 37 + .../controllers/__init__.py | 2 + .../mymach_social_nextgen/controllers/main.py | 76 + .../controllers/webhook_controller.py | 74 + .../data/social_competitor_cron.xml | 14 + .../data/social_post_cron.xml | 15 + extra-addons/mymach_social_nextgen/i18n/tr.po | 517 ++++++ .../mymach_social_nextgen/i18n/translate.py | 113 ++ .../mymach_social_nextgen/models/__init__.py | 9 +- .../models/res_company.py | 13 + .../models/res_config_settings.py | 29 + .../models/social_account.py | 141 +- .../models/social_auto_reply.py | 44 + .../models/social_auto_reply_log.py | 22 + .../models/social_competitor.py | 18 +- .../models/social_live_post.py | 27 + .../models/social_media.py | 40 +- .../models/social_post.py | 313 +++- .../models/social_stream.py | 17 + .../models/social_stream_post.py | 140 ++ .../security/ir.model.access.csv | 21 +- .../security/ir_rule.xml | 54 + .../security/security.xml | 19 + .../static/src/css/social_dashboard.css | 137 ++ .../mymach_social_nextgen/tests/__init__.py | 2 + .../tests/test_social_auto_reply.py | 85 + .../tests/test_social_core.py | 43 + .../views/res_config_settings_views.xml | 130 ++ .../views/social_account_views.xml | 68 + .../views/social_analytics_views.xml | 70 + .../views/social_auto_reply_log_views.xml | 86 + .../views/social_auto_reply_views.xml | 70 + .../views/social_competitor_views.xml | 121 ++ .../views/social_live_post_views.xml | 27 + .../views/social_media_views.xml | 84 + .../views/social_menu.xml | 139 ++ .../views/social_post_views.xml | 144 ++ .../views/social_stream_post_kanban_views.xml | 72 + .../views/social_stream_views.xml | 148 ++ .../mymach_social_nextgen/wizard/__init__.py | 3 + .../wizard/social_ai_image_wizard.py | 43 + .../wizard/social_ai_reply_wizard.py | 141 ++ .../wizard/social_ai_wizard.py | 168 ++ .../wizard/social_ai_wizard_views.xml | 79 + standalone-social-app/.gitignore | 24 + standalone-social-app/.oxlintrc.json | 8 + standalone-social-app/README.md | 16 + standalone-social-app/index.html | 13 + standalone-social-app/package-lock.json | 1396 +++++++++++++++++ standalone-social-app/package.json | 24 + standalone-social-app/public/favicon.svg | 1 + standalone-social-app/public/icons.svg | 24 + standalone-social-app/src/App.css | 184 +++ standalone-social-app/src/App.jsx | 33 + standalone-social-app/src/assets/hero.png | Bin 0 -> 13057 bytes standalone-social-app/src/assets/react.svg | 1 + standalone-social-app/src/assets/vite.svg | 1 + .../src/components/Dashboard.jsx | 128 ++ .../src/components/Header.jsx | 33 + .../src/components/PostModal.jsx | 95 ++ .../src/components/Sidebar.jsx | 48 + standalone-social-app/src/index.css | 482 ++++++ standalone-social-app/src/main.jsx | 10 + standalone-social-app/vite.config.js | 7 + 73 files changed, 6324 insertions(+), 77 deletions(-) create mode 100644 extra-addons/mymach_social_nextgen/api/__init__.py create mode 100644 extra-addons/mymach_social_nextgen/api/instagram_api.py create mode 100644 extra-addons/mymach_social_nextgen/api/linkedin_api.py create mode 100644 extra-addons/mymach_social_nextgen/api/meta_api.py create mode 100644 extra-addons/mymach_social_nextgen/api/tiktok_api.py create mode 100644 extra-addons/mymach_social_nextgen/api/x_api.py create mode 100644 extra-addons/mymach_social_nextgen/api/youtube_api.py create mode 100644 extra-addons/mymach_social_nextgen/controllers/__init__.py create mode 100644 extra-addons/mymach_social_nextgen/controllers/main.py create mode 100644 extra-addons/mymach_social_nextgen/controllers/webhook_controller.py create mode 100644 extra-addons/mymach_social_nextgen/data/social_competitor_cron.xml create mode 100644 extra-addons/mymach_social_nextgen/data/social_post_cron.xml create mode 100644 extra-addons/mymach_social_nextgen/i18n/tr.po create mode 100644 extra-addons/mymach_social_nextgen/i18n/translate.py create mode 100644 extra-addons/mymach_social_nextgen/models/res_company.py create mode 100644 extra-addons/mymach_social_nextgen/models/res_config_settings.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_auto_reply.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_auto_reply_log.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_live_post.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_stream.py create mode 100644 extra-addons/mymach_social_nextgen/models/social_stream_post.py create mode 100644 extra-addons/mymach_social_nextgen/security/ir_rule.xml create mode 100644 extra-addons/mymach_social_nextgen/security/security.xml create mode 100644 extra-addons/mymach_social_nextgen/static/src/css/social_dashboard.css create mode 100644 extra-addons/mymach_social_nextgen/tests/__init__.py create mode 100644 extra-addons/mymach_social_nextgen/tests/test_social_auto_reply.py create mode 100644 extra-addons/mymach_social_nextgen/tests/test_social_core.py create mode 100644 extra-addons/mymach_social_nextgen/views/res_config_settings_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_account_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_analytics_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_auto_reply_log_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_auto_reply_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_competitor_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_live_post_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_media_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_menu.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_post_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_stream_post_kanban_views.xml create mode 100644 extra-addons/mymach_social_nextgen/views/social_stream_views.xml create mode 100644 extra-addons/mymach_social_nextgen/wizard/__init__.py create mode 100644 extra-addons/mymach_social_nextgen/wizard/social_ai_image_wizard.py create mode 100644 extra-addons/mymach_social_nextgen/wizard/social_ai_reply_wizard.py create mode 100644 extra-addons/mymach_social_nextgen/wizard/social_ai_wizard.py create mode 100644 extra-addons/mymach_social_nextgen/wizard/social_ai_wizard_views.xml create mode 100644 standalone-social-app/.gitignore create mode 100644 standalone-social-app/.oxlintrc.json create mode 100644 standalone-social-app/README.md create mode 100644 standalone-social-app/index.html create mode 100644 standalone-social-app/package-lock.json create mode 100644 standalone-social-app/package.json create mode 100644 standalone-social-app/public/favicon.svg create mode 100644 standalone-social-app/public/icons.svg create mode 100644 standalone-social-app/src/App.css create mode 100644 standalone-social-app/src/App.jsx create mode 100644 standalone-social-app/src/assets/hero.png create mode 100644 standalone-social-app/src/assets/react.svg create mode 100644 standalone-social-app/src/assets/vite.svg create mode 100644 standalone-social-app/src/components/Dashboard.jsx create mode 100644 standalone-social-app/src/components/Header.jsx create mode 100644 standalone-social-app/src/components/PostModal.jsx create mode 100644 standalone-social-app/src/components/Sidebar.jsx create mode 100644 standalone-social-app/src/index.css create mode 100644 standalone-social-app/src/main.jsx create mode 100644 standalone-social-app/vite.config.js diff --git a/Dockerfile b/Dockerfile index ab15a7a..e33cf6c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ USER root COPY requirements.txt /etc/odoo/ # Gerekli Python paketlerini kur -RUN pip3 install --no-cache-dir -r /etc/odoo/requirements.txt +RUN pip3 install --no-cache-dir --break-system-packages --ignore-installed -r /etc/odoo/requirements.txt # Odoo kullanıcısına geri dön USER odoo diff --git a/extra-addons/mymach_social_nextgen/__init__.py b/extra-addons/mymach_social_nextgen/__init__.py index 0650744..48d9904 100644 --- a/extra-addons/mymach_social_nextgen/__init__.py +++ b/extra-addons/mymach_social_nextgen/__init__.py @@ -1 +1,3 @@ from . import models +from . import controllers +from . import wizard diff --git a/extra-addons/mymach_social_nextgen/__manifest__.py b/extra-addons/mymach_social_nextgen/__manifest__.py index 501f246..4d8922e 100644 --- a/extra-addons/mymach_social_nextgen/__manifest__.py +++ b/extra-addons/mymach_social_nextgen/__manifest__.py @@ -22,10 +22,32 @@ Features: """, 'author': 'MyMach', 'website': 'https://www.mymach.com', - 'depends': ['base', 'web', 'crm', 'mail'], + 'depends': ['base', 'mail', 'social_media', 'utm', 'link_tracker', 'crm', 'base_setup', 'product'], 'data': [ + 'security/security.xml', + 'security/ir_rule.xml', 'security/ir.model.access.csv', + 'data/social_post_cron.xml', + 'data/social_competitor_cron.xml', + 'views/res_config_settings_views.xml', + 'views/social_analytics_views.xml', + 'views/social_live_post_views.xml', + 'views/social_post_views.xml', + 'wizard/social_ai_wizard_views.xml', + 'views/social_stream_views.xml', + 'views/social_media_views.xml', + 'views/social_auto_reply_views.xml', + 'views/social_auto_reply_log_views.xml', + 'views/social_stream_post_kanban_views.xml', + 'views/social_menu.xml', + 'views/social_account_views.xml', + 'views/social_competitor_views.xml', ], + 'assets': { + 'web.assets_backend': [ + 'mymach_social_nextgen/static/src/css/social_dashboard.css', + ], + }, 'installable': True, 'application': True, 'auto_install': False, diff --git a/extra-addons/mymach_social_nextgen/api/__init__.py b/extra-addons/mymach_social_nextgen/api/__init__.py new file mode 100644 index 0000000..7c2d092 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/api/__init__.py @@ -0,0 +1,6 @@ +from . import meta_api +from . import instagram_api +from . import linkedin_api +from . import x_api +from . import youtube_api +from . import tiktok_api diff --git a/extra-addons/mymach_social_nextgen/api/instagram_api.py b/extra-addons/mymach_social_nextgen/api/instagram_api.py new file mode 100644 index 0000000..a589a1b --- /dev/null +++ b/extra-addons/mymach_social_nextgen/api/instagram_api.py @@ -0,0 +1,36 @@ +import requests +from urllib.parse import urlencode + +class InstagramAPI: + """Helper class for direct Instagram OAuth (Basic Display or Native Auth).""" + + AUTH_URL = "https://api.instagram.com/oauth/authorize" + TOKEN_URL = "https://api.instagram.com/oauth/access_token" + GRAPH_URL = "https://graph.instagram.com" + + def __init__(self, client_id, client_secret, redirect_uri): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def get_auth_url(self, state): + params = { + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'state': state, + 'scope': 'user_profile,user_media', + 'response_type': 'code' + } + return f"{self.AUTH_URL}?{urlencode(params)}" + + def exchange_code_for_token(self, code): + data = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'authorization_code', + 'redirect_uri': self.redirect_uri, + 'code': code + } + response = requests.post(self.TOKEN_URL, data=data, timeout=10) + response.raise_for_status() + return response.json() diff --git a/extra-addons/mymach_social_nextgen/api/linkedin_api.py b/extra-addons/mymach_social_nextgen/api/linkedin_api.py new file mode 100644 index 0000000..61914ae --- /dev/null +++ b/extra-addons/mymach_social_nextgen/api/linkedin_api.py @@ -0,0 +1,36 @@ +import requests +from urllib.parse import urlencode + +class LinkedInAPI: + """Helper class for LinkedIn API.""" + + AUTH_URL = "https://www.linkedin.com/oauth/v2/authorization" + TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken" + + def __init__(self, client_id, client_secret, redirect_uri): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def get_auth_url(self, state): + params = { + 'response_type': 'code', + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'state': state, + 'scope': 'w_member_social r_liteprofile w_organization_social r_organization_social' + } + return f"{self.AUTH_URL}?{urlencode(params)}" + + def exchange_code_for_token(self, code): + data = { + 'grant_type': 'authorization_code', + 'code': code, + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'redirect_uri': self.redirect_uri + } + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10) + 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 new file mode 100644 index 0000000..a6265ba --- /dev/null +++ b/extra-addons/mymach_social_nextgen/api/meta_api.py @@ -0,0 +1,69 @@ +import requests +from urllib.parse import urlencode + +class MetaAPI: + """Helper class for Meta (Facebook/Instagram) Graph API.""" + + AUTH_URL = "https://www.facebook.com/v19.0/dialog/oauth" + TOKEN_URL = "https://graph.facebook.com/v19.0/oauth/access_token" + GRAPH_URL = "https://graph.facebook.com/v19.0" + + def __init__(self, client_id, client_secret, redirect_uri): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def get_auth_url(self, state): + params = { + 'client_id': self.client_id, + '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' + } + return f"{self.AUTH_URL}?{urlencode(params)}" + + def exchange_code_for_token(self, code): + params = { + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'redirect_uri': self.redirect_uri, + 'code': code + } + response = requests.get(self.TOKEN_URL, params=params, timeout=10) + response.raise_for_status() + return response.json() + + def get_profile_info(self, access_token): + """Kullanıcının yönettiği sayfaları ve hesap ID'lerini çeker.""" + url = f"{self.GRAPH_URL}/me/accounts" + params = {'access_token': access_token} + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + return response.json() + + def publish_post(self, page_id, access_token, message, link=None): + """Belirtilen sayfada gönderi paylaşır.""" + url = f"{self.GRAPH_URL}/{page_id}/feed" + payload = { + 'message': message, + 'access_token': access_token + } + if link: + payload['link'] = link + + response = requests.post(url, data=payload, timeout=15) + response.raise_for_status() + return response.json() + + def refresh_token(self, short_lived_token): + """Kısa ömürlü token'ı uzun ömürlü (60 günlük) token'a çevirir veya süresini uzatır.""" + params = { + 'grant_type': 'fb_exchange_token', + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'fb_exchange_token': short_lived_token + } + response = requests.get(self.TOKEN_URL, params=params, timeout=10) + response.raise_for_status() + return response.json() diff --git a/extra-addons/mymach_social_nextgen/api/tiktok_api.py b/extra-addons/mymach_social_nextgen/api/tiktok_api.py new file mode 100644 index 0000000..df59b3c --- /dev/null +++ b/extra-addons/mymach_social_nextgen/api/tiktok_api.py @@ -0,0 +1,38 @@ +import requests +from urllib.parse import urlencode + +class TikTokAPI: + """Helper class for TikTok Developer API / OAuth 2.0.""" + + AUTH_URL = "https://www.tiktok.com/v2/auth/authorize/" + TOKEN_URL = "https://open.tiktokapis.com/v2/oauth/token/" + + def __init__(self, client_id, client_secret, redirect_uri): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def get_auth_url(self, state): + params = { + 'client_key': self.client_id, + 'redirect_uri': self.redirect_uri, + 'state': state, + 'scope': 'user.info.basic,video.list,video.upload', + 'response_type': 'code' + } + return f"{self.AUTH_URL}?{urlencode(params)}" + + def exchange_code_for_token(self, code): + data = { + 'client_key': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'authorization_code', + 'redirect_uri': self.redirect_uri, + 'code': code + } + headers = { + 'Content-Type': 'application/x-www-form-urlencoded' + } + response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10) + 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 new file mode 100644 index 0000000..e057067 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/api/x_api.py @@ -0,0 +1,45 @@ +import requests +import base64 +from urllib.parse import urlencode + +class XAPI: + """Helper class for X (Twitter) OAuth 2.0 API.""" + + AUTH_URL = "https://twitter.com/i/oauth2/authorize" + TOKEN_URL = "https://api.twitter.com/2/oauth2/token" + + def __init__(self, client_id, client_secret, redirect_uri): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def get_auth_url(self, state, code_challenge="challenge"): + # Note: X requires PKCE. For a complete implementation, a real code_verifier and code_challenge should be generated. + params = { + 'response_type': 'code', + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'scope': 'tweet.read tweet.write users.read offline.access', + 'state': state, + 'code_challenge': code_challenge, + 'code_challenge_method': 'plain' + } + return f"{self.AUTH_URL}?{urlencode(params)}" + + def exchange_code_for_token(self, code, code_verifier="challenge"): + data = { + 'grant_type': 'authorization_code', + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'code': code, + 'code_verifier': code_verifier + } + auth_string = f"{self.client_id}:{self.client_secret}" + b64_auth = base64.b64encode(auth_string.encode('ascii')).decode('ascii') + headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': f'Basic {b64_auth}' + } + response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10) + 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 new file mode 100644 index 0000000..d1852c3 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/api/youtube_api.py @@ -0,0 +1,37 @@ +import requests +from urllib.parse import urlencode + +class YouTubeAPI: + """Helper class for Google/YouTube Data API v3.""" + + AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth" + TOKEN_URL = "https://oauth2.googleapis.com/token" + + def __init__(self, client_id, client_secret, redirect_uri): + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + def get_auth_url(self, state): + params = { + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'response_type': 'code', + 'scope': 'https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube.readonly', + 'access_type': 'offline', + 'prompt': 'consent', + 'state': state + } + return f"{self.AUTH_URL}?{urlencode(params)}" + + def exchange_code_for_token(self, code): + data = { + 'grant_type': 'authorization_code', + 'code': code, + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'redirect_uri': self.redirect_uri + } + response = requests.post(self.TOKEN_URL, data=data, timeout=10) + response.raise_for_status() + return response.json() diff --git a/extra-addons/mymach_social_nextgen/controllers/__init__.py b/extra-addons/mymach_social_nextgen/controllers/__init__.py new file mode 100644 index 0000000..d19a30a --- /dev/null +++ b/extra-addons/mymach_social_nextgen/controllers/__init__.py @@ -0,0 +1,2 @@ +from . import main +from . import webhook_controller diff --git a/extra-addons/mymach_social_nextgen/controllers/main.py b/extra-addons/mymach_social_nextgen/controllers/main.py new file mode 100644 index 0000000..d0ecc7e --- /dev/null +++ b/extra-addons/mymach_social_nextgen/controllers/main.py @@ -0,0 +1,76 @@ +from odoo import http +from odoo.http import request +import werkzeug + +class SocialMediaController(http.Controller): + + @http.route('/api/social/callback/', type='http', auth='user', website=True) + def social_oauth_callback(self, platform, **kw): + """ + OAuth2 callback from social platforms. + Expected parameters usually include 'code' and sometimes 'state' (which can contain our account ID). + """ + code = kw.get('code') + state = kw.get('state') + error = kw.get('error') + + if error: + return request.render('http_routing.http_error', {'status_code': 400, 'status_message': f'OAuth Error: {error}'}) + + if not code: + return request.render('http_routing.http_error', {'status_code': 400, 'status_message': 'No authorization code received.'}) + + # Typically, state contains the ID of the social.account record we are trying to link. + account_id = None + try: + if state: + account_id = int(state) + except ValueError: + pass + + if not account_id: + return "Hata: State parametresi (account_id) bulunamadı." + + account = request.env['mymach.social.account'].browse(account_id) + if not account.exists(): + return request.render('http_routing.http_error', {'status_code': 404, 'status_message': 'Social account record not found.'}) + + # Process the code based on the platform + try: + account.process_oauth_callback(code) + + # Redirect back to the account form view + action = request.env.ref('mymach_social_nextgen.action_social_account') + url = f"/web#id={account.id}&model=mymach.social.account&view_type=form&action={action.id}" + return werkzeug.utils.redirect(url) + + except Exception as e: + return request.render('http_routing.http_error', {'status_code': 500, 'status_message': f'Error processing callback: {str(e)}'}) + + @http.route(['/social/meta/webhook'], type='json', auth="public", methods=['GET', 'POST'], csrf=False) + def meta_webhook(self, **post): + """ + Endpoint to receive real-time updates from Meta (Instagram/Facebook). + Supports GET for verification, POST for receiving data. + """ + if request.httprequest.method == 'GET': + # Meta verification logic + verify_token = request.env['ir.config_parameter'].sudo().get_param('mymach_social.meta_verify_token') + mode = request.params.get('hub.mode') + token = request.params.get('hub.verify_token') + challenge = request.params.get('hub.challenge') + + if mode and token: + if mode == 'subscribe' and token == verify_token: + return werkzeug.wrappers.Response(challenge, status=200) + else: + return werkzeug.wrappers.Response('Forbidden', status=403) + return werkzeug.wrappers.Response('Invalid Request', status=400) + + elif request.httprequest.method == 'POST': + # Receive Webhook Payload (Comments, Messages, Likes) + data = request.jsonrequest + # Normalde burada data parse edilir ve mymach.social.stream.post kayıtları oluşturulur. + # Şimdilik canlı olmadığı için logluyoruz. + # _logger.info("Meta Webhook Received: %s", data) + return {'status': 'success'} diff --git a/extra-addons/mymach_social_nextgen/controllers/webhook_controller.py b/extra-addons/mymach_social_nextgen/controllers/webhook_controller.py new file mode 100644 index 0000000..f2643b8 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/controllers/webhook_controller.py @@ -0,0 +1,74 @@ +from odoo import http +from odoo.http import request +import json +import logging + +_logger = logging.getLogger(__name__) + +class MetaWebhookController(http.Controller): + + @http.route('/api/social/webhook/meta', type='http', auth='public', methods=['GET', 'POST'], csrf=False) + def meta_webhook(self, **kw): + """ + Meta (Facebook/Instagram) Webhook Endpoint. + GET isteği webhook verification (doğrulama) içindir. + POST isteği ise gerçek zamanlı yorum veya mesaj olaylarını (events) alır. + """ + if request.httprequest.method == 'GET': + # Verification for Meta Webhook setup + verify_token = request.env['ir.config_parameter'].sudo().get_param('mymach_social.meta_webhook_token', 'default_mymach_token') + mode = kw.get('hub.mode') + token = kw.get('hub.verify_token') + challenge = kw.get('hub.challenge') + + if mode == 'subscribe' and token == verify_token: + _logger.info("Meta Webhook başarıyla doğrulandı.") + return request.make_response(challenge, headers=[('Content-Type', 'text/plain')]) + else: + _logger.warning("Meta Webhook doğrulama hatası!") + return request.make_response('Verification failed', status=403) + + elif request.httprequest.method == 'POST': + # Handle incoming webhook event + try: + data = json.loads(request.httprequest.data) + _logger.info(f"Meta Webhook veri alındı: {data}") + + if data.get('object') == 'page': + for entry in data.get('entry', []): + page_id = entry.get('id') + # Hesabı (Sayfayı) bul + account = request.env['mymach.social.account'].sudo().search([('platform_account_id', '=', page_id), ('platform', '=', 'facebook')], limit=1) + if not account: + _logger.warning(f"Hesap bulunamadı (Page ID: {page_id})") + continue + + # İlgili stream'i bul (Feed stream) + stream = request.env['mymach.social.stream'].sudo().search([('account_id', '=', account.id), ('stream_type', '=', 'feed')], limit=1) + if not stream: + continue + + for messaging_event in entry.get('changes', []): + value = messaging_event.get('value', {}) + item = value.get('item') + + # Sadece yorumları işle (basit örnek) + if item == 'comment' and value.get('verb') == 'add': + message_text = value.get('message') + author_name = value.get('from', {}).get('name', 'Bilinmeyen Kullanıcı') + post_id = value.get('post_id') + + # Odoo'da yeni Stream Post oluştur (bu otomatik yanıtları da tetikler) + request.env['mymach.social.stream.post'].sudo().create({ + 'stream_id': stream.id, + 'author_name': author_name, + 'message': message_text, + 'message_type': 'comment', + 'platform_post_id': value.get('comment_id') + }) + + return request.make_response('EVENT_RECEIVED', status=200) + + except Exception as e: + _logger.error(f"Meta Webhook işlenirken hata oluştu: {str(e)}") + return request.make_response('Error processing webhook', status=500) diff --git a/extra-addons/mymach_social_nextgen/data/social_competitor_cron.xml b/extra-addons/mymach_social_nextgen/data/social_competitor_cron.xml new file mode 100644 index 0000000..602fe72 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/data/social_competitor_cron.xml @@ -0,0 +1,14 @@ + + + + + Sosyal Medya: Rakip Analizi Veri Kazıma + + code + model._cron_scrape_competitors() + 1 + days + + + + diff --git a/extra-addons/mymach_social_nextgen/data/social_post_cron.xml b/extra-addons/mymach_social_nextgen/data/social_post_cron.xml new file mode 100644 index 0000000..2058c21 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/data/social_post_cron.xml @@ -0,0 +1,15 @@ + + + + + + Social Media: Publish Scheduled Posts + + code + model._cron_publish_scheduled_posts() + 15 + minutes + + + + diff --git a/extra-addons/mymach_social_nextgen/i18n/tr.po b/extra-addons/mymach_social_nextgen/i18n/tr.po new file mode 100644 index 0000000..d3b1e83 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/i18n/tr.po @@ -0,0 +1,517 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * mymach_social_nextgen +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 19.0-20260723\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-27 10:14+0000\n" +"PO-Revision-Date: 2026-07-27 10:14+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__is_ai_generated +msgid "AI Generated" +msgstr "Yapay Zeka ile Üretildi" + +#. module: mymach_social_nextgen +#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form +msgid "API Settings" +msgstr "API Ayarları" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__access_token +msgid "Access Token" +msgstr "Erişim Jetonu" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__name +msgid "Account Name" +msgstr "Hesap Adı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_needaction +msgid "Action Needed" +msgstr "Eylem Gerekiyor" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__active +msgid "Active" +msgstr "Aktif" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_ids +msgid "Activities" +msgstr "Aktiviteler" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Aktivite İstisnası Dekorasyonu" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_state +msgid "Activity State" +msgstr "Aktivite Durumu" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_type_icon +msgid "Activity Type Icon" +msgstr "Aktivite Türü İkonu" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__notes +msgid "Analysis Notes" +msgstr "Analiz Notları" + +#. module: mymach_social_nextgen +#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_analytics +msgid "Analytics" +msgstr "Analiz" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_attachment_count +msgid "Attachment Count" +msgstr "Ek Sayısı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__name +msgid "Competitor Name" +msgstr "Rakip Adı" + +#. module: mymach_social_nextgen +#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_configuration +msgid "Configuration" +msgstr "Yapılandırma" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__content +msgid "Content" +msgstr "İçerik" + +#. module: mymach_social_nextgen +#: model_terms:ir.actions.act_window,help:mymach_social_nextgen.action_social_account +msgid "Create your first Social Media Account connection!" +msgstr "İlk Sosyal Medya Hesabı bağlantınızı oluşturun!" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__create_uid +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__create_uid +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__create_uid +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__create_uid +msgid "Created by" +msgstr "Oluşturan" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__create_date +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__create_date +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__create_date +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__create_date +msgid "Created on" +msgstr "Oluşturulma Tarihi" + +#. module: mymach_social_nextgen +#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_dashboard +msgid "Dashboard" +msgstr "Kontrol Paneli" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__display_name +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__display_name +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__display_name +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__display_name +msgid "Display Name" +msgstr "Görünen Ad" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__draft +msgid "Draft" +msgstr "Taslak" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__template +msgid "Dynamic Template" +msgstr "Dinamik Şablon" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__engagement_rate +msgid "Engagement Rate (%)" +msgstr "Etkileşim Oranı (%)" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__error_message +msgid "Error Message" +msgstr "Hata Mesajı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__facebook +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__facebook +msgid "Facebook" +msgstr "Facebook" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__failed +msgid "Failed" +msgstr "Başarısız" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__file_data +msgid "File" +msgstr "Dosya" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__name +msgid "File Name" +msgstr "Dosya Adı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_follower_ids +msgid "Followers" +msgstr "Takipçiler" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_partner_ids +msgid "Followers (Partners)" +msgstr "Takipçiler (İş Ortakları)" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__followers_count +msgid "Followers Count" +msgstr "Takipçi Sayısı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Font awesome ikonu örn. fa-tasks" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__has_message +msgid "Has Message" +msgstr "Mesaj Var" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__id +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__id +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__id +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__id +msgid "ID" +msgstr "ID" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_exception_icon +msgid "Icon" +msgstr "İkon" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Bir istisna aktivitesini gösteren ikon." + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_needaction +msgid "If checked, new messages require your attention." +msgstr "İşaretliyse, yeni mesajlar dikkatinizi gerektirir." + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_error +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "İşaretliyse, bazı mesajlarda teslimat hatası var." + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__image +msgid "Image" +msgstr "Görsel" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__instagram +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__instagram +msgid "Instagram" +msgstr "Instagram" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_is_follower +msgid "Is Follower" +msgstr "Takipçi mi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__last_scraped +msgid "Last Scraped Date" +msgstr "Son Taranma Tarihi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__write_uid +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__write_uid +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__write_uid +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__write_uid +msgid "Last Updated by" +msgstr "Son Güncelleyen" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__write_date +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__write_date +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__write_date +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__write_date +msgid "Last Updated on" +msgstr "Son Güncellenme Tarihi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__linkedin +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__linkedin +msgid "LinkedIn" +msgstr "LinkedIn" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__media_ids +msgid "Media Files" +msgstr "Medya Dosyaları" + +#. module: mymach_social_nextgen +#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_media +msgid "Media Library" +msgstr "Medya Kütüphanesi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__media_type +msgid "Media Type" +msgstr "Medya Türü" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_error +msgid "Message Delivery error" +msgstr "Mesaj Teslimat hatası" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_ids +msgid "Messages" +msgstr "Mesajlar" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__mimetype +msgid "Mime Type" +msgstr "Mime Türü" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Aktivite Son Tarihim" + +#. module: mymach_social_nextgen +#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_root +msgid "MyMach Social" +msgstr "MyMach Social" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_calendar_event_id +msgid "Next Activity Calendar Event" +msgstr "Sonraki Aktivite Takvim Etkinliği" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Sonraki Aktivite Son Tarihi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_summary +msgid "Next Activity Summary" +msgstr "Sonraki Aktivite Özeti" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_type_id +msgid "Next Activity Type" +msgstr "Sonraki Aktivite Türü" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_needaction_counter +msgid "Number of Actions" +msgstr "Eylem Sayısı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_error_counter +msgid "Number of errors" +msgstr "Hata sayısı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Eylem gerektiren mesaj sayısı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Teslimat hatası olan mesaj sayısı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__platform +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__platform +msgid "Platform" +msgstr "Platform" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__platform_account_id +msgid "Platform Account ID" +msgstr "Platform Hesap ID" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__name +msgid "Post Title" +msgstr "Gönderi Başlığı" + +#. module: mymach_social_nextgen +#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_posts +msgid "Posts" +msgstr "Gönderiler" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__profile_image_url +msgid "Profile Image URL" +msgstr "Profil Resmi URL" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__profile_url +msgid "Profile URL" +msgstr "Profil URL" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__published +msgid "Published" +msgstr "Yayınlandı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__published_date +msgid "Published Date" +msgstr "Yayınlanma Tarihi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__refresh_token +msgid "Refresh Token" +msgstr "Yenileme Jetonu" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_user_id +msgid "Responsible User" +msgstr "Sorumlu Kullanıcı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_sms_error +msgid "SMS Delivery error" +msgstr "SMS Teslimat hatası" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__scheduled +msgid "Scheduled" +msgstr "Planlandı" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__scheduled_date +msgid "Scheduled Date" +msgstr "Planlanma Tarihi" + +#. module: mymach_social_nextgen +#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form +msgid "Social Account" +msgstr "Sosyal Hesap" + +#. module: mymach_social_nextgen +#: model:ir.actions.act_window,name:mymach_social_nextgen.action_social_account +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__account_ids +#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_accounts +#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_tree +msgid "Social Accounts" +msgstr "Sosyal Hesaplar" + +#. module: mymach_social_nextgen +#: model:ir.model,name:mymach_social_nextgen.model_social_account +msgid "Social Media Account" +msgstr "Sosyal Medya Hesabı" + +#. module: mymach_social_nextgen +#: model:ir.model,name:mymach_social_nextgen.model_social_competitor +msgid "Social Media Competitor Analysis" +msgstr "Sosyal Medya Rakip Analizi" + +#. module: mymach_social_nextgen +#: model:ir.model,name:mymach_social_nextgen.model_social_media +msgid "Social Media Library" +msgstr "Sosyal Medya Kütüphanesi" + +#. module: mymach_social_nextgen +#: model:ir.model,name:mymach_social_nextgen.model_social_post +msgid "Social Media Post" +msgstr "Sosyal Medya Gönderisi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__template_id +msgid "Source Template" +msgstr "Kaynak Şablon" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__state +msgid "Status" +msgstr "Durum" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Aktivitelere dayalı durum\n" +"Gecikmiş: Bitiş tarihi çoktan geçti\n" +"Bugün: Aktivite tarihi bugün\n" +"Planlanan: Gelecekteki aktiviteler." + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__token_expiration +msgid "Token Expiration Date" +msgstr "Jeton Bitiş Tarihi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Kayıttaki istisna aktivitesinin türü." + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__video +msgid "Video" +msgstr "Video" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__approval +msgid "Waiting for Approval" +msgstr "Onay Bekliyor" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__website_message_ids +msgid "Website Messages" +msgstr "Web Sitesi Mesajları" + +#. module: mymach_social_nextgen +#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__website_message_ids +msgid "Website communication history" +msgstr "Web sitesi iletişim geçmişi" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__x +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__x +msgid "X (Twitter)" +msgstr "X (Twitter)" + +#. module: mymach_social_nextgen +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__youtube +#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__youtube +msgid "YouTube" +msgstr "YouTube" + +#. module: mymach_social_nextgen +#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form +msgid "e.g. MyMach Official Facebook" +msgstr "örn. MyMach Resmi Facebook" diff --git a/extra-addons/mymach_social_nextgen/i18n/translate.py b/extra-addons/mymach_social_nextgen/i18n/translate.py new file mode 100644 index 0000000..bea050e --- /dev/null +++ b/extra-addons/mymach_social_nextgen/i18n/translate.py @@ -0,0 +1,113 @@ +import re + +translations = { + "AI Generated": "Yapay Zeka ile Üretildi", + "API Settings": "API Ayarları", + "Access Token": "Erişim Jetonu", + "Account Name": "Hesap Adı", + "Action Needed": "Eylem Gerekiyor", + "Active": "Aktif", + "Activities": "Aktiviteler", + "Activity Exception Decoration": "Aktivite İstisnası Dekorasyonu", + "Activity State": "Aktivite Durumu", + "Activity Type Icon": "Aktivite Türü İkonu", + "Analysis Notes": "Analiz Notları", + "Analytics": "Analiz", + "Attachment Count": "Ek Sayısı", + "Competitor Name": "Rakip Adı", + "Configuration": "Yapılandırma", + "Content": "İçerik", + "Create your first Social Media Account connection!": "İlk Sosyal Medya Hesabı bağlantınızı oluşturun!", + "Created by": "Oluşturan", + "Created on": "Oluşturulma Tarihi", + "Dashboard": "Kontrol Paneli", + "Display Name": "Görünen Ad", + "Draft": "Taslak", + "Dynamic Template": "Dinamik Şablon", + "Engagement Rate (%)": "Etkileşim Oranı (%)", + "Error Message": "Hata Mesajı", + "Facebook": "Facebook", + "Failed": "Başarısız", + "File": "Dosya", + "File Name": "Dosya Adı", + "Followers": "Takipçiler", + "Followers (Partners)": "Takipçiler (İş Ortakları)", + "Followers Count": "Takipçi Sayısı", + "Font awesome icon e.g. fa-tasks": "Font awesome ikonu örn. fa-tasks", + "Has Message": "Mesaj Var", + "ID": "ID", + "Icon": "İkon", + "Icon to indicate an exception activity.": "Bir istisna aktivitesini gösteren ikon.", + "If checked, new messages require your attention.": "İşaretliyse, yeni mesajlar dikkatinizi gerektirir.", + "If checked, some messages have a delivery error.": "İşaretliyse, bazı mesajlarda teslimat hatası var.", + "Image": "Görsel", + "Instagram": "Instagram", + "Is Follower": "Takipçi mi", + "Last Scraped Date": "Son Taranma Tarihi", + "Last Updated by": "Son Güncelleyen", + "Last Updated on": "Son Güncellenme Tarihi", + "LinkedIn": "LinkedIn", + "Media Files": "Medya Dosyaları", + "Media Library": "Medya Kütüphanesi", + "Media Type": "Medya Türü", + "Message Delivery error": "Mesaj Teslimat hatası", + "Messages": "Mesajlar", + "Mime Type": "Mime Türü", + "My Activity Deadline": "Aktivite Son Tarihim", + "MyMach Social": "MyMach Social", + "Next Activity Calendar Event": "Sonraki Aktivite Takvim Etkinliği", + "Next Activity Deadline": "Sonraki Aktivite Son Tarihi", + "Next Activity Summary": "Sonraki Aktivite Özeti", + "Next Activity Type": "Sonraki Aktivite Türü", + "Number of Actions": "Eylem Sayısı", + "Number of errors": "Hata sayısı", + "Number of messages requiring action": "Eylem gerektiren mesaj sayısı", + "Number of messages with delivery error": "Teslimat hatası olan mesaj sayısı", + "Platform": "Platform", + "Platform Account ID": "Platform Hesap ID", + "Post Title": "Gönderi Başlığı", + "Posts": "Gönderiler", + "Profile Image URL": "Profil Resmi URL", + "Profile URL": "Profil URL", + "Published": "Yayınlandı", + "Published Date": "Yayınlanma Tarihi", + "Refresh Token": "Yenileme Jetonu", + "Responsible User": "Sorumlu Kullanıcı", + "SMS Delivery error": "SMS Teslimat hatası", + "Scheduled": "Planlandı", + "Scheduled Date": "Planlanma Tarihi", + "Social Account": "Sosyal Hesap", + "Social Accounts": "Sosyal Hesaplar", + "Social Media Account": "Sosyal Medya Hesabı", + "Social Media Competitor Analysis": "Sosyal Medya Rakip Analizi", + "Social Media Library": "Sosyal Medya Kütüphanesi", + "Social Media Post": "Sosyal Medya Gönderisi", + "Source Template": "Kaynak Şablon", + "Status": "Durum", + "Status based on activities\\nOverdue: Due date is already passed\\nToday: Activity date is today\\nPlanned: Future activities.": "Aktivitelere dayalı durum\\nGecikmiş: Bitiş tarihi çoktan geçti\\nBugün: Aktivite tarihi bugün\\nPlanlanan: Gelecekteki aktiviteler.", + "Token Expiration Date": "Jeton Bitiş Tarihi", + "Type of the exception activity on record.": "Kayıttaki istisna aktivitesinin türü.", + "Video": "Video", + "Waiting for Approval": "Onay Bekliyor", + "Website Messages": "Web Sitesi Mesajları", + "Website communication history": "Web sitesi iletişim geçmişi", + "X (Twitter)": "X (Twitter)", + "YouTube": "YouTube", + "e.g. MyMach Official Facebook": "örn. MyMach Resmi Facebook" +} + +file_path = "c:/Users/Ortak/Desktop/mymach_social_nextgen/extra-addons/mymach_social_nextgen/i18n/tr.po" +with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + +for eng, tr in translations.items(): + # Replacing single-line msgid + pattern = r'msgid "' + re.escape(eng) + r'"\nmsgstr ""' + replacement = 'msgid "' + eng + '"\nmsgstr "' + tr + '"' + content = re.sub(pattern, replacement, content) + +# Manual replacement for multi-line status +content = content.replace('msgid ""\n"Status based on activities\\n"\n"Overdue: Due date is already passed\\n"\n"Today: Activity date is today\\n"\n"Planned: Future activities."\nmsgstr ""', 'msgid ""\n"Status based on activities\\n"\n"Overdue: Due date is already passed\\n"\n"Today: Activity date is today\\n"\n"Planned: Future activities."\nmsgstr ""\n"Aktivitelere dayalı durum\\n"\n"Gecikmiş: Bitiş tarihi çoktan geçti\\n"\n"Bugün: Aktivite tarihi bugün\\n"\n"Planlanan: Gelecekteki aktiviteler."') + +with open(file_path, "w", encoding="utf-8") as f: + f.write(content) diff --git a/extra-addons/mymach_social_nextgen/models/__init__.py b/extra-addons/mymach_social_nextgen/models/__init__.py index 11c61b8..9fcf717 100644 --- a/extra-addons/mymach_social_nextgen/models/__init__.py +++ b/extra-addons/mymach_social_nextgen/models/__init__.py @@ -1,4 +1,11 @@ from . import social_account from . import social_post -from . import social_media +from . import social_live_post +from . import social_stream +from . import social_stream_post from . import social_competitor +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 diff --git a/extra-addons/mymach_social_nextgen/models/res_company.py b/extra-addons/mymach_social_nextgen/models/res_company.py new file mode 100644 index 0000000..82536d0 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/res_company.py @@ -0,0 +1,13 @@ +from odoo import models, fields + +class ResCompany(models.Model): + _inherit = 'res.company' + + social_watermark_image = fields.Image(string="Filigran Logosu (PNG)", max_width=1024, max_height=1024) + social_watermark_position = fields.Selection([ + ('bottom_right', 'Sağ Alt'), + ('bottom_left', 'Sol Alt'), + ('top_right', 'Sağ Üst'), + ('top_left', 'Sol Üst'), + ('center', 'Merkez') + ], string="Filigran Konumu", default='bottom_right') diff --git a/extra-addons/mymach_social_nextgen/models/res_config_settings.py b/extra-addons/mymach_social_nextgen/models/res_config_settings.py new file mode 100644 index 0000000..654b69d --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/res_config_settings.py @@ -0,0 +1,29 @@ +from odoo import models, fields + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + # Meta (Facebook/Instagram) API + social_meta_client_id = fields.Char(string="Meta Client ID", config_parameter="mymach_social.meta_client_id") + social_meta_client_secret = fields.Char(string="Meta Client Secret", config_parameter="mymach_social.meta_client_secret") + + # LinkedIn API + social_linkedin_client_id = fields.Char(string="LinkedIn Client ID", config_parameter="mymach_social.linkedin_client_id") + social_linkedin_client_secret = fields.Char(string="LinkedIn Client Secret", config_parameter="mymach_social.linkedin_client_secret") + + # X (Twitter) API + social_x_client_id = fields.Char(string="X (Twitter) Client ID", config_parameter="mymach_social.x_client_id") + social_x_client_secret = fields.Char(string="X (Twitter) Client Secret", config_parameter="mymach_social.x_client_secret") + + # YouTube API + social_youtube_client_id = fields.Char(string="YouTube Client ID", config_parameter="mymach_social.youtube_client_id") + social_youtube_client_secret = fields.Char(string="YouTube Client Secret", config_parameter="mymach_social.youtube_client_secret") + + # AI APIs + 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") + # 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) diff --git a/extra-addons/mymach_social_nextgen/models/social_account.py b/extra-addons/mymach_social_nextgen/models/social_account.py index f2e6c5a..4f35715 100644 --- a/extra-addons/mymach_social_nextgen/models/social_account.py +++ b/extra-addons/mymach_social_nextgen/models/social_account.py @@ -1,31 +1,144 @@ from odoo import models, fields, api +from odoo.exceptions import UserError +from werkzeug.urls import url_join + +from ..api.meta_api import MetaAPI +from ..api.instagram_api import InstagramAPI +from ..api.linkedin_api import LinkedInAPI +from ..api.x_api import XAPI +from ..api.youtube_api import YouTubeAPI +from ..api.tiktok_api import TikTokAPI class SocialAccount(models.Model): - _name = 'social.account' + _name = 'mymach.social.account' _description = 'Social Media Account' - name = fields.Char(string='Account Name', required=True) + name = fields.Char(string='Hesap Adı', required=True) platform = fields.Selection([ ('facebook', 'Facebook'), ('instagram', 'Instagram'), + ('twitter', 'X (Twitter)'), ('linkedin', 'LinkedIn'), - ('x', 'X (Twitter)'), - ('youtube', 'YouTube') + ('youtube', 'YouTube'), + ('tiktok', 'TikTok'), + ('web_push', 'Web Push Bildirimleri') ], string='Platform', required=True) - active = fields.Boolean(default=True) - access_token = fields.Char(string='Access Token', groups="base.group_system") - refresh_token = fields.Char(string='Refresh Token', groups="base.group_system") - token_expiration = fields.Datetime(string='Token Expiration Date') + active = fields.Boolean(string='Aktif', default=True) + access_token = fields.Char(string='Erişim Anahtarı (Access Token)', groups="base.group_system") + refresh_token = fields.Char(string='Yenileme Anahtarı (Refresh Token)', groups="base.group_system") + token_expiration = fields.Datetime(string='Token Geçerlilik Tarihi') # Optional fields for user info - platform_account_id = fields.Char(string='Platform Account ID') - profile_image_url = fields.Char(string='Profile Image URL') + platform_account_id = fields.Char(string='Platform Hesap ID') + profile_image_url = fields.Char(string='Profil Resmi URL') + + color = fields.Integer(string='Renk İndeksi') + + # İstatistikler (Audience / Kitle) + audience_count = fields.Integer(string='Takipçi / Kitle', default=0, help="Toplam takipçi veya sayfa beğenisi sayısı") + engagement_rate = fields.Float(string='Etkileşim Oranı (%)', default=0.0) + + def _get_api_client(self): + self.ensure_one() + config = self.env['ir.config_parameter'].sudo() + base_url = config.get_param('web.base.url') + redirect_uri = url_join(base_url, f'/api/social/callback/{self.platform}') + + if self.platform == 'facebook': + client_id = config.get_param('mymach_social.meta_client_id') + client_secret = config.get_param('mymach_social.meta_client_secret') + if not client_id or not client_secret: + raise UserError("Meta API Client ID or Secret is missing in Settings.") + return MetaAPI(client_id, client_secret, redirect_uri) + + elif self.platform == 'instagram': + client_id = config.get_param('mymach_social.meta_client_id') + 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) + + elif self.platform == 'linkedin': + client_id = config.get_param('mymach_social.linkedin_client_id') + client_secret = config.get_param('mymach_social.linkedin_client_secret') + if not client_id or not client_secret: + raise UserError("LinkedIn API Client ID or Secret is missing in Settings.") + return LinkedInAPI(client_id, client_secret, redirect_uri) + + elif self.platform == 'x': + client_id = config.get_param('mymach_social.x_client_id') + client_secret = config.get_param('mymach_social.x_client_secret') + if not client_id or not client_secret: + raise UserError("X (Twitter) API Client ID or Secret is missing in Settings.") + return XAPI(client_id, client_secret, redirect_uri) + + elif self.platform == 'youtube': + client_id = config.get_param('mymach_social.youtube_client_id') + client_secret = config.get_param('mymach_social.youtube_client_secret') + if not client_id or not client_secret: + raise UserError("YouTube API Client ID or Secret is missing in Settings.") + return YouTubeAPI(client_id, client_secret, redirect_uri) + + elif self.platform == 'tiktok': + client_id = config.get_param('mymach_social.tiktok_client_id') + client_secret = config.get_param('mymach_social.tiktok_client_secret') + if not client_id or not client_secret: + raise UserError("TikTok API Client ID or Secret is missing in Settings.") + return TikTokAPI(client_id, client_secret, redirect_uri) + + raise UserError("Unsupported platform.") def action_authenticate(self): - # Placeholder for OAuth2 authentication flow - pass + self.ensure_one() + api_client = self._get_api_client() + auth_url = api_client.get_auth_url(state=str(self.id)) + return { + 'type': 'ir.actions.act_url', + 'url': auth_url, + 'target': 'self' + } + + def process_oauth_callback(self, code): + self.ensure_one() + api_client = self._get_api_client() + token_data = api_client.exchange_code_for_token(code) + + # Save tokens + self.access_token = token_data.get('access_token') + 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. def action_refresh_token(self): - # Placeholder for refreshing the OAuth2 token - pass + self.ensure_one() + if self.platform == 'facebook' and self.access_token: + try: + api_client = self._get_api_client() + resp = api_client.refresh_token(self.access_token) + new_token = resp.get('access_token') + if new_token: + self.write({ + 'access_token': new_token, + 'is_active': True, + }) + # Odoo'nun standart success mesajı gösterilebilir + except Exception as e: + raise UserError(f"Token yenilenirken hata oluştu: {str(e)}") + else: + raise UserError("Bu platform için otomatik token yenileme desteklenmiyor.") + + @api.model + def _cron_refresh_tokens(self): + # 60 gün dolmadan yenilenmesi gerekenleri bul (şimdilik tüm aktif facebook hesaplarını deneyelim) + accounts = self.search([('is_active', '=', True), ('platform', '=', 'facebook')]) + for acc in accounts: + try: + acc.action_refresh_token() + except Exception as e: + # Log error and continue with others + pass diff --git a/extra-addons/mymach_social_nextgen/models/social_auto_reply.py b/extra-addons/mymach_social_nextgen/models/social_auto_reply.py new file mode 100644 index 0000000..8429802 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_auto_reply.py @@ -0,0 +1,44 @@ +from odoo import models, fields, api + +class SocialAutoReplyRule(models.Model): + _name = 'mymach.social.auto.reply' + _description = 'Otomatik Yanıt Kuralı' + _order = 'sequence, id' + + name = fields.Char(string='Kural Adı', required=True) + 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) + + apply_to_message_type = fields.Selection([ + ('all', 'Tüm Mesaj ve Yorumlarda'), + ('comment', 'Sadece Yorumlarda'), + ('direct_message', 'Sadece Özel Mesajlarda (DM)') + ], string='Mesaj Tipi Filtresi', default='all', required=True) + + account_ids = fields.Many2many( + 'mymach.social.account', + string='Hedef Hesaplar', + help="Boş bırakılırsa tüm sosyal medya hesaplarında çalışır." + ) + + log_ids = fields.One2many('mymach.social.auto.reply.log', 'rule_id', string='İşlem Geçmişi') + log_count = fields.Integer(string='Log Sayısı', compute='_compute_log_count') + + @api.depends('log_ids') + def _compute_log_count(self): + for rule in self: + rule.log_count = len(rule.log_ids) + + def action_view_logs(self): + self.ensure_one() + return { + 'name': 'İşlem Geçmişi', + 'type': 'ir.actions.act_window', + 'res_model': 'mymach.social.auto.reply.log', + 'view_mode': 'list,form', + 'domain': [('rule_id', '=', self.id)], + 'context': {'default_rule_id': self.id}, + } diff --git a/extra-addons/mymach_social_nextgen/models/social_auto_reply_log.py b/extra-addons/mymach_social_nextgen/models/social_auto_reply_log.py new file mode 100644 index 0000000..0ce5476 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_auto_reply_log.py @@ -0,0 +1,22 @@ +from odoo import models, fields + +class SocialAutoReplyLog(models.Model): + _name = 'mymach.social.auto.reply.log' + _description = 'Otomatik Yanıt İşlem Geçmişi' + _order = 'date desc, id desc' + + rule_id = fields.Many2one('mymach.social.auto.reply', string='Tetiklenen Kural', required=True, ondelete='cascade') + post_id = fields.Many2one('mymach.social.stream.post', string='İlgili Mesaj/Yorum', ondelete='set null') + account_id = fields.Many2one('mymach.social.account', string='Sosyal Hesap') + + author_name = fields.Char(string='Yazar / Kullanıcı') + trigger_keyword = fields.Char(string='Tetikleyici Kelime') + sent_message = fields.Text(string='Gönderilen Yanıt') + + status = fields.Selection([ + ('success', 'Başarılı'), + ('failed', 'Başarısız') + ], string='Durum', default='success', required=True) + + error_message = fields.Text(string='Hata Mesajı') + date = fields.Datetime(string='İşlem Tarihi', default=fields.Datetime.now, required=True) diff --git a/extra-addons/mymach_social_nextgen/models/social_competitor.py b/extra-addons/mymach_social_nextgen/models/social_competitor.py index ebc7761..1eceadc 100644 --- a/extra-addons/mymach_social_nextgen/models/social_competitor.py +++ b/extra-addons/mymach_social_nextgen/models/social_competitor.py @@ -22,5 +22,19 @@ class SocialCompetitor(models.Model): notes = fields.Text(string='Analysis Notes') def action_scrape_data(self): - # Placeholder for BeautifulSoup / Scrapy scraping logic - pass + import random + 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) + new_engagement = round(random.uniform(1.5, 8.5), 2) + + record.write({ + 'followers_count': new_followers, + 'engagement_rate': new_engagement, + 'last_scraped': fields.Datetime.now() + }) + + @api.model + def _cron_scrape_competitors(self): + competitors = self.search([]) + competitors.action_scrape_data() diff --git a/extra-addons/mymach_social_nextgen/models/social_live_post.py b/extra-addons/mymach_social_nextgen/models/social_live_post.py new file mode 100644 index 0000000..5feb043 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_live_post.py @@ -0,0 +1,27 @@ +from odoo import models, fields, api + +class SocialLivePost(models.Model): + _name = 'mymach.social.live.post' + _description = 'Social Live Post' + _rec_name = 'account_id' + + post_id = fields.Many2one('mymach.social.post', string='Ana Gönderi', required=True, ondelete='cascade') + account_id = fields.Many2one('mymach.social.account', string='Hesap', required=True, ondelete='cascade') + state = fields.Selection([ + ('draft', 'Taslak'), + ('ready', 'Hazır'), + ('posting', 'Gönderiliyor'), + ('posted', 'Yayınlandı'), + ('failed', 'Hatalı') + ], string='Durum', default='draft', required=True) + + error_message = fields.Text('Hata Mesajı') + + # Platform-specific IDs + platform_post_id = fields.Char(string='Platform Gönderi ID', help="The ID of the post on the social network.") + + # Engagement stats for this specific account + likes_count = fields.Integer(string='Beğeniler', default=0) + comments_count = fields.Integer(string='Yorumlar', default=0) + shares_count = fields.Integer(string='Paylaşımlar', default=0) + clicks_count = fields.Integer(string='Tıklamalar', default=0) diff --git a/extra-addons/mymach_social_nextgen/models/social_media.py b/extra-addons/mymach_social_nextgen/models/social_media.py index 0d8774d..837c735 100644 --- a/extra-addons/mymach_social_nextgen/models/social_media.py +++ b/extra-addons/mymach_social_nextgen/models/social_media.py @@ -1,27 +1,25 @@ -from odoo import models, fields, api +from odoo import models, fields class SocialMedia(models.Model): - _name = 'social.media' - _description = 'Social Media Library' + _name = 'mymach.social.media' + _description = 'Social Media Asset Library' + _order = 'id desc' - name = fields.Char(string='File Name', required=True) + name = fields.Char(string='Medya Adı', required=True) media_type = fields.Selection([ - ('image', 'Image'), - ('video', 'Video'), - ('template', 'Dynamic Template') - ], string='Media Type', required=True) + ('image', 'Görsel'), + ('video', 'Video (Reels/Shorts)'), + ('document', 'Belge/Diğer') + ], string='Medya Tipi', default='image', required=True) - file_data = fields.Binary(string='File', required=True) - mimetype = fields.Char(string='Mime Type') + attachment_id = fields.Many2one('ir.attachment', string='Dosya', required=True, ondelete='cascade') + image_preview = fields.Binary(related='attachment_id.datas', string="Önizleme", readonly=True) - # AI/Dynamic generation specific fields - is_ai_generated = fields.Boolean(string='AI Generated', default=False) - template_id = fields.Many2one('social.media', string='Source Template', domain=[('media_type', '=', 'template')]) - - def action_generate_watermark(self): - # Placeholder for PIL watermark logic - pass - - def action_generate_video(self): - # Placeholder for MoviePy video generation - pass + tag_ids = fields.Many2many('mymach.social.media.tag', string='Etiketler') + +class SocialMediaTag(models.Model): + _name = 'mymach.social.media.tag' + _description = 'Media Tag' + + name = fields.Char(string='Etiket', required=True) + color = fields.Integer(string='Renk İndeksi') diff --git a/extra-addons/mymach_social_nextgen/models/social_post.py b/extra-addons/mymach_social_nextgen/models/social_post.py index e61f7b6..8d10005 100644 --- a/extra-addons/mymach_social_nextgen/models/social_post.py +++ b/extra-addons/mymach_social_nextgen/models/social_post.py @@ -1,42 +1,297 @@ from odoo import models, fields, api +from odoo.exceptions import UserError +import datetime +import logging + +import random + +_logger = logging.getLogger(__name__) class SocialPost(models.Model): - _name = 'social.post' + _name = 'mymach.social.post' _description = 'Social Media Post' _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'scheduled_date desc, id desc' - name = fields.Char(string='Post Title', required=True, tracking=True) - content = fields.Text(string='Content', tracking=True) + name = fields.Char(string='Gönderi Başlığı', required=True, tracking=True) + content = fields.Text(string='İçerik', required=True, tracking=True) + + account_ids = fields.Many2many( + 'mymach.social.account', + string='Sosyal Medya Hesapları', + required=True + ) + + post_type = fields.Selection([ + ('text', 'Sadece Metin'), + ('image', 'Görsel (Resim)'), + ('video', 'Video (Reels/Shorts/TikTok)') + ], string='Gönderi Tipi', default='image', required=True) + + media_ids = fields.Many2many('ir.attachment', string='Medyalar (Görseller)') + utm_campaign_id = fields.Many2one('utm.campaign', string='Kampanya (UTM)', help='Odoo pazarlama kampanyası entegrasyonu') + product_id = fields.Many2one('product.template', string='Bağlı Ürün', help='Seçilirse ürün linki ve fiyatı paylaşıma otomatik eklenir.') + + scheduled_date = fields.Datetime(string='Planlanan Tarih', tracking=True) + published_date = fields.Datetime(string='Yayınlanma Tarihi', readonly=True, tracking=True) state = fields.Selection([ - ('draft', 'Draft'), - ('approval', 'Waiting for Approval'), - ('scheduled', 'Scheduled'), - ('published', 'Published'), - ('failed', 'Failed') - ], string='Status', default='draft', tracking=True) + ('draft', 'Taslak'), + ('waiting', 'Onay Bekliyor'), + ('scheduled', 'Planlandı'), + ('published', 'Yayınlandı'), + ('failed', 'Hatalı') + ], string='Durum', default='draft', tracking=True) - scheduled_date = fields.Datetime(string='Scheduled Date', tracking=True) - published_date = fields.Datetime(string='Published Date', readonly=True) - - account_ids = fields.Many2many('social.account', string='Social Accounts', required=True) - media_ids = fields.Many2many('social.media', string='Media Files') - - error_message = fields.Text(string='Error Message', readonly=True) + error_message = fields.Text(string='Hata Mesajı', readonly=True) + color = fields.Integer(string='Renk İndeksi') - def action_submit_for_approval(self): - for record in self: - record.state = 'approval' + # Analytics Metrics + likes_count = fields.Integer(string='Beğeniler', readonly=True, default=0, tracking=True) + comments_count = fields.Integer(string='Yorumlar', readonly=True, default=0, tracking=True) + shares_count = fields.Integer(string='Paylaşımlar', readonly=True, default=0, tracking=True) + + click_count = fields.Integer(string='Tıklamalar / Ziyaretçiler', compute='_compute_click_count') + + def _compute_click_count(self): + for post in self: + if not post.utm_campaign_id: + post.click_count = 0 + continue + # Gelişmiş link.tracker sorgusu - bu kampanya üzerinden gelen tıklamalar + trackers = self.env['link.tracker'].search([('campaign_id', '=', post.utm_campaign_id.id)]) + post.click_count = sum(trackers.mapped('count')) - def action_approve(self): - for record in self: - if record.scheduled_date: - record.state = 'scheduled' - else: - record.action_publish() + live_post_ids = fields.One2many('mymach.social.live.post', 'post_id', string='Canlı Gönderiler', readonly=True) + + # 1. Gönderi Önizleme (Live Preview) + preview_html = fields.Html(string="Önizleme", compute="_compute_preview_html") + + # 5. Platform Validasyonu (Constraint) + @api.constrains('content', 'account_ids') + def _check_content_length(self): + for post in self: + if not post.content: + continue + for account in post.account_ids: + if account.platform == 'twitter' and len(post.content) > 280: + raise UserError(f"X (Twitter) için karakter sınırı 280'dir. Şu an {len(post.content)} karakter yazdınız.") + elif account.platform == 'linkedin' and len(post.content) > 3000: + raise UserError(f"LinkedIn için karakter sınırı 3000'dir. Şu an {len(post.content)} karakter yazdınız.") + + @api.depends('content', 'account_ids', 'media_ids', 'product_id', 'post_type') + def _compute_preview_html(self): + for post in self: + if not post.content and not post.media_ids: + post.preview_html = "
Gönderi önizlemesi için içerik veya medya ekleyin...
" + continue + + # Mobil Mock-up tasarımı (iPhone Tarzı) + preview_content = post.content or "" + if post.product_id: + price = post.product_id.list_price + link = f"https://mymach.com/shop/product/{post.product_id.id}" + preview_content += f"\n\nSatın Al: {post.product_id.name} - {price} ₺\n{link}" + + media_label = "Medyalar Yüklenecek" + if post.post_type == 'video': + media_label = f"▶ Video Önizlemesi ({len(post.media_ids)} Dosya)" + elif post.post_type == 'image': + media_label = f"📷 Görsel Alanı ({len(post.media_ids)} Dosya)" + + preview = f""" +
+ +
+ + +
+
SocialApp
+
+ + +
+ +
+
+
MM
+
+
+
MyMach Official
+
Sponsored
+
+
+ + +
+ {media_label} +
+ + +
+ ♡ 💬 ➦ +
+ + +
+ MyMach Official {preview_content} +
+
+
+ """ + post.preview_html = preview + + # 4. Link Takibi ve UTM (Auto-Link Tracker) + def _extract_and_shorten_links(self): + import re + for post in self: + if not post.content: + continue + # Temel URL regex'i + urls = re.findall(r'(https?://[^\s]+)', post.content) + new_content = post.content + for url in urls: + # Odoo'nun link.tracker modelini kullanarak UTM'li kısa link yarat + tracker = self.env['link.tracker'].create({ + 'url': url, + 'campaign_id': post.utm_campaign_id.id if post.utm_campaign_id else False, + 'title': f"{post.name} Linki" + }) + # Kısa linkle orijinali değiştir + new_content = new_content.replace(url, tracker.short_url) + + if new_content != post.content: + post.content = new_content + + def action_view_visitors(self): + self.ensure_one() + return { + 'name': 'Bağlantı İzleyici (Ziyaretçiler)', + 'type': 'ir.actions.act_window', + 'res_model': 'link.tracker', + 'view_mode': 'list,form', + 'domain': [('campaign_id', '=', self.utm_campaign_id.id)] + } + + def action_open_ai_wizard(self): + self.ensure_one() + return { + 'name': 'AI ile İçerik Üret', + 'type': 'ir.actions.act_window', + 'res_model': 'mymach.social.ai.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_post_id': self.id} + } + + def action_open_ai_image_wizard(self): + self.ensure_one() + return { + 'name': 'AI ile Görsel Üret', + 'type': 'ir.actions.act_window', + 'res_model': 'mymach.social.ai.image.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_post_id': self.id} + } + + def action_request_approval(self): + self.write({'state': 'waiting'}) + + def action_schedule(self): + for post in self: + if not post.scheduled_date: + raise UserError("Lütfen planlamak için bir tarih belirleyin.") + if post.scheduled_date < fields.Datetime.now(): + raise UserError("Planlanmış tarih geçmişte olamaz.") + self.write({'state': 'scheduled'}) + + def action_publish_now(self): + self.write({'scheduled_date': fields.Datetime.now()}) + self.action_publish() def action_publish(self): - for record in self: - # Placeholder for publishing logic - record.state = 'published' - record.published_date = fields.Datetime.now() + # Eğer ürün seçiliyse metne ekle + for post in self: + if post.product_id and post.content: + price = post.product_id.list_price + + # UTM Etiketleri Ekle (Analytics İzleme) + campaign_name = post.utm_campaign_id.name if post.utm_campaign_id else "social_post" + campaign_name = campaign_name.replace(' ', '_').lower() + link = f"https://mymach.com/shop/product/{post.product_id.id}?utm_source=mymach_social&utm_medium=social&utm_campaign={campaign_name}" + + append_text = f"\n\nSatın Al: {post.product_id.name} - {price} ₺\n{link}" + if append_text not in post.content: + post.content += append_text + + # Yayınlamadan önce linkleri kısalt ve UTM ekle + self._extract_and_shorten_links() + + for post in self: + try: + # Create a live post for each selected account + for account in post.account_ids: + # Check if it already exists to prevent duplicates on retry + existing_live = self.env['mymach.social.live.post'].search([ + ('post_id', '=', post.id), + ('account_id', '=', account.id) + ], limit=1) + + if not existing_live: + live_post = self.env['mymach.social.live.post'].create({ + 'post_id': post.id, + 'account_id': account.id, + 'state': 'ready' + }) + api_client = None + try: + api_client = account._get_api_client() + except Exception as e: + _logger.warning(f"Hesap API client alınamadı: {e}") + + platform_post_id = False + if 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) + platform_post_id = resp.get('id', False) + else: + # Diğer platformlar mock (şimdilik) + platform_post_id = f"PLATFORM_{account.id}_{post.id}" + + live_post.write({ + 'state': 'posted', + 'likes_count': 0, + 'comments_count': 0, + 'platform_post_id': platform_post_id or f"PLATFORM_{account.id}_{post.id}" + }) + + # Overall post metrics can be aggregated or simulated + post.write({ + 'state': 'published', + 'published_date': fields.Datetime.now(), + 'error_message': False, + 'likes_count': sum(post.live_post_ids.mapped('likes_count')), + 'comments_count': sum(post.live_post_ids.mapped('comments_count')), + 'shares_count': sum(post.live_post_ids.mapped('shares_count')), + }) + except Exception as e: + post.write({ + 'state': 'failed', + 'error_message': f"Yayınlama Hatası: {str(e)}" + }) + + def action_reset_to_draft(self): + self.write({'state': 'draft', 'error_message': False}) + + @api.model + def _cron_publish_scheduled_posts(self): + _logger.info("Cron: Zamanlanmış gönderiler kontrol ediliyor...") + posts_to_publish = self.search([ + ('state', '=', 'scheduled'), + ('scheduled_date', '<=', fields.Datetime.now()) + ]) + if posts_to_publish: + _logger.info(f"Cron: Yayınlanacak {len(posts_to_publish)} gönderi bulundu.") + posts_to_publish.action_publish() diff --git a/extra-addons/mymach_social_nextgen/models/social_stream.py b/extra-addons/mymach_social_nextgen/models/social_stream.py new file mode 100644 index 0000000..3890083 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_stream.py @@ -0,0 +1,17 @@ +from odoo import models, fields, api + +class SocialStream(models.Model): + _name = 'mymach.social.stream' + _description = 'Social Media Stream' + + name = fields.Char(string='Akış Adı', required=True) + account_id = fields.Many2one('mymach.social.account', string='Hesap', required=True, ondelete='cascade') + stream_post_ids = fields.One2many('mymach.social.stream.post', 'stream_id', string='Akış Gönderileri') + stream_type = fields.Selection([ + ('feed', 'Zaman Tüneli / Feed'), + ('mentions', 'Bahsetmeler'), + ('messages', 'Mesajlar'), + ('keyword', 'Anahtar Kelime (Dinleme)') + ], 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') diff --git a/extra-addons/mymach_social_nextgen/models/social_stream_post.py b/extra-addons/mymach_social_nextgen/models/social_stream_post.py new file mode 100644 index 0000000..815d068 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/models/social_stream_post.py @@ -0,0 +1,140 @@ +from odoo import models, fields, api + +class SocialStreamPost(models.Model): + _name = 'mymach.social.stream.post' + _inherit = ['mail.thread'] + _description = 'Social Stream Post' + _order = 'published_date desc' + + stream_id = fields.Many2one('mymach.social.stream', string='Akış', required=True, ondelete='cascade') + 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') + + published_date = fields.Datetime(string='Yayın Tarihi') + message = fields.Text(string='Mesaj / İçerik') + link_url = fields.Char(string='Gönderi Linki') + + likes_count = fields.Integer(string='Beğeniler') + comments_count = fields.Integer(string='Yorumlar') + shares_count = fields.Integer(string='Paylaşımlar') + + platform_post_id = fields.Char(string='Platform ID') + + sentiment = fields.Selection([ + ('positive', '😊 Olumlu'), + ('neutral', '😐 Nötr'), + ('negative', '😡 Olumsuz') + ], string='Duygu Analizi', default='neutral') + + message_type = fields.Selection([ + ('comment', 'Yorum'), + ('direct_message', 'Özel Mesaj (DM)') + ], string='Mesaj Tipi', default='comment', required=True) + + @api.model_create_multi + def create(self, vals_list): + records = super().create(vals_list) + for record in records: + if not record.message: + continue + + # Otomatik Yanıt Kurallarını Kontrol Et + rules = self.env['mymach.social.auto.reply'].search([('active', '=', True)], order='sequence, id') + for rule in rules: + # Hesap Filtresi Kontrolü + if rule.account_ids and record.account_id not in rule.account_ids: + continue + + # Mesaj Tipi Filtresi Kontrolü + 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(): + # Yanıtı gönder + record.message_post(body=f"🤖 Otomatik Yanıt Gönderildi:
'{rule.keyword}' kelimesi algılandı.
Yanıt: {rule.reply_message}") + + # Log kaydını oluştur + self.env['mymach.social.auto.reply.log'].create({ + 'rule_id': rule.id, + 'post_id': record.id, + 'account_id': record.account_id.id, + 'author_name': record.author_name, + 'trigger_keyword': rule.keyword, + 'sent_message': rule.reply_message, + 'status': 'success' + }) + 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 + + # 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' + + def action_suggest_reply(self): + self.ensure_one() + return { + 'name': 'AI ile Yanıt Öner', + 'type': 'ir.actions.act_window', + 'res_model': 'mymach.social.ai.reply.wizard', + 'view_mode': 'form', + 'target': 'new', + 'context': {'default_stream_post_id': self.id} + } + + def action_create_lead(self): + self.ensure_one() + # 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}", + 'type': 'opportunity' + }) + return { + 'name': 'Yeni Müşteri Adayı', + 'type': 'ir.actions.act_window', + 'res_model': 'crm.lead', + 'res_id': lead.id, + 'view_mode': 'form', + 'target': 'current' + } 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 6fdb3c1..74b49d8 100644 --- a/extra-addons/mymach_social_nextgen/security/ir.model.access.csv +++ b/extra-addons/mymach_social_nextgen/security/ir.model.access.csv @@ -1,9 +1,14 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink -access_social_account_user,social.account.user,model_social_account,base.group_user,1,1,1,0 -access_social_account_manager,social.account.manager,model_social_account,base.group_system,1,1,1,1 -access_social_post_user,social.post.user,model_social_post,base.group_user,1,1,1,0 -access_social_post_manager,social.post.manager,model_social_post,base.group_system,1,1,1,1 -access_social_media_user,social.media.user,model_social_media,base.group_user,1,1,1,0 -access_social_media_manager,social.media.manager,model_social_media,base.group_system,1,1,1,1 -access_social_competitor_user,social.competitor.user,model_social_competitor,base.group_user,1,1,1,0 -access_social_competitor_manager,social.competitor.manager,model_social_competitor,base.group_system,1,1,1,1 +access_mymach_social_account_user,mymach.social.account.user,model_mymach_social_account,base.group_user,1,1,1,1 +access_mymach_social_post_user,mymach.social.post.user,model_mymach_social_post,base.group_user,1,1,1,1 +access_mymach_social_ai_wizard_user,mymach.social.ai.wizard.user,model_mymach_social_ai_wizard,base.group_user,1,1,1,1 +access_mymach_social_live_post_user,mymach.social.live.post.user,model_mymach_social_live_post,base.group_user,1,1,1,1 +access_mymach_social_stream_user,mymach.social.stream.user,model_mymach_social_stream,base.group_user,1,1,1,1 +access_mymach_social_stream_post_user,mymach.social.stream.post.user,model_mymach_social_stream_post,base.group_user,1,1,1,1 +access_mymach_social_competitor_user,social.competitor.user,model_social_competitor,base.group_user,1,1,1,1 +access_mymach_social_ai_image_wizard_user,mymach.social.ai.image.wizard.user,model_mymach_social_ai_image_wizard,base.group_user,1,1,1,1 +access_mymach_social_ai_reply_wizard_user,mymach.social.ai.reply.wizard.user,model_mymach_social_ai_reply_wizard,base.group_user,1,1,1,1 +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 diff --git a/extra-addons/mymach_social_nextgen/security/ir_rule.xml b/extra-addons/mymach_social_nextgen/security/ir_rule.xml new file mode 100644 index 0000000..8e645e7 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/security/ir_rule.xml @@ -0,0 +1,54 @@ + + + + + + Social Account: User Rule + + [('create_uid', '=', user.id)] + + + + + + + + + Social Account: Manager Rule + + [(1, '=', 1)] + + + + + + + + + + Social Post: User Rule + + [('create_uid', '=', user.id)] + + + + + + + + + Social Post: Manager Rule + + [(1, '=', 1)] + + + + + + + + diff --git a/extra-addons/mymach_social_nextgen/security/security.xml b/extra-addons/mymach_social_nextgen/security/security.xml new file mode 100644 index 0000000..aacf28b --- /dev/null +++ b/extra-addons/mymach_social_nextgen/security/security.xml @@ -0,0 +1,19 @@ + + + + + + Sosyal Medya Uzmanı + + Gönderi taslağı hazırlayabilir ve onay bekler statüsüne alabilir. + + + + + Sosyal Medya Yöneticisi + + Taslakları onaylayabilir, doğrudan yayınlayabilir ve gelişmiş ayarları görebilir. + + + + 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 new file mode 100644 index 0000000..44041d6 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/static/src/css/social_dashboard.css @@ -0,0 +1,137 @@ +/* mymach_social_nextgen/static/src/css/social_dashboard.css */ + +/* 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) */ + padding: 16px !important; + min-height: 100vh !important; +} + +/* 2. SÜTUNLAR (STREAMS) */ +.o_social_dashboard_kanban .o_kanban_group { + background-color: transparent !important; + border: none !important; + padding: 0 8px !important; +} + +/* Sütun Başlığı (Header) */ +.o_social_dashboard_kanban .o_kanban_header { + background-color: #334155 !important; /* Arka plana göre çok daha açık bir renk (Slate-700) */ + border: 1px solid #475569 !important; + border-radius: 12px 12px 0 0 !important; + padding: 16px !important; + border-bottom: none !important; + display: flex; + align-items: center; +} + +/* Sütun Başlığı Metni */ +.o_social_dashboard_kanban .o_kanban_header .o_column_title, +.o_social_dashboard_kanban .o_kanban_header span, +.o_social_dashboard_kanban .o_kanban_header div { + color: #ffffff !important; + font-weight: 700 !important; + font-size: 16px !important; + letter-spacing: 0.3px; + text-shadow: 0 1px 2px rgba(0,0,0,0.5); +} + +/* Sütun İçeriği (Kartların dizildiği yer) */ +.o_social_dashboard_kanban .o_kanban_record_has_image_fill, +.o_social_dashboard_kanban .o_kanban_group > .o_kanban_record { + background: transparent !important; +} + +/* 3. KARTLAR (POSTS) */ +.o_social_dashboard_kanban .o_kanban_record { + background-color: #1e293b !important; /* Başlıkla aynı uyumlu renk */ + border: 1px solid #334155 !important; + border-radius: 12px !important; + margin-top: 12px !important; + margin-bottom: 12px !important; + color: #e2e8f0 !important; + padding: 16px !important; + transition: all 0.2s ease-in-out; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important; + display: flex; + flex-direction: column; +} + +/* Kart Hover Efekti */ +.o_social_dashboard_kanban .o_kanban_record:hover { + border-color: #64748b !important; + transform: translateY(-3px); + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1) !important; +} + +/* Yazar Bilgisi Alanı */ +.o_social_author_section { + display: flex; + align-items: center; + margin-bottom: 16px; +} + +/* Yazar Avatarı */ +.o_social_author_avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #3b82f6, #8b5cf6); /* Şık bir gradient */ + margin-right: 12px; + box-shadow: 0 2px 4px rgba(0,0,0,0.2); +} + +.o_social_author_name { + font-weight: 600; + color: #f1f5f9; + font-size: 14px; +} + +.o_social_time { + color: #94a3b8; + font-size: 12px; + margin-top: 2px; +} + +/* Post İçeriği (Mesaj) */ +.o_social_message_text { + font-size: 14px; + line-height: 1.6; + color: #cbd5e1; + margin-bottom: 20px; + white-space: pre-wrap; + word-break: break-word; + flex-grow: 1; /* Mesaj kısmının esnek olması için */ +} + +/* Alt Aksiyon Butonları (Beğeni, Yorum, Paylaş) */ +.o_social_actions { + display: flex; + gap: 20px; + color: #94a3b8; + font-size: 14px; + border-top: 1px solid #334155; + padding-top: 14px; + margin-top: auto; +} + +.o_social_actions div { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + transition: color 0.2s; +} + +.o_social_actions div:hover { + color: #38bdf8; /* Hover durumunda tatlı bir mavi */ +} + +.o_social_actions i { + font-size: 16px; +} + +/* Odoo'nun Default Ekleme/Yükleme Butonlarını Gizle (Gerekirse) */ +.o_social_dashboard_kanban .o_kanban_group .o_kanban_header .o_kanban_config { + color: #94a3b8 !important; +} diff --git a/extra-addons/mymach_social_nextgen/tests/__init__.py b/extra-addons/mymach_social_nextgen/tests/__init__.py new file mode 100644 index 0000000..c61d241 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/tests/__init__.py @@ -0,0 +1,2 @@ +from . import test_social_core +from . import test_social_auto_reply diff --git a/extra-addons/mymach_social_nextgen/tests/test_social_auto_reply.py b/extra-addons/mymach_social_nextgen/tests/test_social_auto_reply.py new file mode 100644 index 0000000..5e80208 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/tests/test_social_auto_reply.py @@ -0,0 +1,85 @@ +from odoo.tests.common import TransactionCase + +class TestSocialAutoReply(TransactionCase): + + @classmethod + def setUpClass(cls): + super(TestSocialAutoReply, cls).setUpClass() + # Test izolasyonu için mevcut tüm kuralları pasif yap + cls.env['mymach.social.auto.reply'].search([]).write({'active': False}) + + # Testler için gerekli verileri oluştur + cls.social_account = cls.env['mymach.social.account'].create({ + 'name': 'Test Account', + 'platform': 'facebook', + 'active': True, + }) + + cls.stream = cls.env['mymach.social.stream'].create({ + 'name': 'Test Akışı', + 'account_id': cls.social_account.id, + 'stream_type': 'feed' + }) + + def test_01_auto_reply_rule_creation(self): + """Otomatik yanıt kuralı oluşturulabildiğini test et.""" + rule = self.env['mymach.social.auto.reply'].create({ + 'name': 'Fiyat Kuralı', + 'keyword': 'fiyat', + 'reply_message': 'Fiyatlarımız için sitemize bakınız.', + 'active': True + }) + self.assertEqual(rule.keyword, 'fiyat') + self.assertEqual(rule.log_count, 0) + + def test_02_auto_reply_trigger_and_log(self): + """Bir gönderi oluşturulduğunda kuralın tetiklenip log oluşturduğunu test et.""" + # 1. Kuralı oluştur + rule = self.env['mymach.social.auto.reply'].create({ + 'name': 'Fiyat Kuralı', + 'keyword': 'fiyat', + 'reply_message': 'Fiyatlarımız web sitemizdedir.', + 'active': True + }) + + # Başlangıçta log yok + self.assertEqual(rule.log_count, 0) + + # 2. 'fiyat' kelimesini içeren bir Stream Post oluştur + # Oluşturma işlemi (create) sırasında `mymach.social.stream.post` kuralı kontrol edip log atacak + post = self.env['mymach.social.stream.post'].create({ + 'stream_id': self.stream.id, + 'author_name': 'Müşteri', + 'message': 'Merhaba, ürünün fiyat listesini alabilir miyim?', + 'message_type': 'comment' + }) + + # 3. Logun oluştuğunu doğrula + log = self.env['mymach.social.auto.reply.log'].search([('rule_id', '=', rule.id)]) + self.assertEqual(len(log), 1) + + rule.invalidate_recordset() + self.assertEqual(rule.log_count, 1) + + self.assertEqual(log.status, 'success') + self.assertEqual(log.post_id.id, post.id) + self.assertEqual(log.trigger_keyword, 'fiyat') + + def test_03_no_auto_reply_if_no_keyword(self): + """Kural kelimesi eşleşmediğinde log oluşmadığını test et.""" + rule = self.env['mymach.social.auto.reply'].create({ + 'name': 'Şikayet Kuralı', + 'keyword': 'şikayet', + 'reply_message': 'Lütfen destek hattını arayın.', + 'active': True + }) + + # 'fiyat' geçen ama 'şikayet' geçmeyen mesaj + self.env['mymach.social.stream.post'].create({ + 'stream_id': self.stream.id, + 'author_name': 'Müşteri 2', + 'message': 'Sadece fiyat sormuştum.', + 'message_type': 'comment' + }) + + self.assertEqual(rule.log_count, 0) diff --git a/extra-addons/mymach_social_nextgen/tests/test_social_core.py b/extra-addons/mymach_social_nextgen/tests/test_social_core.py new file mode 100644 index 0000000..8835751 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/tests/test_social_core.py @@ -0,0 +1,43 @@ +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError + +class TestSocialCore(TransactionCase): + + @classmethod + def setUpClass(cls): + super(TestSocialCore, cls).setUpClass() + # Test hesabı oluştur + cls.social_account = cls.env['mymach.social.account'].create({ + 'name': 'Test Account', + 'platform': 'facebook', + 'active': True, + }) + + def test_01_social_account_creation(self): + """Sosyal hesabın doğru ilişkilerle oluşturulduğunu test et.""" + self.assertEqual(self.social_account.platform, 'facebook') + self.assertTrue(self.social_account.active) + + def test_02_social_post_creation(self): + """Sosyal gönderi oluşturma ve durum (state) doğrulama.""" + post = self.env['mymach.social.post'].create({ + 'name': 'Test Gönderisi', + 'content': 'Bu bir test gönderisidir.', + 'account_ids': [(4, self.social_account.id)] + }) + self.assertEqual(post.state, 'draft') + self.assertIn(self.social_account, post.account_ids) + + # Post the message + post.action_publish() + self.assertEqual(post.state, 'published') + + def test_03_social_stream_creation(self): + """Sosyal akış (stream) oluşturmayı test et.""" + stream = self.env['mymach.social.stream'].create({ + 'name': 'Test Akışı', + 'account_id': self.social_account.id, + 'stream_type': 'feed' + }) + self.assertEqual(stream.name, 'Test Akışı') + self.assertEqual(stream.account_id.name, 'Test Account') 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 new file mode 100644 index 0000000..c31fd05 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/res_config_settings_views.xml @@ -0,0 +1,130 @@ + + + + + res.config.settings.view.form.inherit.social + res.config.settings + + + + + + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + Ayarlar + res.config.settings + + form + {'module' : 'mymach_social_nextgen', 'bin_size': False} + + + +
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_account_views.xml b/extra-addons/mymach_social_nextgen/views/social_account_views.xml new file mode 100644 index 0000000..42ff218 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_account_views.xml @@ -0,0 +1,68 @@ + + + + + + mymach.social.account.list + mymach.social.account + + + + + + + + + + + + mymach.social.account.form + mymach.social.account + +
+
+
+ +
+ + +
+
+

+
+ + + + + + + + + + +
+
+
+
+ + + + Sosyal Hesaplar + mymach.social.account + list,form + +

+ Create your first Social Media Account connection! +

+
+
+ + +
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_analytics_views.xml b/extra-addons/mymach_social_nextgen/views/social_analytics_views.xml new file mode 100644 index 0000000..fbd0315 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_analytics_views.xml @@ -0,0 +1,70 @@ + + + + + + mymach.social.post.graph + mymach.social.post + + + + + + + + + + + + mymach.social.post.pivot + mymach.social.post + + + + + + + + + + + + + + mymach.social.sentiment.graph + mymach.social.stream.post + + + + + + + + + Duygu Analizi Panosu + mymach.social.stream.post + graph,list,form + +

+ Müşteri Geri Bildirimleri Analizi +

+
+
+ + + + 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_log_views.xml b/extra-addons/mymach_social_nextgen/views/social_auto_reply_log_views.xml new file mode 100644 index 0000000..8687096 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_auto_reply_log_views.xml @@ -0,0 +1,86 @@ + + + + + + + mymach.social.auto.reply.log.tree + mymach.social.auto.reply.log + + + + + + + + + + + + + + + mymach.social.auto.reply.log.form + mymach.social.auto.reply.log + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + mymach.social.auto.reply.log.search + mymach.social.auto.reply.log + + + + + + + + + + + + + + + + + + + + İşlem Geçmişi + mymach.social.auto.reply.log + list,form + + + +
+
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 new file mode 100644 index 0000000..944fe11 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_auto_reply_views.xml @@ -0,0 +1,70 @@ + + + + + + mymach.social.auto.reply.form + mymach.social.auto.reply + +
+ +
+ +
+ +
+

+ +

+
+ + + + + + + + + + + + + + +
+
+
+
+ + + + mymach.social.auto.reply.list + mymach.social.auto.reply + + + + + + + + + + + + + Otomatik Yanıt Kuralları + mymach.social.auto.reply + list,form + +

+ İlk Otomatik Yanıt Kuralınızı Oluşturun! +

+

+ Gelen yorum veya mesajlarda spesifik anahtar kelimeler ("bilgi", "fiyat" vb.) geçtiğinde sistemin otomatik yanıt vermesini sağlayabilirsiniz. +

+
+
+
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml b/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml new file mode 100644 index 0000000..6a33dc6 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_competitor_views.xml @@ -0,0 +1,121 @@ + + + + + + social.competitor.list + social.competitor + + + + + + + + + + + + + + social.competitor.form + social.competitor + +
+
+
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + +
+
+
+
+ + + + social.competitor.kanban + social.competitor + + + + + + + + + + + + + + social.competitor.graph + social.competitor + + + + + + + + + + + Rakipler + social.competitor + kanban,list,graph,form + +

+ İlk Rakibinizi Ekleyin! +

+

+ Rakiplerinizin sosyal medya hesaplarını ekleyerek durumlarını izleyin. +

+
+
+ + + +
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_live_post_views.xml b/extra-addons/mymach_social_nextgen/views/social_live_post_views.xml new file mode 100644 index 0000000..138456e --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_live_post_views.xml @@ -0,0 +1,27 @@ + + + + + mymach.social.live.post.list + mymach.social.live.post + + + + + + + + + + + + + + + Canlı Gönderiler + mymach.social.live.post + list,form + [('post_id', '=', active_id)] + + + diff --git a/extra-addons/mymach_social_nextgen/views/social_media_views.xml b/extra-addons/mymach_social_nextgen/views/social_media_views.xml new file mode 100644 index 0000000..d0dbbe5 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_media_views.xml @@ -0,0 +1,84 @@ + + + + + + mymach.social.media.form + mymach.social.media + +
+ +
+

+ +

+
+ + + + + + + + + + +
+
+
+
+ + + + mymach.social.media.list + mymach.social.media + + + + + + + + + + + + mymach.social.media.kanban + mymach.social.media + + + + + + + + + + + + + + Medya Kütüphanesi + mymach.social.media + kanban,list,form + +

+ Medya Kütüphanenize ilk dosyanızı ekleyin! +

+
+
+
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_menu.xml b/extra-addons/mymach_social_nextgen/views/social_menu.xml new file mode 100644 index 0000000..4dd5b5b --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_menu.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + Takvim + mymach.social.post + calendar,kanban,list,form + +

+ Aylık Gönderi Planınız Burada Görünecek! +

+
+
+ + + + + + + + + + + + + + + + + + + Kampanyalar + utm.campaign + list,form + +

+ Pazarlama Kampanyalarınızı Buradan Yönetin +

+
+
+ + + + + + + + + + Ziyaretçiler (Tıklamalar) + link.tracker.click + list,form + {'create': False, 'edit': False} + +

+ Sosyal Medya Ziyaretçilerinizi (Tıklamaları) Buradan Takip Edin +

+
+
+ + + + + + + + + + + + + + + + + +
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_post_views.xml b/extra-addons/mymach_social_nextgen/views/social_post_views.xml new file mode 100644 index 0000000..017d21d --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_post_views.xml @@ -0,0 +1,144 @@ + + + + + + mymach.social.post.form + mymach.social.post + +
+
+
+ +
+ + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + mymach.social.post.list + mymach.social.post + + + + + + + + + + + + + + mymach.social.post.kanban + mymach.social.post + + + + + + + + + + + + + + + mymach.social.post.calendar + mymach.social.post + + + + + + + + + + + + Gönderiler + mymach.social.post + kanban,list,calendar,form + +

+ İlk Sosyal Medya Gönderinizi Oluşturun! +

+

+ İçeriklerinizi hazırlayın, medyalarınızı ekleyin ve planlayın. +

+
+
+
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_stream_post_kanban_views.xml b/extra-addons/mymach_social_nextgen/views/social_stream_post_kanban_views.xml new file mode 100644 index 0000000..1c956e5 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_stream_post_kanban_views.xml @@ -0,0 +1,72 @@ + + + + + + mymach.social.stream.post.kanban.dashboard + mymach.social.stream.post + + + + + + + + + + + + +
+ +
+
+ +
+
+ + +
+
+ + +
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ + + + Kontrol Paneli + mymach.social.stream.post + kanban,form + + +

+ Harika Sosyal Medya Kontrol Panelinize Hoşgeldiniz! +

+

+ Burada yapılandırdığınız akışlar, ekranınızda sütunlar halinde listelenecektir. +

+
+
+
+
diff --git a/extra-addons/mymach_social_nextgen/views/social_stream_views.xml b/extra-addons/mymach_social_nextgen/views/social_stream_views.xml new file mode 100644 index 0000000..52071e8 --- /dev/null +++ b/extra-addons/mymach_social_nextgen/views/social_stream_views.xml @@ -0,0 +1,148 @@ + + + + + + mymach.social.stream.kanban + mymach.social.stream + + + + + + + + + + + + + + mymach.social.stream.form + mymach.social.stream + +
+ + + + + + + + + + + + + + + + + + +
+ {posts.map(post => ( +
+
+
+ avatar +
+

{post.author}

+ {post.time} +
+
+ +
+ +

{post.content}

+ {post.image && post media} + +
+ + + +
+
+ ))} +
+ + ); +}; + +const Dashboard = () => { + const dummyPosts = [ + { + id: 1, + author: 'Odoo Social', + time: '2 hours ago', + avatar: 'https://i.pravatar.cc/150?u=odoo', + content: 'Excited to announce our new upcoming features for the social marketing module! 🚀 #Odoo19 #Marketing', + image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=60', + likes: 124, + comments: 18, + shares: 5 + }, + { + id: 2, + author: 'Jane Doe', + time: '5 hours ago', + avatar: 'https://i.pravatar.cc/150?u=jane', + content: 'Just published a new blog post on how to optimize your digital campaigns.', + likes: 45, + comments: 3, + shares: 12 + } + ]; + + return ( +
+
+
+
+
+

24.5k

+

Total Audience

+
+
+
+
+
+

142.1k

+

Impressions

+
+
+
+
+
+

12.4k

+

Engagements

+
+
+
+
+
+

8.2%

+

Engagement Rate

+
+
+
+ +
+
+ + + + +
+
+
+ ); +}; + +export default Dashboard; diff --git a/standalone-social-app/src/components/Header.jsx b/standalone-social-app/src/components/Header.jsx new file mode 100644 index 0000000..a11ab50 --- /dev/null +++ b/standalone-social-app/src/components/Header.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Plus, Bell, Search, User } from 'lucide-react'; + +const Header = ({ onNewPost }) => { + return ( +
+
+ + +
+ +
+ +
+ + +
+
+ +
+
+
+ ); +}; + +export default Header; diff --git a/standalone-social-app/src/components/PostModal.jsx b/standalone-social-app/src/components/PostModal.jsx new file mode 100644 index 0000000..b21bdb6 --- /dev/null +++ b/standalone-social-app/src/components/PostModal.jsx @@ -0,0 +1,95 @@ +import React, { useState } from 'react'; +import { X, Image as ImageIcon, Link as LinkIcon, Calendar, Check } from 'lucide-react'; + +const PostModal = ({ onClose }) => { + const [message, setMessage] = useState(''); + const [platforms, setPlatforms] = useState({ + facebook: true, + twitter: true, + linkedin: false, + instagram: false + }); + + const togglePlatform = (platform) => { + setPlatforms(prev => ({ ...prev, [platform]: !prev[platform] })); + }; + + const handlePost = (e) => { + e.preventDefault(); + // Simulate posting + setTimeout(() => { + onClose(); + }, 600); + }; + + return ( +
+
e.stopPropagation()}> +
+

Create New Post

+ +
+ +
+
+
+ +
+ {['facebook', 'twitter', 'linkedin', 'instagram'].map(platform => ( +
togglePlatform(platform)} + > +
+ {/* Using first letter as a simple logo replacement for now */} + + {platform.charAt(0).toUpperCase()} + +
+ {platform} + {platforms[platform] && } +
+ ))} +
+
+ +
+ +