pygpt-net 2.6.1__py3-none-any.whl → 2.6.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygpt_net/CHANGELOG.txt +23 -0
- pygpt_net/__init__.py +3 -3
- pygpt_net/app.py +20 -1
- pygpt_net/config.py +55 -65
- pygpt_net/controller/__init__.py +5 -2
- pygpt_net/controller/calendar/note.py +101 -126
- pygpt_net/controller/chat/chat.py +38 -35
- pygpt_net/controller/chat/render.py +154 -214
- pygpt_net/controller/chat/response.py +5 -3
- pygpt_net/controller/chat/stream.py +92 -27
- pygpt_net/controller/config/config.py +39 -42
- pygpt_net/controller/config/field/checkbox.py +16 -12
- pygpt_net/controller/config/field/checkbox_list.py +36 -31
- pygpt_net/controller/config/field/cmd.py +51 -57
- pygpt_net/controller/config/field/combo.py +33 -16
- pygpt_net/controller/config/field/dictionary.py +48 -55
- pygpt_net/controller/config/field/input.py +50 -32
- pygpt_net/controller/config/field/slider.py +40 -45
- pygpt_net/controller/config/field/textarea.py +20 -6
- pygpt_net/controller/config/placeholder.py +110 -231
- pygpt_net/controller/ctx/common.py +48 -48
- pygpt_net/controller/ctx/ctx.py +91 -132
- pygpt_net/controller/lang/mapping.py +57 -95
- pygpt_net/controller/lang/plugins.py +64 -55
- pygpt_net/controller/lang/settings.py +39 -38
- pygpt_net/controller/layout/layout.py +176 -109
- pygpt_net/controller/mode/mode.py +88 -85
- pygpt_net/controller/model/model.py +73 -73
- pygpt_net/controller/plugins/plugins.py +209 -223
- pygpt_net/controller/plugins/presets.py +54 -55
- pygpt_net/controller/plugins/settings.py +54 -69
- pygpt_net/controller/presets/editor.py +33 -88
- pygpt_net/controller/presets/experts.py +20 -1
- pygpt_net/controller/presets/presets.py +293 -298
- pygpt_net/controller/settings/profile.py +16 -4
- pygpt_net/controller/theme/theme.py +72 -81
- pygpt_net/controller/ui/mode.py +118 -186
- pygpt_net/controller/ui/tabs.py +69 -90
- pygpt_net/controller/ui/ui.py +47 -56
- pygpt_net/controller/ui/vision.py +24 -23
- pygpt_net/core/agents/runner.py +15 -7
- pygpt_net/core/bridge/bridge.py +5 -5
- pygpt_net/core/command/command.py +149 -219
- pygpt_net/core/ctx/ctx.py +94 -146
- pygpt_net/core/debug/debug.py +48 -58
- pygpt_net/core/experts/experts.py +3 -3
- pygpt_net/core/models/models.py +74 -112
- pygpt_net/core/modes/modes.py +13 -21
- pygpt_net/core/plugins/plugins.py +154 -177
- pygpt_net/core/presets/presets.py +103 -176
- pygpt_net/core/render/web/body.py +217 -215
- pygpt_net/core/render/web/renderer.py +330 -474
- pygpt_net/core/text/utils.py +28 -44
- pygpt_net/core/tokens/tokens.py +104 -203
- pygpt_net/data/config/config.json +3 -3
- pygpt_net/data/config/models.json +3 -3
- pygpt_net/data/locale/locale.de.ini +2 -0
- pygpt_net/data/locale/locale.en.ini +2 -0
- pygpt_net/data/locale/locale.es.ini +2 -0
- pygpt_net/data/locale/locale.fr.ini +2 -0
- pygpt_net/data/locale/locale.it.ini +2 -0
- pygpt_net/data/locale/locale.pl.ini +3 -1
- pygpt_net/data/locale/locale.uk.ini +2 -0
- pygpt_net/data/locale/locale.zh.ini +2 -0
- pygpt_net/item/ctx.py +141 -139
- pygpt_net/plugin/agent/plugin.py +2 -1
- pygpt_net/plugin/audio_output/plugin.py +5 -2
- pygpt_net/plugin/base/plugin.py +101 -85
- pygpt_net/plugin/bitbucket/__init__.py +12 -0
- pygpt_net/plugin/bitbucket/config.py +267 -0
- pygpt_net/plugin/bitbucket/plugin.py +126 -0
- pygpt_net/plugin/bitbucket/worker.py +569 -0
- pygpt_net/plugin/cmd_code_interpreter/plugin.py +3 -2
- pygpt_net/plugin/cmd_custom/plugin.py +3 -2
- pygpt_net/plugin/cmd_files/plugin.py +3 -2
- pygpt_net/plugin/cmd_history/plugin.py +3 -2
- pygpt_net/plugin/cmd_mouse_control/plugin.py +5 -2
- pygpt_net/plugin/cmd_serial/plugin.py +3 -2
- pygpt_net/plugin/cmd_system/plugin.py +3 -6
- pygpt_net/plugin/cmd_web/plugin.py +3 -2
- pygpt_net/plugin/experts/plugin.py +2 -2
- pygpt_net/plugin/facebook/__init__.py +12 -0
- pygpt_net/plugin/facebook/config.py +359 -0
- pygpt_net/plugin/facebook/plugin.py +113 -0
- pygpt_net/plugin/facebook/worker.py +698 -0
- pygpt_net/plugin/github/__init__.py +12 -0
- pygpt_net/plugin/github/config.py +441 -0
- pygpt_net/plugin/github/plugin.py +126 -0
- pygpt_net/plugin/github/worker.py +674 -0
- pygpt_net/plugin/google/__init__.py +12 -0
- pygpt_net/plugin/google/config.py +367 -0
- pygpt_net/plugin/google/plugin.py +126 -0
- pygpt_net/plugin/google/worker.py +826 -0
- pygpt_net/plugin/idx_llama_index/plugin.py +3 -2
- pygpt_net/plugin/mailer/plugin.py +3 -5
- pygpt_net/plugin/openai_vision/plugin.py +3 -2
- pygpt_net/plugin/real_time/plugin.py +52 -60
- pygpt_net/plugin/slack/__init__.py +12 -0
- pygpt_net/plugin/slack/config.py +349 -0
- pygpt_net/plugin/slack/plugin.py +115 -0
- pygpt_net/plugin/slack/worker.py +639 -0
- pygpt_net/plugin/telegram/__init__.py +12 -0
- pygpt_net/plugin/telegram/config.py +308 -0
- pygpt_net/plugin/telegram/plugin.py +117 -0
- pygpt_net/plugin/telegram/worker.py +563 -0
- pygpt_net/plugin/twitter/__init__.py +12 -0
- pygpt_net/plugin/twitter/config.py +491 -0
- pygpt_net/plugin/twitter/plugin.py +125 -0
- pygpt_net/plugin/twitter/worker.py +837 -0
- pygpt_net/provider/agents/llama_index/legacy/openai_assistant.py +35 -3
- pygpt_net/tools/code_interpreter/tool.py +0 -1
- pygpt_net/tools/translator/tool.py +1 -1
- pygpt_net/ui/base/config_dialog.py +86 -100
- pygpt_net/ui/base/context_menu.py +48 -46
- pygpt_net/ui/dialog/preset.py +34 -77
- pygpt_net/ui/layout/ctx/ctx_list.py +10 -6
- pygpt_net/ui/layout/toolbox/presets.py +41 -41
- pygpt_net/ui/main.py +49 -31
- pygpt_net/ui/tray.py +61 -60
- pygpt_net/ui/widget/calendar/select.py +86 -70
- pygpt_net/ui/widget/lists/attachment.py +86 -44
- pygpt_net/ui/widget/lists/base_list_combo.py +85 -33
- pygpt_net/ui/widget/lists/context.py +135 -188
- pygpt_net/ui/widget/lists/preset.py +59 -61
- pygpt_net/ui/widget/textarea/web.py +161 -48
- pygpt_net/utils.py +8 -1
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/METADATA +164 -2
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/RECORD +131 -103
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/LICENSE +0 -0
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/WHEEL +0 -0
- {pygpt_net-2.6.1.dist-info → pygpt_net-2.6.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,698 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# ================================================== #
|
|
4
|
+
# This file is a part of PYGPT package #
|
|
5
|
+
# Website: https://pygpt.net #
|
|
6
|
+
# GitHub: https://github.com/szczyglis-dev/py-gpt #
|
|
7
|
+
# MIT License #
|
|
8
|
+
# Created By : Marcin Szczygliński #
|
|
9
|
+
# Updated Date: 2025.08.15 00:00:00 #
|
|
10
|
+
# ================================================== #
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import hashlib
|
|
16
|
+
import http.server
|
|
17
|
+
import json
|
|
18
|
+
import mimetypes
|
|
19
|
+
import os
|
|
20
|
+
import random
|
|
21
|
+
import socket
|
|
22
|
+
import threading
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
from typing import Any, Dict, List, Optional
|
|
26
|
+
from urllib.parse import urlencode, urlparse, parse_qs
|
|
27
|
+
|
|
28
|
+
import requests
|
|
29
|
+
from PySide6.QtCore import Slot
|
|
30
|
+
|
|
31
|
+
from pygpt_net.plugin.base.worker import BaseWorker, BaseSignals
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WorkerSignals(BaseSignals):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Worker(BaseWorker):
|
|
39
|
+
"""
|
|
40
|
+
Facebook (Meta Graph API) plugin worker:
|
|
41
|
+
OAuth2 (PKCE), Me, Pages, Posts, Media. Auto-authorization when required.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(self, *args, **kwargs):
|
|
45
|
+
super(Worker, self).__init__()
|
|
46
|
+
self.signals = WorkerSignals()
|
|
47
|
+
self.args = args
|
|
48
|
+
self.kwargs = kwargs
|
|
49
|
+
self.plugin = None
|
|
50
|
+
self.cmds = None
|
|
51
|
+
self.ctx = None
|
|
52
|
+
self.msg = None
|
|
53
|
+
|
|
54
|
+
# ---------------------- Core runner ----------------------
|
|
55
|
+
|
|
56
|
+
@Slot()
|
|
57
|
+
def run(self):
|
|
58
|
+
try:
|
|
59
|
+
responses = []
|
|
60
|
+
for item in self.cmds:
|
|
61
|
+
if self.is_stopped():
|
|
62
|
+
break
|
|
63
|
+
try:
|
|
64
|
+
response = None
|
|
65
|
+
if item["cmd"] in self.plugin.allowed_cmds and self.plugin.has_cmd(item["cmd"]):
|
|
66
|
+
|
|
67
|
+
# -------- Auth --------
|
|
68
|
+
if item["cmd"] == "fb_oauth_begin":
|
|
69
|
+
response = self.cmd_fb_oauth_begin(item)
|
|
70
|
+
elif item["cmd"] == "fb_oauth_exchange":
|
|
71
|
+
response = self.cmd_fb_oauth_exchange(item)
|
|
72
|
+
elif item["cmd"] == "fb_token_extend":
|
|
73
|
+
response = self.cmd_fb_token_extend(item)
|
|
74
|
+
|
|
75
|
+
# -------- Me --------
|
|
76
|
+
elif item["cmd"] == "fb_me":
|
|
77
|
+
response = self.cmd_fb_me(item)
|
|
78
|
+
|
|
79
|
+
# -------- Pages --------
|
|
80
|
+
elif item["cmd"] == "fb_pages_list":
|
|
81
|
+
response = self.cmd_fb_pages_list(item)
|
|
82
|
+
elif item["cmd"] == "fb_page_set_default":
|
|
83
|
+
response = self.cmd_fb_page_set_default(item)
|
|
84
|
+
|
|
85
|
+
# -------- Posts --------
|
|
86
|
+
elif item["cmd"] == "fb_page_posts":
|
|
87
|
+
response = self.cmd_fb_page_posts(item)
|
|
88
|
+
elif item["cmd"] == "fb_page_post_create":
|
|
89
|
+
response = self.cmd_fb_page_post_create(item)
|
|
90
|
+
elif item["cmd"] == "fb_page_post_delete":
|
|
91
|
+
response = self.cmd_fb_page_post_delete(item)
|
|
92
|
+
|
|
93
|
+
# -------- Media --------
|
|
94
|
+
elif item["cmd"] == "fb_page_photo_upload":
|
|
95
|
+
response = self.cmd_fb_page_photo_upload(item)
|
|
96
|
+
|
|
97
|
+
if response:
|
|
98
|
+
responses.append(response)
|
|
99
|
+
|
|
100
|
+
except Exception as e:
|
|
101
|
+
responses.append(self.make_response(item, self.throw_error(e)))
|
|
102
|
+
|
|
103
|
+
if responses:
|
|
104
|
+
self.reply_more(responses)
|
|
105
|
+
if self.msg is not None:
|
|
106
|
+
self.status(self.msg)
|
|
107
|
+
except Exception as e:
|
|
108
|
+
self.error(e)
|
|
109
|
+
finally:
|
|
110
|
+
self.cleanup()
|
|
111
|
+
|
|
112
|
+
# ---------------------- HTTP / helpers ----------------------
|
|
113
|
+
|
|
114
|
+
def _graph_version(self) -> str:
|
|
115
|
+
# expects like "v21.0"
|
|
116
|
+
v = (self.plugin.get_option_value("graph_version") or "v21.0").strip("/")
|
|
117
|
+
return v
|
|
118
|
+
|
|
119
|
+
def _api_base(self) -> str:
|
|
120
|
+
# final: https://graph.facebook.com/v21.0
|
|
121
|
+
base = (self.plugin.get_option_value("api_base") or "https://graph.facebook.com").rstrip("/")
|
|
122
|
+
return f"{base}/{self._graph_version()}"
|
|
123
|
+
|
|
124
|
+
def _auth_base(self) -> str:
|
|
125
|
+
# final: https://www.facebook.com/v21.0
|
|
126
|
+
base = (self.plugin.get_option_value("authorize_base") or "https://www.facebook.com").rstrip("/")
|
|
127
|
+
return f"{base}/{self._graph_version()}"
|
|
128
|
+
|
|
129
|
+
def _timeout(self) -> int:
|
|
130
|
+
try:
|
|
131
|
+
return int(self.plugin.get_option_value("http_timeout") or 30)
|
|
132
|
+
except Exception:
|
|
133
|
+
return 30
|
|
134
|
+
|
|
135
|
+
def _now(self) -> int:
|
|
136
|
+
return int(time.time())
|
|
137
|
+
|
|
138
|
+
def _auth_header(self, token: Optional[str] = None, user_context: bool = False) -> Dict[str, str]:
|
|
139
|
+
# prefer explicit token; else resolve user token (auto-run OAuth if enabled)
|
|
140
|
+
tok = (token or "").strip()
|
|
141
|
+
if not tok:
|
|
142
|
+
if user_context:
|
|
143
|
+
tok = self._ensure_user_token(optional=True)
|
|
144
|
+
if not tok and bool(self.plugin.get_option_value("oauth_auto_begin") or True):
|
|
145
|
+
self._auto_authorize_interactive()
|
|
146
|
+
tok = self._ensure_user_token(optional=False)
|
|
147
|
+
else:
|
|
148
|
+
tok = self._ensure_user_token(optional=True)
|
|
149
|
+
if not tok and bool(self.plugin.get_option_value("oauth_auto_begin") or True):
|
|
150
|
+
self._auto_authorize_interactive()
|
|
151
|
+
tok = self._ensure_user_token(optional=False)
|
|
152
|
+
if not tok:
|
|
153
|
+
raise RuntimeError("Missing access token. Complete Facebook OAuth first.")
|
|
154
|
+
return {
|
|
155
|
+
"Authorization": f"Bearer {tok}",
|
|
156
|
+
"User-Agent": "pygpt-net-facebook-plugin/1.0",
|
|
157
|
+
"Accept": "application/json",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
def _handle_response(self, r: requests.Response) -> dict:
|
|
161
|
+
try:
|
|
162
|
+
data = r.json() if r.content else {}
|
|
163
|
+
except Exception:
|
|
164
|
+
data = {"raw": r.text}
|
|
165
|
+
if not (200 <= r.status_code < 300):
|
|
166
|
+
# Graph errors come in "error" object
|
|
167
|
+
try:
|
|
168
|
+
err = (data or {}).get("error")
|
|
169
|
+
if err:
|
|
170
|
+
raise RuntimeError(json.dumps({"status": r.status_code, "error": err}, ensure_ascii=False))
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
raise RuntimeError(f"HTTP {r.status_code}: {data or r.text}")
|
|
174
|
+
# include rate meta if present
|
|
175
|
+
data["_meta"] = {"status": r.status_code}
|
|
176
|
+
return data
|
|
177
|
+
|
|
178
|
+
def _get(self, path: str, params: dict = None, token: Optional[str] = None, user_context: bool = False):
|
|
179
|
+
url = f"{self._api_base()}{path}"
|
|
180
|
+
headers = self._auth_header(token=token, user_context=user_context)
|
|
181
|
+
r = requests.get(url, headers=headers, params=params or {}, timeout=self._timeout())
|
|
182
|
+
return self._handle_response(r)
|
|
183
|
+
|
|
184
|
+
def _delete(self, path: str, params: dict = None, token: Optional[str] = None, user_context: bool = False):
|
|
185
|
+
url = f"{self._api_base()}{path}"
|
|
186
|
+
headers = self._auth_header(token=token, user_context=user_context)
|
|
187
|
+
r = requests.delete(url, headers=headers, params=params or {}, timeout=self._timeout())
|
|
188
|
+
return self._handle_response(r)
|
|
189
|
+
|
|
190
|
+
def _post_json(self, path: str, payload: dict, token: Optional[str] = None, user_context: bool = False):
|
|
191
|
+
url = f"{self._api_base()}{path}"
|
|
192
|
+
headers = self._auth_header(token=token, user_context=user_context)
|
|
193
|
+
headers["Content-Type"] = "application/json"
|
|
194
|
+
r = requests.post(url, headers=headers, json=payload or {}, timeout=self._timeout())
|
|
195
|
+
return self._handle_response(r)
|
|
196
|
+
|
|
197
|
+
def _post_form(self, path: str, form: dict, files: dict | None = None, token: Optional[str] = None, user_context: bool = False):
|
|
198
|
+
url = f"{self._api_base()}{path}"
|
|
199
|
+
headers = self._auth_header(token=token, user_context=user_context)
|
|
200
|
+
r = requests.post(url, headers=headers, data=form or {}, files=files, timeout=self._timeout())
|
|
201
|
+
return self._handle_response(r)
|
|
202
|
+
|
|
203
|
+
# ---------------------- Token storage ----------------------
|
|
204
|
+
|
|
205
|
+
def _ensure_user_token(self, optional: bool = False) -> Optional[str]:
|
|
206
|
+
access = (self.plugin.get_option_value("oauth2_access_token") or "").strip()
|
|
207
|
+
exp = int(self.plugin.get_option_value("oauth2_expires_at") or 0)
|
|
208
|
+
if access and exp and self._now() >= exp:
|
|
209
|
+
# Facebook does not provide a standard refresh_token. Use fb_token_extend manually if needed.
|
|
210
|
+
pass
|
|
211
|
+
if not access and not optional:
|
|
212
|
+
raise RuntimeError("User access token missing. Run OAuth first.")
|
|
213
|
+
return access or None
|
|
214
|
+
|
|
215
|
+
def _save_tokens(self, tok: dict):
|
|
216
|
+
access = tok.get("access_token")
|
|
217
|
+
expires_in = int(tok.get("expires_in") or 0)
|
|
218
|
+
expires_at = self._now() + expires_in - 60 if expires_in else 0
|
|
219
|
+
if access:
|
|
220
|
+
self.plugin.set_option_value("oauth2_access_token", access)
|
|
221
|
+
if expires_at:
|
|
222
|
+
self.plugin.set_option_value("oauth2_expires_at", str(expires_at))
|
|
223
|
+
|
|
224
|
+
# ---------------------- OAuth2 (PKCE) ----------------------
|
|
225
|
+
|
|
226
|
+
def _gen_code_verifier(self, n: int = 64) -> str:
|
|
227
|
+
alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
|
228
|
+
return "".join(random.choice(alphabet) for _ in range(n))
|
|
229
|
+
|
|
230
|
+
def _code_challenge(self, verifier: str) -> str:
|
|
231
|
+
digest = hashlib.sha256(verifier.encode("ascii")).digest()
|
|
232
|
+
return base64.urlsafe_b64encode(digest).decode().rstrip("=")
|
|
233
|
+
|
|
234
|
+
def _redirect_is_local(self, redirect_uri: str) -> bool:
|
|
235
|
+
try:
|
|
236
|
+
u = urlparse(redirect_uri)
|
|
237
|
+
return u.scheme in ("http",) and (u.hostname in ("127.0.0.1", "localhost"))
|
|
238
|
+
except Exception:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
def _can_bind(self, host: str, port: int) -> bool:
|
|
242
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
243
|
+
try:
|
|
244
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
245
|
+
s.bind((host, port))
|
|
246
|
+
return True
|
|
247
|
+
except Exception:
|
|
248
|
+
return False
|
|
249
|
+
finally:
|
|
250
|
+
try:
|
|
251
|
+
s.close()
|
|
252
|
+
except Exception:
|
|
253
|
+
pass
|
|
254
|
+
|
|
255
|
+
def _pick_port(self, host: str, preferred: int) -> int:
|
|
256
|
+
base = preferred if preferred and preferred >= 1024 else 8732
|
|
257
|
+
for p in range(base, base + 50):
|
|
258
|
+
if self._can_bind(host, p):
|
|
259
|
+
return p
|
|
260
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
261
|
+
s.bind((host, 0))
|
|
262
|
+
free_port = s.getsockname()[1]
|
|
263
|
+
s.close()
|
|
264
|
+
return free_port
|
|
265
|
+
|
|
266
|
+
def _prepare_effective_redirect(self, redirect_uri: str) -> str:
|
|
267
|
+
u = urlparse(redirect_uri)
|
|
268
|
+
host = u.hostname or "127.0.0.1"
|
|
269
|
+
scheme = u.scheme or "http"
|
|
270
|
+
path = u.path or "/"
|
|
271
|
+
if not self._redirect_is_local(redirect_uri):
|
|
272
|
+
return redirect_uri
|
|
273
|
+
port = u.port
|
|
274
|
+
allow_fallback = bool(self.plugin.get_option_value("oauth_allow_port_fallback") or True)
|
|
275
|
+
if not port or port < 1024 or not self._can_bind(host, port):
|
|
276
|
+
if not allow_fallback and (not port or port < 1024):
|
|
277
|
+
raise RuntimeError("Configured redirect uses a privileged or unavailable port. Use port >1024.")
|
|
278
|
+
pref = int(self.plugin.get_option_value("oauth_local_port") or 8732)
|
|
279
|
+
new_port = self._pick_port(host, pref)
|
|
280
|
+
return f"{scheme}://{host}:{new_port}{path}"
|
|
281
|
+
return redirect_uri
|
|
282
|
+
|
|
283
|
+
def _build_auth_url(self, scopes: str, verifier: str, state: str, nonce: str, redirect_uri: Optional[str] = None) -> str:
|
|
284
|
+
client_id = self.plugin.get_option_value("oauth2_client_id") or ""
|
|
285
|
+
redirect_uri = redirect_uri or (self.plugin.get_option_value("oauth2_redirect_uri") or "")
|
|
286
|
+
challenge = self._code_challenge(verifier)
|
|
287
|
+
# Facebook Login dialog with PKCE (OIDC-compatible)
|
|
288
|
+
return (
|
|
289
|
+
f"{self._auth_base()}/dialog/oauth?"
|
|
290
|
+
+ urlencode({
|
|
291
|
+
"client_id": client_id,
|
|
292
|
+
"response_type": "code",
|
|
293
|
+
"redirect_uri": redirect_uri,
|
|
294
|
+
"scope": scopes,
|
|
295
|
+
"state": state,
|
|
296
|
+
"code_challenge": challenge,
|
|
297
|
+
"code_challenge_method": "S256",
|
|
298
|
+
"nonce": nonce,
|
|
299
|
+
})
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def _run_local_callback_and_wait(self, auth_url: str, redirect_uri: str) -> (str, str):
|
|
303
|
+
u = urlparse(redirect_uri)
|
|
304
|
+
host = u.hostname or "127.0.0.1"
|
|
305
|
+
port = u.port or 8732
|
|
306
|
+
timeout_sec = int(self.plugin.get_option_value("oauth_local_timeout") or 180)
|
|
307
|
+
html_ok = (self.plugin.get_option_value("oauth_success_html")
|
|
308
|
+
or "<html><body><h3>Authorization complete. You can close this window.</h3></body></html>")
|
|
309
|
+
html_err = (self.plugin.get_option_value("oauth_fail_html")
|
|
310
|
+
or "<html><body><h3>Authorization failed.</h3></body></html>")
|
|
311
|
+
|
|
312
|
+
result = {"code": None, "state": None}
|
|
313
|
+
event = threading.Event()
|
|
314
|
+
|
|
315
|
+
class Handler(http.server.BaseHTTPRequestHandler):
|
|
316
|
+
def log_message(self, fmt, *args):
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
def do_GET(self):
|
|
320
|
+
try:
|
|
321
|
+
q = urlparse(self.path)
|
|
322
|
+
qs = parse_qs(q.query)
|
|
323
|
+
result["code"] = (qs.get("code") or [None])[0]
|
|
324
|
+
result["state"] = (qs.get("state") or [None])[0]
|
|
325
|
+
ok = result["code"] is not None
|
|
326
|
+
data = html_ok if ok else html_err
|
|
327
|
+
self.send_response(200 if ok else 400)
|
|
328
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
329
|
+
self.send_header("Content-Length", str(len(data.encode("utf-8"))))
|
|
330
|
+
self.end_headers()
|
|
331
|
+
self.wfile.write(data.encode("utf-8"))
|
|
332
|
+
finally:
|
|
333
|
+
event.set()
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
httpd = http.server.HTTPServer((host, port), Handler)
|
|
337
|
+
except PermissionError:
|
|
338
|
+
raise RuntimeError(f"Cannot bind local callback on {host}:{port}. Use a port >1024.")
|
|
339
|
+
except OSError as e:
|
|
340
|
+
raise RuntimeError(f"Port {port} busy on {host}. Change oauth_local_port. ({e})")
|
|
341
|
+
|
|
342
|
+
srv_thr = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
343
|
+
srv_thr.start()
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
if bool(self.plugin.get_option_value("oauth_open_browser") or True):
|
|
347
|
+
self.plugin.open_url(auth_url)
|
|
348
|
+
except Exception:
|
|
349
|
+
pass
|
|
350
|
+
|
|
351
|
+
got = event.wait(timeout=timeout_sec)
|
|
352
|
+
try:
|
|
353
|
+
httpd.shutdown()
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
srv_thr.join(timeout=5)
|
|
357
|
+
|
|
358
|
+
if not got or not result["code"]:
|
|
359
|
+
raise RuntimeError("No OAuth code received (timeout). Check redirect URI in Meta App settings.")
|
|
360
|
+
return result["code"], result["state"]
|
|
361
|
+
|
|
362
|
+
def _exchange_code_for_token(self, code: str):
|
|
363
|
+
client_id = self.plugin.get_option_value("oauth2_client_id") or ""
|
|
364
|
+
client_secret = self.plugin.get_option_value("oauth2_client_secret") or ""
|
|
365
|
+
redirect_uri = self.plugin.get_option_value("oauth2_redirect_uri") or ""
|
|
366
|
+
verifier = self.plugin.get_option_value("oauth2_code_verifier") or ""
|
|
367
|
+
if not (client_id and redirect_uri and code):
|
|
368
|
+
raise RuntimeError("Exchange failed: missing client_id/redirect_uri/code.")
|
|
369
|
+
# With PKCE we may omit client_secret and include code_verifier
|
|
370
|
+
params = {
|
|
371
|
+
"client_id": client_id,
|
|
372
|
+
"redirect_uri": redirect_uri,
|
|
373
|
+
"code": code,
|
|
374
|
+
}
|
|
375
|
+
if client_secret and bool(self.plugin.get_option_value("oauth2_confidential") or False):
|
|
376
|
+
params["client_secret"] = client_secret
|
|
377
|
+
else:
|
|
378
|
+
if not verifier:
|
|
379
|
+
raise RuntimeError("Missing code_verifier for PKCE exchange.")
|
|
380
|
+
params["code_verifier"] = verifier
|
|
381
|
+
|
|
382
|
+
url = f"{self._api_base()}/oauth/access_token"
|
|
383
|
+
r = requests.get(url, params=params, timeout=self._timeout())
|
|
384
|
+
res = self._handle_response(r)
|
|
385
|
+
self._save_tokens(res)
|
|
386
|
+
|
|
387
|
+
def _auto_authorize_interactive(self):
|
|
388
|
+
client_id = self.plugin.get_option_value("oauth2_client_id") or ""
|
|
389
|
+
redirect_cfg = self.plugin.get_option_value("oauth2_redirect_uri") or ""
|
|
390
|
+
if not (client_id and redirect_cfg):
|
|
391
|
+
raise RuntimeError("OAuth auto-start: set oauth2_client_id and oauth2_redirect_uri first.")
|
|
392
|
+
|
|
393
|
+
scopes = (self.plugin.get_option_value("oauth2_scopes") or
|
|
394
|
+
"public_profile pages_show_list pages_read_engagement pages_manage_posts pages_read_user_content openid")
|
|
395
|
+
verifier = self._gen_code_verifier()
|
|
396
|
+
state = self._gen_code_verifier(32)
|
|
397
|
+
nonce = self._gen_code_verifier(32)
|
|
398
|
+
self.plugin.set_option_value("oauth2_code_verifier", verifier)
|
|
399
|
+
self.plugin.set_option_value("oauth2_state", state)
|
|
400
|
+
self.plugin.set_option_value("oauth2_nonce", nonce)
|
|
401
|
+
|
|
402
|
+
effective_redirect = self._prepare_effective_redirect(redirect_cfg)
|
|
403
|
+
auth_url = self._build_auth_url(scopes, verifier, state, nonce, redirect_uri=effective_redirect)
|
|
404
|
+
|
|
405
|
+
if bool(self.plugin.get_option_value("oauth_open_browser") or True):
|
|
406
|
+
try:
|
|
407
|
+
self.plugin.open_url(auth_url)
|
|
408
|
+
except Exception:
|
|
409
|
+
pass
|
|
410
|
+
|
|
411
|
+
if bool(self.plugin.get_option_value("oauth_local_server") or True) and self._redirect_is_local(effective_redirect):
|
|
412
|
+
code, st = self._run_local_callback_and_wait(auth_url, effective_redirect)
|
|
413
|
+
if (self.plugin.get_option_value("oauth2_state") or "") and st and st != self.plugin.get_option_value("oauth2_state"):
|
|
414
|
+
raise RuntimeError("OAuth state mismatch.")
|
|
415
|
+
self._exchange_code_for_token(code)
|
|
416
|
+
try:
|
|
417
|
+
me = self._get("/me", params={"fields": "id,name"}, user_context=True)
|
|
418
|
+
usr = (me or {})
|
|
419
|
+
if usr.get("id"):
|
|
420
|
+
self.plugin.set_option_value("user_id", usr["id"])
|
|
421
|
+
if usr.get("name"):
|
|
422
|
+
self.plugin.set_option_value("user_name", usr["name"])
|
|
423
|
+
except Exception:
|
|
424
|
+
pass
|
|
425
|
+
self.msg = f"Facebook: Authorization complete on {effective_redirect}."
|
|
426
|
+
return
|
|
427
|
+
|
|
428
|
+
self.msg = f"Authorize in browser and run fb_oauth_exchange with 'code'. URL: {auth_url}"
|
|
429
|
+
|
|
430
|
+
# ---------------------- Auth commands ----------------------
|
|
431
|
+
|
|
432
|
+
def cmd_fb_oauth_begin(self, item: dict) -> dict:
|
|
433
|
+
p = item.get("params", {}) or {}
|
|
434
|
+
client_id = self.plugin.get_option_value("oauth2_client_id") or ""
|
|
435
|
+
redirect_cfg = self.plugin.get_option_value("oauth2_redirect_uri") or ""
|
|
436
|
+
scopes = p.get("scopes") or (self.plugin.get_option_value("oauth2_scopes") or
|
|
437
|
+
"public_profile pages_show_list pages_read_engagement pages_manage_posts pages_read_user_content openid")
|
|
438
|
+
if not (client_id and redirect_cfg):
|
|
439
|
+
return self.make_response(item, "Set oauth2_client_id and oauth2_redirect_uri in options first.")
|
|
440
|
+
verifier = self._gen_code_verifier()
|
|
441
|
+
state = p.get("state") or self._gen_code_verifier(32)
|
|
442
|
+
nonce = self._gen_code_verifier(32)
|
|
443
|
+
self.plugin.set_option_value("oauth2_code_verifier", verifier)
|
|
444
|
+
self.plugin.set_option_value("oauth2_state", state)
|
|
445
|
+
self.plugin.set_option_value("oauth2_nonce", nonce)
|
|
446
|
+
effective_redirect = self._prepare_effective_redirect(redirect_cfg)
|
|
447
|
+
auth_url = self._build_auth_url(scopes, verifier, state, nonce, redirect_uri=effective_redirect)
|
|
448
|
+
if bool(self.plugin.get_option_value("oauth_open_browser") or True):
|
|
449
|
+
try:
|
|
450
|
+
self.plugin.open_url(auth_url)
|
|
451
|
+
except Exception:
|
|
452
|
+
pass
|
|
453
|
+
return self.make_response(item, {"authorize_url": auth_url, "redirect_uri": effective_redirect})
|
|
454
|
+
|
|
455
|
+
def cmd_fb_oauth_exchange(self, item: dict) -> dict:
|
|
456
|
+
p = item.get("params", {}) or {}
|
|
457
|
+
code = p.get("code")
|
|
458
|
+
state = p.get("state")
|
|
459
|
+
expected_state = self.plugin.get_option_value("oauth2_state") or ""
|
|
460
|
+
if not code:
|
|
461
|
+
return self.make_response(item, "Param 'code' required.")
|
|
462
|
+
if expected_state and state and state != expected_state:
|
|
463
|
+
return self.make_response(item, "State mismatch.")
|
|
464
|
+
self._exchange_code_for_token(code)
|
|
465
|
+
# cache identity
|
|
466
|
+
try:
|
|
467
|
+
me = self._get("/me", params={"fields": "id,name"}, user_context=True)
|
|
468
|
+
if me.get("id"):
|
|
469
|
+
self.plugin.set_option_value("user_id", me["id"])
|
|
470
|
+
if me.get("name"):
|
|
471
|
+
self.plugin.set_option_value("user_name", me["name"])
|
|
472
|
+
except Exception:
|
|
473
|
+
pass
|
|
474
|
+
return self.make_response(item, {
|
|
475
|
+
"access_token": self.plugin.get_option_value("oauth2_access_token"),
|
|
476
|
+
"expires_at": self.plugin.get_option_value("oauth2_expires_at"),
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
def cmd_fb_token_extend(self, item: dict) -> dict:
|
|
480
|
+
# Exchange short-lived user token for long-lived user token
|
|
481
|
+
client_id = self.plugin.get_option_value("oauth2_client_id") or ""
|
|
482
|
+
client_secret = self.plugin.get_option_value("oauth2_client_secret") or ""
|
|
483
|
+
if not (client_id and client_secret):
|
|
484
|
+
return self.make_response(item, "Set oauth2_client_id and oauth2_client_secret first.")
|
|
485
|
+
access = (self.plugin.get_option_value("oauth2_access_token") or "").strip()
|
|
486
|
+
if not access:
|
|
487
|
+
return self.make_response(item, "No user access token to extend.")
|
|
488
|
+
url = f"{self._api_base()}/oauth/access_token"
|
|
489
|
+
params = {
|
|
490
|
+
"grant_type": "fb_exchange_token",
|
|
491
|
+
"client_id": client_id,
|
|
492
|
+
"client_secret": client_secret,
|
|
493
|
+
"fb_exchange_token": access,
|
|
494
|
+
}
|
|
495
|
+
r = requests.get(url, params=params, timeout=self._timeout())
|
|
496
|
+
res = self._handle_response(r)
|
|
497
|
+
self._save_tokens(res)
|
|
498
|
+
return self.make_response(item, {"access_token": res.get("access_token"), "expires_in": res.get("expires_in")})
|
|
499
|
+
|
|
500
|
+
# ---------------------- Me ----------------------
|
|
501
|
+
|
|
502
|
+
def cmd_fb_me(self, item: dict) -> dict:
|
|
503
|
+
p = item.get("params", {}) or {}
|
|
504
|
+
fields = p.get("fields") or "id,name"
|
|
505
|
+
res = self._get("/me", params={"fields": fields}, user_context=True)
|
|
506
|
+
if res.get("id"):
|
|
507
|
+
self.plugin.set_option_value("user_id", res["id"])
|
|
508
|
+
if res.get("name"):
|
|
509
|
+
self.plugin.set_option_value("user_name", res["name"])
|
|
510
|
+
return self.make_response(item, res)
|
|
511
|
+
|
|
512
|
+
# ---------------------- Pages ----------------------
|
|
513
|
+
|
|
514
|
+
def cmd_fb_pages_list(self, item: dict) -> dict:
|
|
515
|
+
p = item.get("params", {}) or {}
|
|
516
|
+
fields = p.get("fields") or "id,name,access_token,tasks"
|
|
517
|
+
limit = int(p.get("limit") or 25)
|
|
518
|
+
params = {"fields": fields, "limit": limit}
|
|
519
|
+
if p.get("after"):
|
|
520
|
+
params["after"] = p["after"]
|
|
521
|
+
if p.get("before"):
|
|
522
|
+
params["before"] = p["before"]
|
|
523
|
+
res = self._get("/me/accounts", params=params, user_context=True)
|
|
524
|
+
return self.make_response(item, res)
|
|
525
|
+
|
|
526
|
+
def cmd_fb_page_set_default(self, item: dict) -> dict:
|
|
527
|
+
p = item.get("params", {}) or {}
|
|
528
|
+
page_id = p.get("page_id")
|
|
529
|
+
if not page_id:
|
|
530
|
+
return self.make_response(item, "Param 'page_id' required")
|
|
531
|
+
# fetch name + access_token
|
|
532
|
+
fetch_token = bool(p.get("fetch_token", True))
|
|
533
|
+
name = None
|
|
534
|
+
tok = None
|
|
535
|
+
fields = "id,name" + (",access_token" if fetch_token else "")
|
|
536
|
+
info = self._get(f"/{page_id}", params={"fields": fields}, user_context=True)
|
|
537
|
+
name = info.get("name")
|
|
538
|
+
tok = info.get("access_token")
|
|
539
|
+
self.plugin.set_option_value("fb_page_id", page_id)
|
|
540
|
+
if name:
|
|
541
|
+
self.plugin.set_option_value("fb_page_name", name)
|
|
542
|
+
if tok:
|
|
543
|
+
self.plugin.set_option_value("fb_page_access_token", tok)
|
|
544
|
+
return self.make_response(item, {"page_id": page_id, "page_name": name, "has_token": bool(tok)})
|
|
545
|
+
|
|
546
|
+
def _ensure_page_id(self) -> str:
|
|
547
|
+
pid = self.plugin.get_option_value("fb_page_id") or ""
|
|
548
|
+
if not pid:
|
|
549
|
+
raise RuntimeError("No default page_id set. Run fb_page_set_default or pass page_id in params.")
|
|
550
|
+
return pid
|
|
551
|
+
|
|
552
|
+
def _ensure_page_token(self, page_id: str) -> str:
|
|
553
|
+
# prefer cached token for default page
|
|
554
|
+
if page_id == (self.plugin.get_option_value("fb_page_id") or ""):
|
|
555
|
+
tok = (self.plugin.get_option_value("fb_page_access_token") or "").strip()
|
|
556
|
+
if tok:
|
|
557
|
+
return tok
|
|
558
|
+
# try to fetch fresh access_token via fields=access_token
|
|
559
|
+
info = self._get(f"/{page_id}", params={"fields": "access_token"}, user_context=True)
|
|
560
|
+
tok = info.get("access_token")
|
|
561
|
+
if not tok:
|
|
562
|
+
raise RuntimeError("Cannot resolve Page access token. Ensure permissions and roles are correct.")
|
|
563
|
+
if page_id == (self.plugin.get_option_value("fb_page_id") or ""):
|
|
564
|
+
self.plugin.set_option_value("fb_page_access_token", tok)
|
|
565
|
+
return tok
|
|
566
|
+
|
|
567
|
+
# ---------------------- Posts ----------------------
|
|
568
|
+
|
|
569
|
+
def cmd_fb_page_posts(self, item: dict) -> dict:
|
|
570
|
+
p = item.get("params", {}) or {}
|
|
571
|
+
page_id = p.get("page_id") or self._ensure_page_id()
|
|
572
|
+
token = self._ensure_page_token(page_id)
|
|
573
|
+
fields = p.get("fields") or "id,message,created_time,permalink_url,is_published"
|
|
574
|
+
limit = int(p.get("limit") or 25)
|
|
575
|
+
params: Dict[str, Any] = {"fields": fields, "limit": limit}
|
|
576
|
+
for k in ("since", "until", "after", "before"):
|
|
577
|
+
if p.get(k):
|
|
578
|
+
params[k] = p[k]
|
|
579
|
+
res = self._get(f"/{page_id}/feed", params=params, token=token, user_context=False)
|
|
580
|
+
return self.make_response(item, res)
|
|
581
|
+
|
|
582
|
+
def _upload_photo(self, page_id: str, token: str, path: Optional[str] = None, url: Optional[str] = None,
|
|
583
|
+
caption: Optional[str] = None, published: bool = True, temporary: bool = False) -> dict:
|
|
584
|
+
form: Dict[str, Any] = {"published": "true" if published else "false"}
|
|
585
|
+
if caption:
|
|
586
|
+
form["caption"] = caption
|
|
587
|
+
if temporary:
|
|
588
|
+
form["temporary"] = "true"
|
|
589
|
+
files = None
|
|
590
|
+
if path:
|
|
591
|
+
local = self.prepare_path(path)
|
|
592
|
+
if not os.path.exists(local):
|
|
593
|
+
raise RuntimeError(f"Local file not found: {local}")
|
|
594
|
+
mt, _ = mimetypes.guess_type(local)
|
|
595
|
+
mt = mt or "application/octet-stream"
|
|
596
|
+
files = {"source": (os.path.basename(local), open(local, "rb"), mt)}
|
|
597
|
+
elif url:
|
|
598
|
+
form["url"] = url
|
|
599
|
+
else:
|
|
600
|
+
raise RuntimeError("Provide 'path' or 'url' to upload photo.")
|
|
601
|
+
res = self._post_form(f"/{page_id}/photos", form=form, files=files, token=token, user_context=False)
|
|
602
|
+
return res
|
|
603
|
+
|
|
604
|
+
def cmd_fb_page_photo_upload(self, item: dict) -> dict:
|
|
605
|
+
p = item.get("params", {}) or {}
|
|
606
|
+
page_id = p.get("page_id") or self._ensure_page_id()
|
|
607
|
+
token = self._ensure_page_token(page_id)
|
|
608
|
+
res = self._upload_photo(
|
|
609
|
+
page_id=page_id,
|
|
610
|
+
token=token,
|
|
611
|
+
path=p.get("path"),
|
|
612
|
+
url=p.get("url"),
|
|
613
|
+
caption=p.get("caption"),
|
|
614
|
+
published=bool(p.get("published", True)),
|
|
615
|
+
temporary=bool(p.get("temporary", False)),
|
|
616
|
+
)
|
|
617
|
+
return self.make_response(item, res)
|
|
618
|
+
|
|
619
|
+
def _upload_photos_unpublished(self, page_id: str, token: str, photos: List[dict]) -> List[dict]:
|
|
620
|
+
# photos: [{"path": "..."}] or [{"url": "..."}]
|
|
621
|
+
media = []
|
|
622
|
+
for ph in photos:
|
|
623
|
+
res = self._upload_photo(
|
|
624
|
+
page_id=page_id,
|
|
625
|
+
token=token,
|
|
626
|
+
path=ph.get("path"),
|
|
627
|
+
url=ph.get("url"),
|
|
628
|
+
caption=ph.get("caption"),
|
|
629
|
+
published=False,
|
|
630
|
+
temporary=bool(ph.get("temporary", False)),
|
|
631
|
+
)
|
|
632
|
+
pid = res.get("id")
|
|
633
|
+
if pid:
|
|
634
|
+
media.append({"media_fbid": pid})
|
|
635
|
+
return media
|
|
636
|
+
|
|
637
|
+
def cmd_fb_page_post_create(self, item: dict) -> dict:
|
|
638
|
+
p = item.get("params", {}) or {}
|
|
639
|
+
page_id = p.get("page_id") or self._ensure_page_id()
|
|
640
|
+
token = self._ensure_page_token(page_id)
|
|
641
|
+
|
|
642
|
+
payload: Dict[str, Any] = {}
|
|
643
|
+
if p.get("message") is not None:
|
|
644
|
+
payload["message"] = p.get("message") or ""
|
|
645
|
+
if p.get("link"):
|
|
646
|
+
payload["link"] = p.get("link")
|
|
647
|
+
published = bool(p.get("published", True))
|
|
648
|
+
payload["published"] = published
|
|
649
|
+
if not published and p.get("scheduled_publish_time"):
|
|
650
|
+
payload["scheduled_publish_time"] = p.get("scheduled_publish_time")
|
|
651
|
+
payload["unpublished_content_type"] = "SCHEDULED"
|
|
652
|
+
if p.get("targeting"):
|
|
653
|
+
payload["targeting"] = p.get("targeting")
|
|
654
|
+
|
|
655
|
+
# attached_media via already uploaded photo ids or auto-upload URLs/paths
|
|
656
|
+
attached_media = []
|
|
657
|
+
media_fbids = p.get("media_fbids") or []
|
|
658
|
+
if media_fbids:
|
|
659
|
+
for mid in media_fbids:
|
|
660
|
+
attached_media.append({"media_fbid": str(mid)})
|
|
661
|
+
photo_urls = p.get("photo_urls") or []
|
|
662
|
+
photo_paths = p.get("photo_paths") or []
|
|
663
|
+
if photo_urls or photo_paths:
|
|
664
|
+
lst = ([{"url": u} for u in photo_urls] + [{"path": ph} for ph in photo_paths])
|
|
665
|
+
attached_media.extend(self._upload_photos_unpublished(page_id, token, lst))
|
|
666
|
+
if attached_media:
|
|
667
|
+
payload["attached_media"] = attached_media
|
|
668
|
+
|
|
669
|
+
res = self._post_json(f"/{page_id}/feed", payload=payload, token=token, user_context=False)
|
|
670
|
+
return self.make_response(item, res)
|
|
671
|
+
|
|
672
|
+
def cmd_fb_page_post_delete(self, item: dict) -> dict:
|
|
673
|
+
p = item.get("params", {}) or {}
|
|
674
|
+
post_id = p.get("post_id")
|
|
675
|
+
if not post_id:
|
|
676
|
+
return self.make_response(item, "Param 'post_id' required (e.g. {pageid}_{postid})")
|
|
677
|
+
# Page token required to delete
|
|
678
|
+
page_id = (self.plugin.get_option_value("fb_page_id") or "")
|
|
679
|
+
token = None
|
|
680
|
+
if page_id:
|
|
681
|
+
try:
|
|
682
|
+
token = self._ensure_page_token(page_id)
|
|
683
|
+
except Exception:
|
|
684
|
+
token = None
|
|
685
|
+
res = self._delete(f"/{post_id}", token=token, user_context=token is None)
|
|
686
|
+
return self.make_response(item, res)
|
|
687
|
+
|
|
688
|
+
# ---------------------- FS helpers ----------------------
|
|
689
|
+
|
|
690
|
+
def prepare_path(self, path: str) -> str:
|
|
691
|
+
if path in [".", "./"]:
|
|
692
|
+
return self.plugin.window.core.config.get_user_dir("data")
|
|
693
|
+
if self.is_absolute_path(path):
|
|
694
|
+
return path
|
|
695
|
+
return os.path.join(self.plugin.window.core.config.get_user_dir("data"), path)
|
|
696
|
+
|
|
697
|
+
def is_absolute_path(self, path: str) -> bool:
|
|
698
|
+
return os.path.isabs(path)
|