feat: platforms with a chrome extension
This commit is contained in:
parent
f72d63bd43
commit
e3c3854840
73 changed files with 1358 additions and 878 deletions
|
|
@ -76,6 +76,9 @@ MASTODON_URL="https://mastodon.social"
|
|||
MASTODON_CLIENT_ID=""
|
||||
MASTODON_CLIENT_SECRET=""
|
||||
|
||||
# Chrome Extension Settings (for cookie-based platform integrations like Skool)
|
||||
EXTENSION_ID=""
|
||||
|
||||
# Misc Settings
|
||||
OPENAI_API_KEY=""
|
||||
NEXT_PUBLIC_DISCORD_SUPPORT=""
|
||||
|
|
|
|||
|
|
@ -232,6 +232,10 @@ export class NoAuthIntegrationsController {
|
|||
? AuthService.fixedEncryption(
|
||||
Buffer.from(body.code, 'base64').toString()
|
||||
)
|
||||
: integrationProvider.isChromeExtension
|
||||
? AuthService.signJWT(
|
||||
JSON.parse(Buffer.from(body.code, 'base64').toString())
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
|
||||
|
|
@ -284,11 +288,21 @@ export class NoAuthIntegrationsController {
|
|||
await ioRedis.del(`redirect:${body.state}`);
|
||||
}
|
||||
|
||||
const extensionToken = integrationProvider.isChromeExtension
|
||||
? AuthService.signJWT({
|
||||
integrationId: createUpdate.id,
|
||||
organizationId: org.id,
|
||||
internalId: String(id),
|
||||
provider: integration,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...createUpdate,
|
||||
onboarding: onboarding === 'true',
|
||||
pages,
|
||||
...(returnURL ? { returnURL } : {}),
|
||||
...(extensionToken ? { extensionToken } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -307,4 +321,75 @@ export class NoAuthIntegrationsController {
|
|||
|
||||
return this._integrationService.saveProviderPage(org.id, id, body);
|
||||
}
|
||||
|
||||
@Post('/extension-refresh')
|
||||
async extensionRefreshCookies(
|
||||
@Body() body: { jwt: string; cookies: string }
|
||||
) {
|
||||
let payload: any;
|
||||
try {
|
||||
payload = AuthService.verifyJWT(body.jwt);
|
||||
} catch {
|
||||
throw new HttpException('Invalid token', 401);
|
||||
}
|
||||
|
||||
const { integrationId, organizationId, internalId, provider } = payload;
|
||||
if (!integrationId || !organizationId || !internalId || !provider) {
|
||||
throw new HttpException('Invalid token payload', 400);
|
||||
}
|
||||
|
||||
const integration = await this._integrationService.getIntegrationById(
|
||||
organizationId,
|
||||
integrationId
|
||||
);
|
||||
if (!integration || integration.internalId !== internalId) {
|
||||
throw new HttpException('Integration not found', 404);
|
||||
}
|
||||
|
||||
const integrationProvider =
|
||||
this._integrationManager.getSocialIntegration(provider);
|
||||
if (!integrationProvider?.isChromeExtension) {
|
||||
throw new HttpException('Not a Chrome extension integration', 400);
|
||||
}
|
||||
|
||||
const authResult = await integrationProvider.authenticate({
|
||||
code: body.cookies,
|
||||
codeVerifier: '',
|
||||
});
|
||||
|
||||
if (typeof authResult === 'string') {
|
||||
throw new HttpException(authResult, 400);
|
||||
}
|
||||
|
||||
if (String(authResult.id) !== String(integration.internalId)) {
|
||||
await this._integrationService.refreshNeeded(
|
||||
organizationId,
|
||||
integrationId
|
||||
);
|
||||
return { success: false, reason: 'account_mismatch' };
|
||||
}
|
||||
|
||||
await this._integrationService.createOrUpdateIntegration(
|
||||
undefined,
|
||||
false,
|
||||
organizationId,
|
||||
integration.name,
|
||||
undefined,
|
||||
'social',
|
||||
integration.internalId,
|
||||
integration.providerIdentifier,
|
||||
authResult.accessToken,
|
||||
'',
|
||||
authResult.expiresIn,
|
||||
undefined,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
AuthService.signJWT(
|
||||
JSON.parse(Buffer.from(body.cookies, 'base64').toString())
|
||||
)
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
apps/extension/manifest.dev.json
Executable file → Normal file
37
apps/extension/manifest.dev.json
Executable file → Normal file
|
|
@ -1,15 +1,30 @@
|
|||
{
|
||||
"action": {
|
||||
"default_icon": "public/dev-icon-32.png",
|
||||
"default_popup": "src/pages/popup/index.html"
|
||||
},
|
||||
"manifest_version": 3,
|
||||
"name": "Postiz",
|
||||
"version": "2.0.0",
|
||||
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqtH6qclAsfFf6qbUKfPmhBbfycGrt13+0h6ti/olniCGnjQjhkVVTnURfLFz+v+842Ee+pAS5HBEXo57dQ9xUtwFGXnavVR+myjN+Un9NIfFyYmYEBvLrinclsMJBwWMM8JkhxKuaOagxp1hqGgNAO4C0bzE3YN/SPoTjNpGU8TGm/ENZ/TDUneZyyVM5HEEmOTZEmjmy9FJaxbzGmZ2rixNO45pkjXMFp8+/XrFSNiCqNZt6LQNIqL5SfVIRUKGBjE3OG/gtahVToBdlXi5yzP1uYE0Qs4grJ/T1rUUzTXFAQa7heWA9mskf0xAMEtTSED4N9bZ4sF8cf5J+SGGlwIDAQAB",
|
||||
"description": "Postiz browser extension for social media scheduling",
|
||||
"icons": {
|
||||
"128": "public/dev-icon-128.png"
|
||||
"32": "icon-32.png",
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["contentStyle.css", "dev-icon-128.png", "dev-icon-32.png"],
|
||||
"matches": []
|
||||
}
|
||||
]
|
||||
"permissions": [
|
||||
"cookies",
|
||||
"alarms",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"*://*.skool.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"externally_connectable": {
|
||||
"matches": [
|
||||
"http://localhost/*",
|
||||
"https://localhost/*",
|
||||
"https://*.postiz.com/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,29 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Postiz",
|
||||
"description": "Your ultimate social media scheduling tool",
|
||||
"options_ui": {
|
||||
"page": "src/pages/options/index.html"
|
||||
},
|
||||
"action": {
|
||||
"default_popup": "src/pages/popup/index.html",
|
||||
"default_icon": {
|
||||
"32": "icon-32.png"
|
||||
}
|
||||
},
|
||||
"version": "2.0.0",
|
||||
"description": "Postiz browser extension for social media scheduling",
|
||||
"icons": {
|
||||
"32": "icon-32.png",
|
||||
"128": "icon-128.png"
|
||||
},
|
||||
"permissions": ["activeTab", "cookies", "tabs"],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["http://*/*", "https://*/*", "<all_urls>"],
|
||||
"js": ["src/pages/content/index.tsx"],
|
||||
"css": ["contentStyle.css"]
|
||||
}
|
||||
"permissions": [
|
||||
"cookies",
|
||||
"alarms",
|
||||
"storage"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["contentStyle.css", "icon-128.png", "icon-32.png"],
|
||||
"matches": []
|
||||
}
|
||||
]
|
||||
"host_permissions": [
|
||||
"*://*.skool.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"externally_connectable": {
|
||||
"matches": [
|
||||
"http://localhost/*",
|
||||
"https://localhost/*",
|
||||
"https://*.postiz.com/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"__DEV__": "true"
|
||||
},
|
||||
"watch": [
|
||||
"src",
|
||||
"utils",
|
||||
"vite.config.base.ts",
|
||||
"vite.config.chrome.ts",
|
||||
"manifest.json",
|
||||
"manifest.dev.json"
|
||||
],
|
||||
"ext": "tsx,css,html,ts,json",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "vite build --config vite.config.chrome.ts --mode development"
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"__DEV__": "true"
|
||||
},
|
||||
"watch": [
|
||||
"src",
|
||||
"utils",
|
||||
"vite.config.base.ts",
|
||||
"vite.config.firefox.ts",
|
||||
"manifest.json",
|
||||
"manifest.dev.json"
|
||||
],
|
||||
"ext": "tsx,css,html,ts,json",
|
||||
"ignore": ["src/**/*.spec.ts"],
|
||||
"exec": "vite build --config vite.config.firefox.ts --mode development"
|
||||
}
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
{
|
||||
"name": "postiz-extension",
|
||||
"version": "1.0.3",
|
||||
"description": "A simple chrome & firefox extension template with Vite, React, TypeScript and Tailwind CSS.",
|
||||
"version": "2.0.0",
|
||||
"description": "Postiz browser extension for cookie-based platform authentication",
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && vite build --config vite.config.chrome.ts && zip -r extension.zip dist",
|
||||
"build:chrome": "vite build --config vite.config.chrome.ts",
|
||||
"build:firefox": "vite build --config vite.config.firefox.ts",
|
||||
"dev": "rm -rf dist && dotenv -e ../../.env -- vite build --config vite.config.chrome.ts --mode development --watch",
|
||||
"dev:chrome": "nodemon --config nodemon.chrome.json",
|
||||
"dev:firefox": "nodemon --config nodemon.firefox.json"
|
||||
"build": "rm -rf dist && vite build && cp manifest.json dist/manifest.json && cd dist && zip -r ../extension.zip .",
|
||||
"dev": "rm -rf dist && HOT_RELOAD_EXTENSION_VITE_PORT=8081 NODE_ENV=development dotenv -e ../../.env -- vite build --config vite.config.chrome.ts --mode development --watch"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1 KiB |
|
|
@ -1,7 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
|
||||
<g fill="#61DAFB">
|
||||
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
|
||||
<circle cx="420.9" cy="296.5" r="45.7"/>
|
||||
<path d="M520.5 78.1z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
|
|
@ -1,13 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@theme {
|
||||
--animate-spin-slow: spin 20s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
209
apps/extension/src/background.ts
Normal file
209
apps/extension/src/background.ts
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
import { ExtensionRequest, GetCookiesResponse, ProviderInfo, StoredRefreshEntry } from './types/messages';
|
||||
import { getAllProviders, getProvider } from './providers/provider.registry';
|
||||
import { CookieProvider } from './providers/cookie-provider.interface';
|
||||
|
||||
const EXTENSION_VERSION = '2.0.0';
|
||||
const REFRESH_ALARM_NAME = 'cookie-refresh';
|
||||
const STORAGE_KEY = 'refreshEntries';
|
||||
|
||||
const ALLOWED_ORIGIN_PATTERNS = [
|
||||
/^https?:\/\/localhost(:\d+)?$/,
|
||||
/^https?:\/\/([a-z0-9-]+\.)*postiz\.com$/,
|
||||
];
|
||||
|
||||
function isOriginAllowed(origin: string | undefined): boolean {
|
||||
if (!origin) return false;
|
||||
return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));
|
||||
}
|
||||
|
||||
async function extractCookies(provider: CookieProvider): Promise<GetCookiesResponse> {
|
||||
const allCookies = await chrome.cookies.getAll({ url: provider.url });
|
||||
|
||||
const extracted: Record<string, string> = {};
|
||||
const missingRequired: string[] = [];
|
||||
|
||||
for (const def of provider.cookies) {
|
||||
const found = allCookies.find((c) => c.name === def.name);
|
||||
if (found) {
|
||||
extracted[def.name] = found.value;
|
||||
} else if (def.required) {
|
||||
missingRequired.push(def.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingRequired.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
provider: provider.identifier,
|
||||
error: `Missing required cookies: ${missingRequired.join(', ')}. User may need to log in to ${provider.name}.`,
|
||||
missingCookies: missingRequired,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: provider.identifier,
|
||||
cookies: extracted,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Refresh Token Storage Helpers ---
|
||||
|
||||
async function getStoredEntries(): Promise<Record<string, StoredRefreshEntry>> {
|
||||
const result = await chrome.storage.local.get(STORAGE_KEY);
|
||||
return result[STORAGE_KEY] || {};
|
||||
}
|
||||
|
||||
async function setStoredEntries(entries: Record<string, StoredRefreshEntry>): Promise<void> {
|
||||
await chrome.storage.local.set({ [STORAGE_KEY]: entries });
|
||||
}
|
||||
|
||||
async function ensureAlarm(): Promise<void> {
|
||||
const existing = await chrome.alarms.get(REFRESH_ALARM_NAME);
|
||||
if (!existing) {
|
||||
chrome.alarms.create(REFRESH_ALARM_NAME, { periodInMinutes: 1440 });
|
||||
}
|
||||
}
|
||||
|
||||
async function clearAlarmIfEmpty(): Promise<void> {
|
||||
const entries = await getStoredEntries();
|
||||
if (Object.keys(entries).length === 0) {
|
||||
await chrome.alarms.clear(REFRESH_ALARM_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Background Cookie Refresh ---
|
||||
|
||||
async function refreshAllCookies(): Promise<void> {
|
||||
const entries = await getStoredEntries();
|
||||
for (const [integrationId, entry] of Object.entries(entries)) {
|
||||
try {
|
||||
const provider = getProvider(entry.provider);
|
||||
if (!provider) continue;
|
||||
|
||||
const cookieResult = await extractCookies(provider);
|
||||
if (!cookieResult.success) continue;
|
||||
|
||||
const base64Cookies = btoa(JSON.stringify(cookieResult.cookies));
|
||||
|
||||
await fetch(`${entry.backendUrl}/integrations/extension-refresh`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ jwt: entry.jwt, cookies: base64Cookies }),
|
||||
});
|
||||
} catch {
|
||||
// Silently skip — will retry next cycle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Alarm Listener ---
|
||||
|
||||
chrome.alarms.onAlarm.addListener((alarm) => {
|
||||
if (alarm.name === REFRESH_ALARM_NAME) {
|
||||
refreshAllCookies();
|
||||
}
|
||||
});
|
||||
|
||||
// --- Ensure alarm on startup ---
|
||||
|
||||
(async () => {
|
||||
const entries = await getStoredEntries();
|
||||
if (Object.keys(entries).length > 0) {
|
||||
await ensureAlarm();
|
||||
}
|
||||
})();
|
||||
|
||||
// --- Message Listener ---
|
||||
|
||||
chrome.runtime.onMessageExternal.addListener(
|
||||
(
|
||||
message: ExtensionRequest,
|
||||
sender: chrome.runtime.MessageSender,
|
||||
sendResponse: (response: unknown) => void
|
||||
) => {
|
||||
const origin = sender.origin ?? sender.url;
|
||||
if (!isOriginAllowed(origin)) {
|
||||
sendResponse({ error: 'Unauthorized origin' });
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (message.type) {
|
||||
case 'PING': {
|
||||
sendResponse({ status: 'ok', version: EXTENSION_VERSION });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET_PROVIDERS': {
|
||||
const providers = getAllProviders();
|
||||
const providerInfos: ProviderInfo[] = providers.map((p) => ({
|
||||
identifier: p.identifier,
|
||||
name: p.name,
|
||||
url: p.url,
|
||||
cookieNames: p.cookies.map((c) => c.name),
|
||||
}));
|
||||
sendResponse({ providers: providerInfos });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'GET_COOKIES': {
|
||||
const provider = getProvider(message.provider);
|
||||
if (!provider) {
|
||||
sendResponse({
|
||||
success: false,
|
||||
provider: message.provider,
|
||||
error: `Unknown provider: ${message.provider}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
extractCookies(provider)
|
||||
.then((result) => sendResponse(result))
|
||||
.catch((err) =>
|
||||
sendResponse({
|
||||
success: false,
|
||||
provider: message.provider,
|
||||
error: `Failed to extract cookies: ${err.message}`,
|
||||
})
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'STORE_REFRESH_TOKEN': {
|
||||
(async () => {
|
||||
const entries = await getStoredEntries();
|
||||
entries[message.integrationId] = {
|
||||
jwt: message.jwt,
|
||||
backendUrl: message.backendUrl,
|
||||
provider: message.provider,
|
||||
};
|
||||
await setStoredEntries(entries);
|
||||
await ensureAlarm();
|
||||
sendResponse({ success: true });
|
||||
})().catch(() => sendResponse({ success: false }));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'REMOVE_REFRESH_TOKEN': {
|
||||
(async () => {
|
||||
const entries = await getStoredEntries();
|
||||
delete entries[message.integrationId];
|
||||
await setStoredEntries(entries);
|
||||
await clearAlarmIfEmpty();
|
||||
sendResponse({ success: true });
|
||||
})().catch(() => sendResponse({ success: false }));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
default: {
|
||||
sendResponse({ error: `Unknown message type: ${(message as any).type}` });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
11
apps/extension/src/global.d.ts
vendored
11
apps/extension/src/global.d.ts
vendored
|
|
@ -1,11 +0,0 @@
|
|||
declare module '*.svg' {
|
||||
import React = require('react');
|
||||
export const ReactComponent: React.SFC<React.SVGProps<SVGSVGElement>>;
|
||||
const src: string;
|
||||
export default src;
|
||||
}
|
||||
|
||||
declare module '*.json' {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extName": {
|
||||
"message": "name in src/locales/en/messages.json",
|
||||
"description": "Extension name"
|
||||
},
|
||||
"extDescription": {
|
||||
"message": "description in src/locales/en/messages.json",
|
||||
"description": "Extension description"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { fetchRequestUtil } from '@gitroom/extension/utils/request.util';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
|
||||
if (request.action === 'makeHttpRequest') {
|
||||
fetchRequestUtil(request).then((response) => {
|
||||
sendResponse(response);
|
||||
});
|
||||
}
|
||||
|
||||
if (request.action === 'loadStorage') {
|
||||
chrome.storage.local.get([request.key], function (storage) {
|
||||
sendResponse(storage[request.key]);
|
||||
});
|
||||
}
|
||||
|
||||
if (request.action === 'saveStorage') {
|
||||
chrome.storage.local.set({ [request.key]: request.value }, function () {
|
||||
sendResponse({ success: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (request.action === 'loadCookie') {
|
||||
chrome.cookies.get(
|
||||
{
|
||||
url: import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL,
|
||||
name: request.cookieName,
|
||||
},
|
||||
function (cookies) {
|
||||
sendResponse(cookies?.value);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
|
@ -1,115 +0,0 @@
|
|||
import { FC, memo, useCallback, useEffect, useState } from 'react';
|
||||
import { ProviderInterface } from '@gitroom/extension/providers/provider.interface';
|
||||
import { fetchCookie } from '@gitroom/extension/utils/load.cookie';
|
||||
|
||||
const Comp: FC<{ removeModal: () => void; platform: string; style: string }> = (
|
||||
props
|
||||
) => {
|
||||
const load = async () => {
|
||||
const cookie = await fetchCookie(`auth`);
|
||||
if (document.querySelector('iframe#modal-postiz')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
|
||||
div.style.position = 'fixed';
|
||||
div.style.top = '0';
|
||||
div.style.left = '0';
|
||||
div.style.zIndex = '9999';
|
||||
div.style.width = '100%';
|
||||
div.style.height = '100%';
|
||||
div.style.border = 'none';
|
||||
div.style.overflow = 'hidden';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.backgroundColor = 'transparent';
|
||||
// @ts-ignore
|
||||
iframe.allowTransparency = 'true';
|
||||
iframe.src =
|
||||
(import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL) +
|
||||
`/modal/${props.style}/${props.platform}?loggedAuth=${cookie}`;
|
||||
iframe.id = 'modal-postiz';
|
||||
iframe.style.width = '100%';
|
||||
iframe.style.height = '100%';
|
||||
iframe.style.position = 'fixed';
|
||||
iframe.style.top = '0';
|
||||
iframe.style.left = '0';
|
||||
iframe.style.zIndex = '9999';
|
||||
iframe.style.border = 'none';
|
||||
div.appendChild(iframe);
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.action === 'closeIframe') {
|
||||
const iframe = document.querySelector('iframe#modal-postiz');
|
||||
if (iframe) {
|
||||
props.removeModal();
|
||||
div.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, []);
|
||||
return <></>;
|
||||
};
|
||||
export const ActionComponent: FC<{
|
||||
target: Node;
|
||||
keyIndex: number;
|
||||
actionType: string;
|
||||
provider: ProviderInterface;
|
||||
wrap: boolean;
|
||||
selector: string;
|
||||
}> = memo((props) => {
|
||||
const { wrap, provider, selector, target, actionType } = props;
|
||||
const [modal, showModal] = useState(false);
|
||||
const handle = useCallback(async (e: any) => {
|
||||
showModal(true);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const blockingDiv = document.createElement('div');
|
||||
if (document.querySelector(`.${selector}`)) {
|
||||
console.log('already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
// @ts-ignore
|
||||
const targetInformation = target.getBoundingClientRect();
|
||||
blockingDiv.style.position = 'absolute';
|
||||
blockingDiv.id = 'blockingDiv';
|
||||
blockingDiv.style.cursor = 'pointer';
|
||||
blockingDiv.style.top = `${targetInformation.top}px`;
|
||||
blockingDiv.style.left = `${targetInformation.left}px`;
|
||||
blockingDiv.style.width = `${targetInformation.width}px`;
|
||||
blockingDiv.style.height = `${targetInformation.height}px`;
|
||||
blockingDiv.style.zIndex = '9999';
|
||||
blockingDiv.className = selector;
|
||||
|
||||
document.body.appendChild(blockingDiv);
|
||||
blockingDiv.addEventListener('click', handle);
|
||||
}, 1000);
|
||||
return () => {
|
||||
blockingDiv.removeEventListener('click', handle);
|
||||
blockingDiv.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="g-wrapper" style={{ position: 'relative' }}>
|
||||
<div className="absolute start-0 top-0 z-[9999] w-full h-full" />
|
||||
{modal && (
|
||||
<Comp
|
||||
platform={provider.identifier}
|
||||
style={provider.style}
|
||||
removeModal={() => showModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import './style.css';
|
||||
import { MainContent } from '@gitroom/extension/pages/content/main.content';
|
||||
const div = document.createElement('div');
|
||||
div.id = '__root';
|
||||
document.body.appendChild(div);
|
||||
|
||||
const rootContainer = document.querySelector('#__root');
|
||||
if (!rootContainer) throw new Error("Can't find Content root element");
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<MainContent />);
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
import {
|
||||
FC,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ProviderList } from '@gitroom/extension/providers/provider.list';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ActionComponent } from '@gitroom/extension/pages/content/elements/action.component';
|
||||
|
||||
// Define a type to track elements with their action types
|
||||
interface ActionElement {
|
||||
element: HTMLElement;
|
||||
actionType: string;
|
||||
}
|
||||
|
||||
export const MainContent: FC = () => {
|
||||
return <MainContentInner />;
|
||||
};
|
||||
|
||||
export const MainContentInner: FC = (props) => {
|
||||
const [actionElements, setActionElements] = useState<ActionElement[]>([]);
|
||||
const actionSetRef = useRef(new Map<HTMLElement, string>());
|
||||
const provider = useMemo(() => {
|
||||
return ProviderList.find((p) => {
|
||||
return p.baseUrl.indexOf(new URL(window.location.href).hostname) > -1;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!provider) return;
|
||||
|
||||
// Helper to scan DOM for existing matching elements
|
||||
const scanDOMForExistingMatches = () => {
|
||||
const action = { selector: provider.element, type: 'post' };
|
||||
const matches = document.querySelectorAll(action.selector);
|
||||
matches.forEach((match) => {
|
||||
const htmlMatch = match as HTMLElement;
|
||||
if (!actionSetRef.current.has(htmlMatch)) {
|
||||
actionSetRef.current.set(htmlMatch, action.type);
|
||||
}
|
||||
});
|
||||
|
||||
// Update state
|
||||
const elements: ActionElement[] = [];
|
||||
actionSetRef.current.forEach((actionType, element) => {
|
||||
elements.push({ element, actionType });
|
||||
});
|
||||
setActionElements(elements);
|
||||
};
|
||||
|
||||
// Initial scan before observing
|
||||
scanDOMForExistingMatches();
|
||||
|
||||
const observer = new MutationObserver((mutationsList) => {
|
||||
let addedSomething = false;
|
||||
let removedSomething = false;
|
||||
|
||||
for (const mutation of mutationsList) {
|
||||
if (mutation.type === 'childList') {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement;
|
||||
|
||||
const action = { selector: provider.element, type: 'post' };
|
||||
if (
|
||||
el.matches?.(action.selector) &&
|
||||
!actionSetRef.current.has(el)
|
||||
) {
|
||||
actionSetRef.current.set(el, action.type);
|
||||
addedSomething = true;
|
||||
}
|
||||
|
||||
if (el.querySelectorAll) {
|
||||
const matches = el.querySelectorAll(action.selector);
|
||||
matches.forEach((match) => {
|
||||
const htmlMatch = match as HTMLElement;
|
||||
if (!actionSetRef.current.has(htmlMatch)) {
|
||||
actionSetRef.current.set(htmlMatch, action.type);
|
||||
addedSomething = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const node of mutation.removedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
const el = node as HTMLElement;
|
||||
|
||||
if (actionSetRef.current.has(el)) {
|
||||
actionSetRef.current.delete(el);
|
||||
removedSomething = true;
|
||||
}
|
||||
|
||||
const action = { selector: provider.element, type: 'post' };
|
||||
if (el.querySelectorAll) {
|
||||
const matches = el.querySelectorAll(action.selector);
|
||||
matches.forEach((match) => {
|
||||
const htmlMatch = match as HTMLElement;
|
||||
if (actionSetRef.current.has(htmlMatch)) {
|
||||
actionSetRef.current.delete(htmlMatch);
|
||||
removedSomething = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mutation.type === 'attributes') {
|
||||
const el = mutation.target;
|
||||
if (el instanceof HTMLElement) {
|
||||
const action = { selector: provider.element, type: 'post' };
|
||||
const matchesNow = el.matches(action.selector);
|
||||
const wasTracked = actionSetRef.current.has(el);
|
||||
|
||||
if (matchesNow && !wasTracked) {
|
||||
actionSetRef.current.set(el, action.type);
|
||||
addedSomething = true;
|
||||
} else if (!matchesNow && wasTracked) {
|
||||
actionSetRef.current.delete(el);
|
||||
removedSomething = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (addedSomething || removedSomething) {
|
||||
const elements: ActionElement[] = [];
|
||||
actionSetRef.current.forEach((actionType, element) => {
|
||||
elements.push({ element, actionType });
|
||||
});
|
||||
setActionElements(elements);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeOldValue: true,
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return actionElements.map((actionEl, index) => (
|
||||
<Fragment key={index}>
|
||||
{createPortal(
|
||||
<ActionComponent
|
||||
target={actionEl.element}
|
||||
keyIndex={index}
|
||||
actionType={actionEl.actionType}
|
||||
provider={provider}
|
||||
wrap={true}
|
||||
selector={stringToABC(
|
||||
provider.element
|
||||
.split(',')
|
||||
.map((z) => z.trim())
|
||||
.find((p) => actionEl.element.matches(p)) || ''
|
||||
)}
|
||||
/>,
|
||||
actionEl.element
|
||||
)}
|
||||
</Fragment>
|
||||
));
|
||||
};
|
||||
|
||||
function stringToABC(text: string, length = 8) {
|
||||
// Simple DJB2-like hash (non-cryptographic!)
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
hash = (hash * 33) ^ text.charCodeAt(i);
|
||||
}
|
||||
|
||||
hash = Math.abs(hash);
|
||||
|
||||
// Convert to base-26 string using a–z
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
|
||||
let result = '';
|
||||
while (result.length < length) {
|
||||
result = alphabet[hash % 26] + result;
|
||||
hash = Math.floor(hash / 26);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.my-wrapper {
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
position: fixed !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
z-index: 999999 !important;
|
||||
display: flex !important;
|
||||
justify-content: center !important;
|
||||
background: rgba(0, 0, 0, 0.5) !important;
|
||||
}
|
||||
|
||||
.my-wrapper > div {
|
||||
background: white !important;
|
||||
width: 600px !important;
|
||||
height: 300px !important;
|
||||
border-radius: 10px !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
justify-items: center !important;
|
||||
margin-top: 100px !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 50vh;
|
||||
font-size: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
import React from 'react';
|
||||
import '@gitroom/extension/pages/options/Options.css';
|
||||
|
||||
export default function Options() {
|
||||
return <div className="container">Options</div>;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Options</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="__root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import '@gitroom/extension/pages/options/index.css';
|
||||
import Options from '@gitroom/extension/pages/options/Options';
|
||||
|
||||
function init() {
|
||||
const rootContainer = document.querySelector('#__root');
|
||||
if (!rootContainer) throw new Error("Can't find Options root element");
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<Options />);
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
body {
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
.container {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import React from 'react';
|
||||
import '@pages/panel/Panel.css';
|
||||
|
||||
export default function Panel() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Side Panel</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Devtools Panel</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="__root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import Panel from '@pages/panel/Panel';
|
||||
import '@pages/panel/index.css';
|
||||
import '@assets/styles/tailwind.css';
|
||||
|
||||
function init() {
|
||||
const rootContainer = document.querySelector('#__root');
|
||||
if (!rootContainer) throw new Error("Can't find Panel root element");
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<Panel />);
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ProviderList } from '@gitroom/extension/providers/provider.list';
|
||||
import { fetchCookie } from '@gitroom/extension/utils/load.cookie';
|
||||
|
||||
export const PopupContainerContainer: FC = () => {
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
|
||||
setUrl(tabs[0]?.url);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (!url) {
|
||||
return (
|
||||
<div className="text-4xl">This website is not supported by Postiz</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <PopupContainer url={url} />;
|
||||
};
|
||||
|
||||
export const PopupContainer: FC<{ url: string }> = (props) => {
|
||||
const { url } = props;
|
||||
const [isLoggedIn, setIsLoggedIn] = useState<false | string>(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const provider = useMemo(() => {
|
||||
return ProviderList.find((p) => {
|
||||
return p.baseUrl.indexOf(new URL(url).hostname) > -1;
|
||||
});
|
||||
}, [url]);
|
||||
|
||||
const loadCookie = useCallback(async () => {
|
||||
try {
|
||||
if (!provider) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const auth = await fetchCookie(`auth`);
|
||||
|
||||
if (auth) {
|
||||
setIsLoggedIn(auth);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadCookie();
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
return (
|
||||
<div className="text-4xl">This website is not supported by Postiz</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
return <div className="text-4xl">You are not logged in to Postiz</div>;
|
||||
}
|
||||
|
||||
return <div />;
|
||||
};
|
||||
|
||||
export default function Popup() {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
<PopupContainerContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
width: 300px;
|
||||
height: 260px;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Popup</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="__root"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import './index.css';
|
||||
import '@gitroom/extension/assets/styles/tailwind.css';
|
||||
import Popup from '@gitroom/extension/pages/popup/Popup';
|
||||
|
||||
function init() {
|
||||
const rootContainer = document.querySelector('#__root');
|
||||
if (!rootContainer) throw new Error("Can't find Popup root element");
|
||||
const root = createRoot(rootContainer);
|
||||
root.render(<Popup />);
|
||||
}
|
||||
|
||||
init();
|
||||
19
apps/extension/src/providers/cookie-provider.interface.ts
Normal file
19
apps/extension/src/providers/cookie-provider.interface.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
export interface CookieDefinition {
|
||||
/** The cookie name to extract, e.g., 'client_id' */
|
||||
name: string;
|
||||
/** Whether this cookie must exist for the extraction to be considered successful */
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
export interface CookieProvider {
|
||||
/** Unique identifier used in messages, e.g., 'skool' */
|
||||
identifier: string;
|
||||
/** Human-readable name, e.g., 'Skool' */
|
||||
name: string;
|
||||
/** URL to query cookies for, e.g., 'https://www.skool.com' — passed to chrome.cookies.getAll({ url }) */
|
||||
url: string;
|
||||
/** URL pattern for host_permissions in manifest, e.g., '*://*.skool.com/*' */
|
||||
hostPermission: string;
|
||||
/** List of cookies to extract from this site */
|
||||
cookies: CookieDefinition[];
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { ProviderInterface } from '@gitroom/extension/providers/provider.interface';
|
||||
|
||||
export class LinkedinProvider implements ProviderInterface {
|
||||
identifier = 'linkedin';
|
||||
baseUrl = 'https://www.linkedin.com';
|
||||
element = `.share-box-feed-entry__closed-share-box`;
|
||||
attachTo = `[role="main"]`;
|
||||
style = 'light' as 'light';
|
||||
findIdentifier = (element: HTMLElement) => {
|
||||
return element.closest('[data-urn]').getAttribute('data-urn');
|
||||
};
|
||||
}
|
||||
12
apps/extension/src/providers/list/skool.provider.ts
Normal file
12
apps/extension/src/providers/list/skool.provider.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { CookieProvider } from '../cookie-provider.interface';
|
||||
|
||||
export const skoolProvider: CookieProvider = {
|
||||
identifier: 'skool',
|
||||
name: 'Skool',
|
||||
url: 'https://www.skool.com',
|
||||
hostPermission: '*://*.skool.com/*',
|
||||
cookies: [
|
||||
{ name: 'client_id', required: true },
|
||||
{ name: 'auth_token', required: true },
|
||||
],
|
||||
};
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { ProviderInterface } from '@gitroom/extension/providers/provider.interface';
|
||||
|
||||
export class XProvider implements ProviderInterface {
|
||||
identifier = 'x';
|
||||
baseUrl = 'https://x.com';
|
||||
element = `[data-testid="primaryColumn"]:has([data-testid="toolBar"]) [data-testid="tweetTextarea_0_label"], [data-testid="SideNav_NewTweet_Button"]`;
|
||||
attachTo = `#react-root`;
|
||||
style = 'dark' as 'dark';
|
||||
findIdentifier = (element: HTMLElement) => {
|
||||
return (
|
||||
Array.from(
|
||||
(
|
||||
element?.closest('article') ||
|
||||
element?.closest(`[aria-labelledby="modal-header"]`)
|
||||
)?.querySelectorAll('a') || []
|
||||
)
|
||||
?.find((p) => {
|
||||
return p?.getAttribute('href')?.includes('/status/');
|
||||
})
|
||||
?.getAttribute('href')
|
||||
?.split('/status/')?.[1] || window.location.href.split('/status/')?.[1]
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
export interface ProviderInterface {
|
||||
identifier: string;
|
||||
baseUrl: string;
|
||||
element: string;
|
||||
findIdentifier: (element: HTMLElement) => string;
|
||||
attachTo: string;
|
||||
style: 'dark' | 'light';
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
import { XProvider } from './list/x.provider';
|
||||
import { ProviderInterface } from './provider.interface';
|
||||
import { LinkedinProvider } from './list/linkedin.provider';
|
||||
|
||||
export const ProviderList = [
|
||||
new XProvider(),
|
||||
new LinkedinProvider(),
|
||||
] satisfies ProviderInterface[] as ProviderInterface[];
|
||||
18
apps/extension/src/providers/provider.registry.ts
Normal file
18
apps/extension/src/providers/provider.registry.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { CookieProvider } from './cookie-provider.interface';
|
||||
import { skoolProvider } from './list/skool.provider';
|
||||
|
||||
export const providers: CookieProvider[] = [
|
||||
skoolProvider,
|
||||
];
|
||||
|
||||
const providerMap = new Map<string, CookieProvider>(
|
||||
providers.map((p) => [p.identifier, p])
|
||||
);
|
||||
|
||||
export function getAllProviders(): CookieProvider[] {
|
||||
return providers;
|
||||
}
|
||||
|
||||
export function getProvider(identifier: string): CookieProvider | undefined {
|
||||
return providerMap.get(identifier);
|
||||
}
|
||||
85
apps/extension/src/types/messages.ts
Normal file
85
apps/extension/src/types/messages.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
// ---- Request Types ----
|
||||
|
||||
export interface PingRequest {
|
||||
type: 'PING';
|
||||
}
|
||||
|
||||
export interface GetProvidersRequest {
|
||||
type: 'GET_PROVIDERS';
|
||||
}
|
||||
|
||||
export interface GetCookiesRequest {
|
||||
type: 'GET_COOKIES';
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface StoreRefreshTokenRequest {
|
||||
type: 'STORE_REFRESH_TOKEN';
|
||||
provider: string;
|
||||
integrationId: string;
|
||||
jwt: string;
|
||||
backendUrl: string;
|
||||
}
|
||||
|
||||
export interface RemoveRefreshTokenRequest {
|
||||
type: 'REMOVE_REFRESH_TOKEN';
|
||||
integrationId: string;
|
||||
}
|
||||
|
||||
export type ExtensionRequest =
|
||||
| PingRequest
|
||||
| GetProvidersRequest
|
||||
| GetCookiesRequest
|
||||
| StoreRefreshTokenRequest
|
||||
| RemoveRefreshTokenRequest;
|
||||
|
||||
// ---- Response Types ----
|
||||
|
||||
export interface PingResponse {
|
||||
status: 'ok';
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ProviderInfo {
|
||||
identifier: string;
|
||||
name: string;
|
||||
url: string;
|
||||
cookieNames: string[];
|
||||
}
|
||||
|
||||
export interface GetProvidersResponse {
|
||||
providers: ProviderInfo[];
|
||||
}
|
||||
|
||||
export interface GetCookiesSuccessResponse {
|
||||
success: true;
|
||||
provider: string;
|
||||
cookies: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface GetCookiesErrorResponse {
|
||||
success: false;
|
||||
provider: string;
|
||||
error: string;
|
||||
missingCookies?: string[];
|
||||
}
|
||||
|
||||
export type GetCookiesResponse =
|
||||
| GetCookiesSuccessResponse
|
||||
| GetCookiesErrorResponse;
|
||||
|
||||
export interface StoredRefreshEntry {
|
||||
jwt: string;
|
||||
backendUrl: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
}
|
||||
|
||||
export type ExtensionResponse =
|
||||
| PingResponse
|
||||
| GetProvidersResponse
|
||||
| GetCookiesResponse
|
||||
| ErrorResponse;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
export const fetchCookie = (cookieName: string) => {
|
||||
return chrome.runtime.sendMessage({
|
||||
action: 'loadCookie',
|
||||
cookieName,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCookie = async (
|
||||
cookies: chrome.cookies.Cookie[],
|
||||
cookie: string
|
||||
) => {
|
||||
// return cookies.find((c) => c.name === cookie).value;
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export const fetchStorage = (key: string) => {
|
||||
return chrome.runtime.sendMessage({
|
||||
action: 'loadStorage',
|
||||
key,
|
||||
});
|
||||
};
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
const isDev = process.env.NODE_ENV === 'development';
|
||||
export const sendRequest = (
|
||||
auth: string,
|
||||
url: string,
|
||||
method: 'GET' | 'POST',
|
||||
body?: string
|
||||
) => {
|
||||
return chrome.runtime.sendMessage({
|
||||
action: 'makeHttpRequest',
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
auth,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchRequestUtil = async (request: any) => {
|
||||
return (
|
||||
await fetch(
|
||||
(import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL) +
|
||||
request.url,
|
||||
{
|
||||
method: request.method || 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: request.auth,
|
||||
// Add any auth headers here if needed
|
||||
},
|
||||
...(request.body ? { body: request.body } : {}),
|
||||
}
|
||||
)
|
||||
).json();
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
export const saveStorage = (key: string, value: any) => {
|
||||
return chrome.runtime.sendMessage({
|
||||
action: 'saveStorage',
|
||||
key,
|
||||
value,
|
||||
});
|
||||
};
|
||||
1
apps/extension/src/vite-env.d.ts
vendored
1
apps/extension/src/vite-env.d.ts
vendored
|
|
@ -1 +0,0 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
|
@ -21,7 +21,6 @@
|
|||
"src",
|
||||
"utils",
|
||||
"vite.config.base.ts",
|
||||
"vite.config.chrome.ts",
|
||||
"vite.config.firefox.ts"
|
||||
"vite.config.chrome.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,28 @@
|
|||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from 'path';
|
||||
import { ManifestV3Export } from '@crxjs/vite-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import { defineConfig, BuildOptions } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { stripDevIcons, crxI18n } from './custom-vite-plugins';
|
||||
import manifest from './manifest.json';
|
||||
import devManifest from './manifest.dev.json';
|
||||
import pkg from './package.json';
|
||||
import { ProviderList } from './src/providers/provider.list';
|
||||
import { providers } from './src/providers/provider.registry';
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development';
|
||||
// set this flag to true, if you want localization support
|
||||
const localize = false;
|
||||
|
||||
const merge = isDev ? devManifest : ({} as ManifestV3Export);
|
||||
const { matches, ...rest } = manifest?.content_scripts?.[0] || {};
|
||||
|
||||
export const baseManifest = {
|
||||
...manifest,
|
||||
host_permissions: [
|
||||
...ProviderList.map((p) => p.baseUrl + '/'),
|
||||
import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL + '/*',
|
||||
(import.meta.env?.NEXT_PUBLIC_BACKEND_URL || process?.env?.NEXT_PUBLIC_BACKEND_URL || '') + '/*',
|
||||
...providers.map(p => p.hostPermission)
|
||||
],
|
||||
permissions: [...(manifest.permissions || [])],
|
||||
content_scripts: [
|
||||
{
|
||||
matches: ProviderList.reduce(
|
||||
(all, p) => [...all, p.baseUrl + '/*'],
|
||||
[import.meta.env?.FRONTEND_URL || process?.env?.FRONTEND_URL + '/*']
|
||||
),
|
||||
...rest,
|
||||
},
|
||||
],
|
||||
version: pkg.version,
|
||||
...merge,
|
||||
...(localize
|
||||
|
|
@ -50,9 +40,8 @@ export const baseBuildOptions: BuildOptions = {
|
|||
};
|
||||
|
||||
export default defineConfig({
|
||||
envPrefix: ['NEXT_PUBLIC_', 'FRONTEND_URL'],
|
||||
envPrefix: ['NEXT_PUBLIC_', 'FRONTEND_URL', 'NEXT_PUBLIC_BACKEND_URL'],
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
tsconfigPaths(),
|
||||
react(),
|
||||
stripDevIcons(isDev),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export default mergeConfig(
|
|||
manifest: {
|
||||
...baseManifest,
|
||||
background: {
|
||||
service_worker: 'src/pages/background/index.ts',
|
||||
service_worker: 'src/background.ts',
|
||||
type: 'module',
|
||||
},
|
||||
} as ManifestV3Export,
|
||||
|
|
@ -28,7 +28,7 @@ export default mergeConfig(
|
|||
? [
|
||||
hotReloadExtension({
|
||||
log: true,
|
||||
backgroundPath: 'src/pages/background/index.ts',
|
||||
backgroundPath: 'src/background.ts',
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
|
|
|
|||
|
|
@ -1,31 +0,0 @@
|
|||
import { resolve } from 'path';
|
||||
import { mergeConfig, defineConfig } from 'vite';
|
||||
import { crx, ManifestV3Export } from '@crxjs/vite-plugin';
|
||||
import baseConfig, { baseManifest, baseBuildOptions } from './vite.config.base';
|
||||
|
||||
const outDir = resolve(__dirname, 'dist_firefox');
|
||||
|
||||
export default mergeConfig(
|
||||
baseConfig,
|
||||
defineConfig({
|
||||
plugins: [
|
||||
crx({
|
||||
manifest: {
|
||||
...baseManifest,
|
||||
background: {
|
||||
scripts: ['src/pages/background/index.ts'],
|
||||
},
|
||||
} as ManifestV3Export,
|
||||
browser: 'firefox',
|
||||
contentScripts: {
|
||||
injectCss: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
...baseBuildOptions,
|
||||
outDir,
|
||||
},
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
})
|
||||
);
|
||||
23
apps/extension/vite.config.ts
Normal file
23
apps/extension/vite.config.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: resolve(__dirname, 'dist'),
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/background.ts'),
|
||||
formats: ['es'],
|
||||
fileName: () => 'background.js',
|
||||
},
|
||||
rollupOptions: {
|
||||
output: {
|
||||
entryFileNames: 'background.js',
|
||||
},
|
||||
},
|
||||
target: 'esnext',
|
||||
minify: false,
|
||||
sourcemap: process.env.NODE_ENV === 'development',
|
||||
},
|
||||
publicDir: resolve(__dirname, 'public'),
|
||||
});
|
||||
BIN
apps/frontend/public/icons/platforms/skool.png
Normal file
BIN
apps/frontend/public/icons/platforms/skool.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -78,6 +78,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
disableImageCompression={!!process.env.DISABLE_IMAGE_COMPRESSION}
|
||||
disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS}
|
||||
sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!}
|
||||
extensionId={process.env.EXTENSION_ID || ''}
|
||||
language={allHeaders.get(headerName)}
|
||||
transloadit={
|
||||
process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
disableImageCompression={!!process.env.DISABLE_IMAGE_COMPRESSION}
|
||||
disableXAnalytics={!!process.env.DISABLE_X_ANALYTICS}
|
||||
sentryDsn={process.env.NEXT_PUBLIC_SENTRY_DSN!}
|
||||
extensionId={process.env.EXTENSION_ID || ''}
|
||||
transloadit={
|
||||
process.env.TRANSLOADIT_AUTH && process.env.TRANSLOADIT_TEMPLATE
|
||||
? [
|
||||
|
|
|
|||
15
apps/frontend/src/chrome.d.ts
vendored
Normal file
15
apps/frontend/src/chrome.d.ts
vendored
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Minimal Chrome extension API types for externally_connectable messaging.
|
||||
* Web pages listed in an extension's externally_connectable can use
|
||||
* chrome.runtime.sendMessage to communicate with the extension.
|
||||
*/
|
||||
declare namespace chrome {
|
||||
namespace runtime {
|
||||
const lastError: { message?: string } | undefined;
|
||||
function sendMessage(
|
||||
extensionId: string,
|
||||
message: any,
|
||||
callback: (response: any) => void
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
|
@ -251,6 +251,109 @@ export const CustomVariables: FC<{
|
|||
</div>
|
||||
);
|
||||
};
|
||||
const ExtensionNotFound: FC = () => {
|
||||
const modals = useModals();
|
||||
const t = useT();
|
||||
return (
|
||||
<div className="flex flex-col gap-[16px] pt-[8px]">
|
||||
<p className="text-[14px] text-textColor/80">
|
||||
{t(
|
||||
'extension_not_available',
|
||||
'The Postiz browser extension is not installed. You need to install it before connecting this channel.'
|
||||
)}
|
||||
</p>
|
||||
<div className="flex gap-[10px]">
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
window.open(
|
||||
'https://chromewebstore.google.com/detail/postiz/cidhffagahknaeodkplfbcpfeielnkjl?hl=en',
|
||||
'_blank'
|
||||
);
|
||||
modals.closeCurrent();
|
||||
}}
|
||||
>
|
||||
{t('install_extension', 'Install Extension')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1 !bg-transparent border border-tableBorder text-textColor"
|
||||
onClick={() => modals.closeCurrent()}
|
||||
>
|
||||
{t('cancel', 'Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ChromeExtensionWarning: FC<{
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}> = ({ onConfirm, onCancel }) => {
|
||||
const modals = useModals();
|
||||
const t = useT();
|
||||
return (
|
||||
<div className="flex flex-col gap-[16px] pt-[8px]">
|
||||
<p className="text-[14px] text-textColor/80">
|
||||
{t(
|
||||
'chrome_extension_warning_intro',
|
||||
'This channel connects via the browser extension. Please be aware of the following:'
|
||||
)}
|
||||
</p>
|
||||
<ul className="flex flex-col gap-[8px] list-disc ps-[20px] text-[14px] text-textColor/80">
|
||||
<li>
|
||||
{t(
|
||||
'chrome_extension_warning_tos',
|
||||
'Using a browser extension to interact with a platform may violate its terms of service and could result in your account being suspended or banned.'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'chrome_extension_warning_unstable',
|
||||
'This method is not as reliable as native integrations and may experience random disconnections.'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
{t(
|
||||
'chrome_extension_warning_reconnect',
|
||||
'You may need to reconnect periodically if the session expires.'
|
||||
)}
|
||||
</li>
|
||||
<li>
|
||||
We will store your cookies securely to facilitate the connection.
|
||||
</li>
|
||||
<li>
|
||||
Postiz does not take responsibility for any issues arising or account termination due to the use of this method.
|
||||
</li>
|
||||
</ul>
|
||||
<div className="flex gap-[10px] mt-[8px]">
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1"
|
||||
onClick={() => {
|
||||
modals.closeCurrent();
|
||||
onConfirm();
|
||||
}}
|
||||
>
|
||||
{t('i_understand_continue', 'I understand, continue')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1 !bg-transparent border border-tableBorder text-textColor"
|
||||
onClick={() => {
|
||||
modals.closeCurrent();
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
{t('cancel', 'Cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddProviderComponent: FC<{
|
||||
social: Array<{
|
||||
identifier: string;
|
||||
|
|
@ -258,6 +361,11 @@ export const AddProviderComponent: FC<{
|
|||
toolTip?: string;
|
||||
isExternal: boolean;
|
||||
isWeb3: boolean;
|
||||
isChromeExtension?: boolean;
|
||||
extensionCookies?: Array<{
|
||||
name: string;
|
||||
domain: string;
|
||||
}>;
|
||||
customFields?: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
|
|
@ -274,7 +382,7 @@ export const AddProviderComponent: FC<{
|
|||
onboarding?: boolean;
|
||||
}> = (props) => {
|
||||
const { update, social, article, onboarding } = props;
|
||||
const { isGeneral } = useVariables();
|
||||
const { isGeneral, extensionId } = useVariables();
|
||||
const toaster = useToaster();
|
||||
const router = useRouter();
|
||||
const fetch = useFetch();
|
||||
|
|
@ -285,6 +393,7 @@ export const AddProviderComponent: FC<{
|
|||
identifier: string,
|
||||
isExternal: boolean,
|
||||
isWeb3: boolean,
|
||||
isChromeExtension?: boolean,
|
||||
customFields?: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
|
|
@ -364,6 +473,106 @@ export const AddProviderComponent: FC<{
|
|||
openWeb3();
|
||||
return;
|
||||
}
|
||||
if (isChromeExtension) {
|
||||
const confirmed = await new Promise<boolean>((resolve) => {
|
||||
modal.openModal({
|
||||
title: t('chrome_extension_notice', 'Browser Extension Notice'),
|
||||
withCloseButton: true,
|
||||
onClose: () => resolve(false),
|
||||
children: (
|
||||
<ChromeExtensionWarning
|
||||
onConfirm={() => {
|
||||
resolve(true);
|
||||
}}
|
||||
onCancel={() => {
|
||||
resolve(false);
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
});
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
if (!extensionId || !chrome?.runtime?.sendMessage) {
|
||||
modal.openModal({
|
||||
title: t('extension_not_available_title', 'Extension Not Found'),
|
||||
withCloseButton: true,
|
||||
children: <ExtensionNotFound />,
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
chrome.runtime.sendMessage(
|
||||
extensionId,
|
||||
{ type: 'PING' },
|
||||
(response: any) => {
|
||||
if (chrome.runtime.lastError || !response?.status) {
|
||||
reject(new Error('Extension not reachable'));
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
toaster.show(
|
||||
t(
|
||||
'extension_not_installed',
|
||||
'Postiz browser extension is not installed or not reachable.'
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const cookieResponse = await new Promise<any>((resolve, reject) => {
|
||||
chrome.runtime.sendMessage(
|
||||
extensionId,
|
||||
{ type: 'GET_COOKIES', provider: identifier },
|
||||
(response: any) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
if (!cookieResponse.success) {
|
||||
toaster.show(
|
||||
cookieResponse.error ||
|
||||
t(
|
||||
'extension_cookies_missing',
|
||||
'Could not get cookies. Please log in to the platform first.'
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { url } = await (
|
||||
await fetch(
|
||||
`/integrations/social/${identifier}${
|
||||
onboarding ? '?onboarding=true' : ''
|
||||
}`
|
||||
)
|
||||
).json();
|
||||
modal.closeAll();
|
||||
window.location.href = `/integrations/social/${identifier}?state=${url}&code=${Buffer.from(
|
||||
JSON.stringify(cookieResponse.cookies)
|
||||
).toString('base64')}${onboarding ? '&onboarding=true' : ''}`;
|
||||
} catch {
|
||||
toaster.show(
|
||||
t(
|
||||
'extension_communication_error',
|
||||
'Failed to communicate with the browser extension.'
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isExternal) {
|
||||
modal.openModal({
|
||||
title: 'URL',
|
||||
|
|
@ -415,7 +624,12 @@ export const AddProviderComponent: FC<{
|
|||
return true;
|
||||
}
|
||||
|
||||
return !item.isExternal && !item.isWeb3 && !item.customFields;
|
||||
return (
|
||||
!item.isExternal &&
|
||||
!item.isWeb3 &&
|
||||
!item.isChromeExtension &&
|
||||
!item.customFields
|
||||
);
|
||||
})
|
||||
.map((item) => (
|
||||
<div
|
||||
|
|
@ -425,6 +639,7 @@ export const AddProviderComponent: FC<{
|
|||
item.identifier,
|
||||
item.isExternal,
|
||||
item.isWeb3,
|
||||
item.isChromeExtension,
|
||||
item.customFields
|
||||
)}
|
||||
{...(!!item.toolTip
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export interface Integrations {
|
|||
id: string;
|
||||
disabled?: boolean;
|
||||
inBetweenSteps: boolean;
|
||||
editor: 'normal' | 'markdown' | 'html';
|
||||
editor: 'none' | 'normal' | 'markdown' | 'html';
|
||||
display: string;
|
||||
identifier: string;
|
||||
type: string;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import dayjs from 'dayjs';
|
|||
import { continueProviderList } from '@gitroom/frontend/components/new-launch/providers/continue-provider/list';
|
||||
import { IntegrationContext } from '@gitroom/frontend/components/launches/helpers/use.integration';
|
||||
import { newDayjs } from '@gitroom/frontend/components/layout/set.timezone';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
||||
interface TwoStepState {
|
||||
integrationId: string;
|
||||
|
|
@ -31,6 +32,7 @@ export const ContinueIntegration: FC<{
|
|||
const { push } = useRouter();
|
||||
const t = useT();
|
||||
const fetch = useFetch();
|
||||
const { extensionId, backendUrl } = useVariables();
|
||||
const [error, setError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [twoStepState, setTwoStepState] = useState<TwoStepState | null>(null);
|
||||
|
|
@ -135,9 +137,34 @@ export const ContinueIntegration: FC<{
|
|||
onboarding: resOnboarding,
|
||||
pages,
|
||||
returnURL,
|
||||
extensionToken,
|
||||
} = await data.json();
|
||||
const onboarding = resOnboarding || searchParams.onboarding === 'true';
|
||||
|
||||
// Store refresh token in extension for background cookie refresh
|
||||
if (
|
||||
extensionToken &&
|
||||
extensionId &&
|
||||
typeof chrome !== 'undefined' &&
|
||||
chrome?.runtime?.sendMessage
|
||||
) {
|
||||
try {
|
||||
chrome.runtime.sendMessage(
|
||||
extensionId,
|
||||
{
|
||||
type: 'STORE_REFRESH_TOKEN',
|
||||
provider,
|
||||
integrationId: id,
|
||||
jwt: extensionToken,
|
||||
backendUrl,
|
||||
},
|
||||
() => {}
|
||||
);
|
||||
} catch {
|
||||
// Silently ignore — extension may not be available
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a two-step provider, show the selection UI inline
|
||||
if (inBetweenSteps && !searchParams.refresh) {
|
||||
setTwoStepState({
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { Integration } from '@prisma/client';
|
|||
import { SettingsModal } from '@gitroom/frontend/components/launches/settings.modal';
|
||||
import { CustomVariables } from '@gitroom/frontend/components/launches/add.provider.component';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
import { AddEditModal } from '@gitroom/frontend/components/new-launch/add.edit.modal';
|
||||
import dayjs from 'dayjs';
|
||||
|
|
@ -59,6 +60,7 @@ export const Menu: FC<{
|
|||
|
||||
const fetch = useFetch();
|
||||
const router = useRouter();
|
||||
const { extensionId } = useVariables();
|
||||
const { integrations, reloadCalendarView } = useCalendar();
|
||||
const toast = useToaster();
|
||||
const modal = useModals();
|
||||
|
|
@ -146,10 +148,26 @@ export const Menu: FC<{
|
|||
);
|
||||
return;
|
||||
}
|
||||
// Clean up extension refresh token if applicable
|
||||
if (
|
||||
extensionId &&
|
||||
typeof chrome !== 'undefined' &&
|
||||
chrome?.runtime?.sendMessage
|
||||
) {
|
||||
try {
|
||||
chrome.runtime.sendMessage(
|
||||
extensionId,
|
||||
{ type: 'REMOVE_REFRESH_TOKEN', integrationId: id },
|
||||
() => {}
|
||||
);
|
||||
} catch {
|
||||
// Silently ignore
|
||||
}
|
||||
}
|
||||
toast.show(t('channel_deleted', 'Channel Deleted'), 'success');
|
||||
setShow(false);
|
||||
onChange(true);
|
||||
}, [t]);
|
||||
}, [t, extensionId, id]);
|
||||
|
||||
const enableChannel = useCallback(async () => {
|
||||
await fetch('/integrations/enable', {
|
||||
|
|
|
|||
|
|
@ -526,7 +526,7 @@ export const EditorWrapper: FC<{
|
|||
};
|
||||
|
||||
export const Editor: FC<{
|
||||
editorType?: 'normal' | 'markdown' | 'html';
|
||||
editorType?: 'none' | 'normal' | 'markdown' | 'html';
|
||||
totalPosts: number;
|
||||
value: string;
|
||||
num?: number;
|
||||
|
|
@ -777,14 +777,18 @@ export const Editor: FC<{
|
|||
toolBar={
|
||||
<div className="flex gap-[5px]">
|
||||
<SignatureBox editor={editorRef?.current?.editor} />
|
||||
<UText
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
<BoldText
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
{editorType !== 'none' && (
|
||||
<>
|
||||
<UText
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
<BoldText
|
||||
editor={editorRef?.current?.editor}
|
||||
currentValue={props.value!}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(editorType === 'markdown' || editorType === 'html') &&
|
||||
identifier !== 'telegram' && (
|
||||
<>
|
||||
|
|
@ -854,7 +858,7 @@ export const Editor: FC<{
|
|||
export const OnlyEditor = forwardRef<
|
||||
any,
|
||||
{
|
||||
editorType: 'normal' | 'markdown' | 'html';
|
||||
editorType: 'none' | 'normal' | 'markdown' | 'html';
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
paste?: (event: ClipboardEvent | File[]) => void;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import WordpressProvider from '@gitroom/frontend/components/new-launch/providers
|
|||
import ListmonkProvider from '@gitroom/frontend/components/new-launch/providers/listmonk/listmonk.provider';
|
||||
import GmbProvider from '@gitroom/frontend/components/new-launch/providers/gmb/gmb.provider';
|
||||
import MoltbookProvider from '@gitroom/frontend/components/new-launch/providers/moltbook/moltbook.provider';
|
||||
import SkoolProvider from '@gitroom/frontend/components/new-launch/providers/skool/skool.provider';
|
||||
|
||||
export const Providers = [
|
||||
{
|
||||
|
|
@ -157,6 +158,10 @@ export const Providers = [
|
|||
{
|
||||
identifier: 'moltbook',
|
||||
component: MoltbookProvider,
|
||||
},
|
||||
{
|
||||
identifier: 'skool',
|
||||
component: SkoolProvider,
|
||||
}
|
||||
];
|
||||
export const ShowAllProviders = forwardRef((props, ref) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const SkoolGroupSelect: FC<{
|
||||
name: string;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name } = props;
|
||||
const t = useT();
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [groups, setGroups] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentGroup, setCurrentGroup] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentGroup(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
customFunc.get('groups').then((data) => setGroups(data));
|
||||
const settings = getValues()[props.name];
|
||||
if (settings) {
|
||||
setCurrentGroup(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!groups.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select Group"
|
||||
onChange={onChangeInner}
|
||||
value={currentGroup}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{groups.map((group: any) => (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
'use client';
|
||||
|
||||
import { FC, useEffect, useState } from 'react';
|
||||
import { useCustomProviderFunction } from '@gitroom/frontend/components/launches/helpers/use.custom.provider.function';
|
||||
import { Select } from '@gitroom/react/form/select';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { useT } from '@gitroom/react/translation/get.transation.service.client';
|
||||
export const SkoolLabelSelect: FC<{
|
||||
name: string;
|
||||
groupId: string | undefined;
|
||||
onChange: (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => void;
|
||||
}> = (props) => {
|
||||
const { onChange, name, groupId } = props;
|
||||
const t = useT();
|
||||
const customFunc = useCustomProviderFunction();
|
||||
const [labels, setLabels] = useState([]);
|
||||
const { getValues } = useSettings();
|
||||
const [currentLabel, setCurrentLabel] = useState<string | undefined>();
|
||||
const onChangeInner = (event: {
|
||||
target: {
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
}) => {
|
||||
setCurrentLabel(event.target.value);
|
||||
onChange(event);
|
||||
};
|
||||
useEffect(() => {
|
||||
if (!groupId) {
|
||||
setLabels([]);
|
||||
setCurrentLabel(undefined);
|
||||
return;
|
||||
}
|
||||
customFunc.get('label', { id: groupId }).then((data) => setLabels(data));
|
||||
}, [groupId]);
|
||||
useEffect(() => {
|
||||
const settings = getValues()[name];
|
||||
if (settings) {
|
||||
setCurrentLabel(settings);
|
||||
}
|
||||
}, []);
|
||||
if (!groupId || !labels.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Select
|
||||
name={name}
|
||||
label="Select Label"
|
||||
onChange={onChangeInner}
|
||||
value={currentLabel}
|
||||
>
|
||||
<option value="">{t('select_1', '--Select--')}</option>
|
||||
{labels.map((label: any) => (
|
||||
<option key={label.id} value={label.id}>
|
||||
{label.name}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
'use client';
|
||||
|
||||
import {
|
||||
withProvider,
|
||||
} from '@gitroom/frontend/components/new-launch/providers/high.order.provider';
|
||||
import { FC, useState } from 'react';
|
||||
import { SkoolDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/skool.dto';
|
||||
import { SkoolGroupSelect } from '@gitroom/frontend/components/new-launch/providers/skool/skool.group.select';
|
||||
import { SkoolLabelSelect } from '@gitroom/frontend/components/new-launch/providers/skool/skool.label.select';
|
||||
import { useSettings } from '@gitroom/frontend/components/launches/helpers/use.values';
|
||||
import { Input } from '@gitroom/react/form/input';
|
||||
const SkoolComponent: FC = () => {
|
||||
const form = useSettings();
|
||||
const [selectedGroup, setSelectedGroup] = useState<string | undefined>(
|
||||
form.getValues().group
|
||||
);
|
||||
const groupRegister = form.register('group');
|
||||
const onGroupChange = (event: { target: { value: string; name: string } }) => {
|
||||
setSelectedGroup(event.target.value);
|
||||
groupRegister.onChange(event);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<Input label="Title" {...form.register('title')} />
|
||||
<SkoolGroupSelect {...groupRegister} onChange={onGroupChange} />
|
||||
<SkoolLabelSelect {...form.register('label')} groupId={selectedGroup} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default withProvider({
|
||||
minimumCharacters: [],
|
||||
SettingsComponent: SkoolComponent,
|
||||
CustomPreviewComponent: undefined,
|
||||
dto: SkoolDto,
|
||||
checkValidity: undefined,
|
||||
maximumCharacters: 5000,
|
||||
});
|
||||
|
|
@ -26,7 +26,7 @@ export interface SelectedIntegrations {
|
|||
}
|
||||
|
||||
interface StoreState {
|
||||
editor: undefined | 'normal' | 'markdown' | 'html';
|
||||
editor: undefined | 'none' | 'normal' | 'markdown' | 'html';
|
||||
loaded: boolean;
|
||||
date: dayjs.Dayjs;
|
||||
postComment: PostComment;
|
||||
|
|
@ -128,7 +128,7 @@ interface StoreState {
|
|||
setPostComment: (postComment: PostComment) => void;
|
||||
setActivateExitButton?: (activateExitButton: boolean) => void;
|
||||
setDummy: (dummy: boolean) => void;
|
||||
setEditor: (editor: 'normal' | 'markdown' | 'html') => void;
|
||||
setEditor: (editor: 'none' | 'normal' | 'markdown' | 'html') => void;
|
||||
setLoaded?: (loaded: boolean) => void;
|
||||
setChars: (id: string, chars: number) => void;
|
||||
chars: Record<string, number>;
|
||||
|
|
@ -613,7 +613,7 @@ export const useLaunchStore = create<StoreState>()((set) => ({
|
|||
set((state) => ({
|
||||
dummy,
|
||||
})),
|
||||
setEditor: (editor: 'normal' | 'markdown' | 'html') =>
|
||||
setEditor: (editor: 'none' | 'normal' | 'markdown' | 'html') =>
|
||||
set((state) => ({
|
||||
editor,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -145,6 +145,16 @@ export const stripHtmlValidation = (
|
|||
|
||||
const value = serialize(parseFragment(val));
|
||||
|
||||
if (type === 'none') {
|
||||
return striptags(value)
|
||||
.replace(/>/gi, '>')
|
||||
.replace(/</gi, '<')
|
||||
.replace(/&/gi, '&')
|
||||
.replace(/ /gi, ' ')
|
||||
.replace(/"/gi, '"')
|
||||
.replace(/'/gi, "'");
|
||||
}
|
||||
|
||||
if (type === 'html') {
|
||||
return striptags(convertMention(value, convertMentionFunction), [
|
||||
'ul',
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import { GmbSettingsDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-s
|
|||
import { FarcasterDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/farcaster.dto';
|
||||
import { FacebookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/facebook.dto';
|
||||
import { MoltbookDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/moltbook.dto';
|
||||
import { SkoolDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/skool.dto';
|
||||
|
||||
export type ProviderExtension<T extends string, M> = { __type: T } & M;
|
||||
export type AllProvidersSettings =
|
||||
|
|
@ -53,7 +54,8 @@ export type AllProvidersSettings =
|
|||
| ProviderExtension<'telegram', None>
|
||||
| ProviderExtension<'nostr', None>
|
||||
| ProviderExtension<'moltbook', MoltbookDto>
|
||||
| ProviderExtension<'vk', None>;
|
||||
| ProviderExtension<'vk', None>
|
||||
| ProviderExtension<'skool', SkoolDto>;
|
||||
|
||||
type None = NonNullable<unknown>;
|
||||
|
||||
|
|
@ -89,6 +91,7 @@ export const allProviders = (setEmpty?: any) => {
|
|||
{ value: setEmpty, name: 'nostr' },
|
||||
{ value: setEmpty, name: 'vk' },
|
||||
{ value: MoltbookDto, name: 'moltbook' },
|
||||
{ value: SkoolDto, name: 'skool' },
|
||||
].filter((f) => f.value);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
import { IsDefined, IsString, MinLength } from 'class-validator';
|
||||
import { JSONSchema } from 'class-validator-jsonschema';
|
||||
|
||||
export class SkoolDto {
|
||||
@MinLength(1)
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@JSONSchema({
|
||||
description: 'Group must be an id',
|
||||
})
|
||||
group: string;
|
||||
|
||||
@MinLength(1)
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@JSONSchema({
|
||||
description: 'Label must be an id',
|
||||
})
|
||||
label: string;
|
||||
|
||||
@MinLength(1)
|
||||
@IsDefined()
|
||||
@IsString()
|
||||
@JSONSchema({
|
||||
description: 'Title of the post',
|
||||
})
|
||||
title: string;
|
||||
}
|
||||
|
|
@ -33,6 +33,7 @@ import { KickProvider } from '@gitroom/nestjs-libraries/integrations/social/kick
|
|||
import { TwitchProvider } from '@gitroom/nestjs-libraries/integrations/social/twitch.provider';
|
||||
import { SocialAbstract } from '@gitroom/nestjs-libraries/integrations/social.abstract';
|
||||
import { MoltbookProvider } from '@gitroom/nestjs-libraries/integrations/social/moltbook.provider';
|
||||
import { SkoolProvider } from '@gitroom/nestjs-libraries/integrations/social/skool.provider';
|
||||
|
||||
export const socialIntegrationList: Array<SocialAbstract & SocialProvider> = [
|
||||
new XProvider(),
|
||||
|
|
@ -65,6 +66,7 @@ export const socialIntegrationList: Array<SocialAbstract & SocialProvider> = [
|
|||
new WordpressProvider(),
|
||||
new ListmonkProvider(),
|
||||
new MoltbookProvider(),
|
||||
new SkoolProvider(),
|
||||
// new MastodonCustomProvider(),
|
||||
];
|
||||
|
||||
|
|
@ -80,6 +82,8 @@ export class IntegrationManager {
|
|||
editor: p.editor,
|
||||
isExternal: !!p.externalUrl,
|
||||
isWeb3: !!p.isWeb3,
|
||||
isChromeExtension: !!p.isChromeExtension,
|
||||
...(p.extensionCookies ? { extensionCookies: p.extensionCookies } : {}),
|
||||
...(p.customFields ? { customFields: await p.customFields() } : {}),
|
||||
}))
|
||||
),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';
|
||||
import { SocialAbstract } from '../social.abstract';
|
||||
import {
|
||||
AuthTokenDetails,
|
||||
MediaContent,
|
||||
PostDetails,
|
||||
PostResponse,
|
||||
SocialProvider,
|
||||
} from './social.integrations.interface';
|
||||
import dayjs from 'dayjs';
|
||||
import { Integration } from '@prisma/client';
|
||||
import { Tool } from '@gitroom/nestjs-libraries/integrations/tool.decorator';
|
||||
import { SkoolDto } from '@gitroom/nestjs-libraries/dtos/posts/providers-settings/skool.dto';
|
||||
import { AuthService } from '@gitroom/helpers/auth/auth.service';
|
||||
|
||||
export class SkoolProvider extends SocialAbstract implements SocialProvider {
|
||||
identifier = 'skool';
|
||||
name = 'Skool';
|
||||
isBetweenSteps = false;
|
||||
isChromeExtension = true;
|
||||
scopes = [] as string[];
|
||||
editor = 'normal' as const;
|
||||
dto = SkoolDto;
|
||||
|
||||
extensionCookies = [
|
||||
{ name: 'client_id', domain: '.skool.com' },
|
||||
{ name: 'auth_token', domain: '.skool.com' },
|
||||
];
|
||||
|
||||
private getCookies(integration: Integration): {
|
||||
client_id: string;
|
||||
auth_token: string;
|
||||
} {
|
||||
return AuthService.verifyJWT(integration.customInstanceDetails!) as {
|
||||
client_id: string;
|
||||
auth_token: string;
|
||||
};
|
||||
}
|
||||
|
||||
override handleErrors(
|
||||
body: string
|
||||
):
|
||||
| { type: 'refresh-token' | 'bad-body' | 'retry'; value: string }
|
||||
| undefined {
|
||||
if (body.includes('must be admin or level')) {
|
||||
return { type: 'bad-body', value: 'You can\'t post to this channel' };
|
||||
}
|
||||
if (body.includes('cannot post to this label')) {
|
||||
return { type: 'bad-body', value: 'Cannot post to this label' };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
maxLength() {
|
||||
return 5000;
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string): Promise<AuthTokenDetails> {
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: 0,
|
||||
accessToken: '',
|
||||
id: '',
|
||||
name: '',
|
||||
picture: '',
|
||||
username: '',
|
||||
};
|
||||
}
|
||||
|
||||
async generateAuthUrl() {
|
||||
const state = makeId(6);
|
||||
return {
|
||||
url: state,
|
||||
codeVerifier: makeId(10),
|
||||
state,
|
||||
};
|
||||
}
|
||||
|
||||
async authenticate(params: {
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
refresh?: string;
|
||||
}) {
|
||||
try {
|
||||
const cookies: Record<string, string> = JSON.parse(
|
||||
Buffer.from(params.code, 'base64').toString()
|
||||
);
|
||||
|
||||
const missing = this.extensionCookies
|
||||
.map((c) => c.name)
|
||||
.filter((name) => !cookies[name]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
return `Missing required cookies: ${missing.join(', ')}`;
|
||||
}
|
||||
|
||||
const data = await (
|
||||
await fetch('https://api2.skool.com/self', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: `auth_token=${cookies.auth_token}; client_id=${cookies.client_id}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
return {
|
||||
refreshToken: '',
|
||||
expiresIn: dayjs().add(100, 'year').unix() - dayjs().unix(),
|
||||
accessToken: AuthService.signJWT(cookies),
|
||||
id: data.id,
|
||||
name: data.first_name + ' ' + data.last_name,
|
||||
picture: data.metadata.picture_profile || '',
|
||||
username: data.name,
|
||||
};
|
||||
} catch (e) {
|
||||
return 'Invalid cookie data';
|
||||
}
|
||||
}
|
||||
|
||||
@Tool({ description: 'Groups', dataSchema: [] })
|
||||
async groups(accessToken: string, params: any, id: string, integration: Integration) {
|
||||
try {
|
||||
const { client_id, auth_token } = this.getCookies(integration);
|
||||
const { groups } = await (
|
||||
await fetch(
|
||||
`https://api2.skool.com/users/${id}/groups?offset=0&limit=30`,
|
||||
{
|
||||
headers: {
|
||||
Cookie: `auth_token=${auth_token}; client_id=${client_id}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
).json();
|
||||
|
||||
return groups.map((p: any) => ({
|
||||
id: String(p.id),
|
||||
name: p.metadata.display_name,
|
||||
}));
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@Tool({ description: 'Label', dataSchema: [] })
|
||||
async label(accessToken: string, params: any, id: string, integration: Integration) {
|
||||
try {
|
||||
const { client_id, auth_token } = this.getCookies(integration);
|
||||
const { metadata } = await (
|
||||
await this.fetch(`https://api2.skool.com/groups/${params.id}`, {
|
||||
headers: {
|
||||
Cookie: `auth_token=${auth_token}; client_id=${client_id}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
|
||||
if (!metadata.labels || metadata.labels.length === 0) {
|
||||
return [{ id: 'none', name: 'Default Label' }];
|
||||
}
|
||||
|
||||
const labels = metadata.labels.split(',');
|
||||
|
||||
if (labels.length === 0) {
|
||||
return [{ id: 'none', name: 'Default Label' }];
|
||||
}
|
||||
|
||||
const labelInformation = await Promise.all(
|
||||
labels.map(async (labelId: string) => {
|
||||
return (
|
||||
await this.fetch(`https://api2.skool.com/labels/${labelId}`, {
|
||||
headers: {
|
||||
Cookie: `auth_token=${auth_token}; client_id=${client_id}`,
|
||||
},
|
||||
})
|
||||
).json();
|
||||
})
|
||||
);
|
||||
|
||||
return labelInformation.map((p: any) => ({
|
||||
id: String(p.id),
|
||||
name: p.metadata.display_name,
|
||||
}));
|
||||
} catch (err) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private async uploadMediaToSkool(
|
||||
media: MediaContent[],
|
||||
userId: string,
|
||||
cookies: { client_id: string; auth_token: string }
|
||||
): Promise<string> {
|
||||
if (!media || media.length === 0) return '';
|
||||
|
||||
const fileIds: string[] = [];
|
||||
|
||||
for (const item of media) {
|
||||
const fileResponse = await fetch(item.path);
|
||||
const fileBuffer = await fileResponse.arrayBuffer();
|
||||
const contentType =
|
||||
fileResponse.headers.get('content-type') || 'application/octet-stream';
|
||||
const fileName = item.path.split('/').pop() || 'file';
|
||||
|
||||
const createFileResponse = await (
|
||||
await this.fetch('https://api2.skool.com/files', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Cookie: `auth_token=${cookies.auth_token}; client_id=${cookies.client_id}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
file_name: fileName,
|
||||
content_type: contentType,
|
||||
content_length: fileBuffer.byteLength,
|
||||
content_disposition: '',
|
||||
ref: '',
|
||||
owner_id: userId,
|
||||
large_thumbnail: false,
|
||||
}),
|
||||
}, 'create file record')
|
||||
).json();
|
||||
|
||||
await fetch(createFileResponse.write_url, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': createFileResponse.content_type,
|
||||
'x-amz-acl': createFileResponse.acl,
|
||||
},
|
||||
body: fileBuffer,
|
||||
});
|
||||
|
||||
fileIds.push(createFileResponse.file.id);
|
||||
}
|
||||
|
||||
return fileIds.join(',');
|
||||
}
|
||||
|
||||
async post(
|
||||
id: string,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const { client_id, auth_token } = this.getCookies(integration);
|
||||
const [post] = postDetails;
|
||||
|
||||
const attachments = await this.uploadMediaToSkool(
|
||||
post.media || [],
|
||||
id,
|
||||
{ client_id, auth_token }
|
||||
);
|
||||
|
||||
const { id: postId, name } = await (
|
||||
await this.fetch('https://api2.skool.com/posts?follow=true', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: `auth_token=${auth_token}; client_id=${client_id}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
post_type: 'generic',
|
||||
group_id: post.settings.group,
|
||||
metadata: {
|
||||
title: post.settings.title,
|
||||
content: post.message,
|
||||
attachments,
|
||||
...(post.settings.label && post.settings.label !== 'none'
|
||||
? { labels: post.settings.label }
|
||||
: {}),
|
||||
action: 0,
|
||||
video_ids: '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: String(postId),
|
||||
postId,
|
||||
releaseURL: `https://www.skool.com/${post.settings.group}/${name}`,
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async comment(
|
||||
id: string,
|
||||
postId: string,
|
||||
lastCommentId: string | undefined,
|
||||
accessToken: string,
|
||||
postDetails: PostDetails[],
|
||||
integration: Integration
|
||||
): Promise<PostResponse[]> {
|
||||
const { client_id, auth_token } = this.getCookies(integration);
|
||||
const [post] = postDetails;
|
||||
|
||||
const attachments = await this.uploadMediaToSkool(
|
||||
post.media || [],
|
||||
id,
|
||||
{ client_id, auth_token }
|
||||
);
|
||||
|
||||
const { id: postIdFinal, name } = await (
|
||||
await this.fetch('https://api2.skool.com/posts?follow=true', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Cookie: `auth_token=${auth_token}; client_id=${client_id}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
post_type: 'comment',
|
||||
group_id: post.settings.group,
|
||||
root_id: postId,
|
||||
parent_id: lastCommentId || postId,
|
||||
metadata: {
|
||||
title: '',
|
||||
content: post.message,
|
||||
attachments,
|
||||
action: 0,
|
||||
video_ids: '',
|
||||
},
|
||||
}),
|
||||
})
|
||||
).json();
|
||||
|
||||
return [
|
||||
{
|
||||
id: String(id),
|
||||
postId: postIdFinal,
|
||||
releaseURL: `https://www.skool.com/${post.settings.group}/${name}`,
|
||||
status: 'success',
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -141,7 +141,9 @@ export interface SocialProvider
|
|||
dto?: any;
|
||||
maxLength: (additionalSettings?: any) => number;
|
||||
isWeb3?: boolean;
|
||||
editor: 'normal' | 'markdown' | 'html';
|
||||
isChromeExtension?: boolean;
|
||||
extensionCookies?: { name: string; domain: string }[];
|
||||
editor: 'none' | 'normal' | 'markdown' | 'html';
|
||||
customFields?: () => Promise<
|
||||
{
|
||||
key: string;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ interface VariableContextInterface {
|
|||
dub: boolean;
|
||||
transloadit: string[];
|
||||
sentryDsn: string;
|
||||
extensionId: string;
|
||||
}
|
||||
const VariableContext = createContext({
|
||||
stripeClient: '',
|
||||
|
|
@ -49,6 +50,7 @@ const VariableContext = createContext({
|
|||
dub: false,
|
||||
transloadit: [],
|
||||
sentryDsn: '',
|
||||
extensionId: '',
|
||||
} as VariableContextInterface);
|
||||
export const VariableContextComponent: FC<
|
||||
VariableContextInterface & {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue