Support personal profile LinkedIn posting
- OAuth callback fetches /v2/userinfo to store member_id + name, auto-creates a 'profile' personal page on first connect - Page modal adds type selector (personal/org), hides org ID for personal - renderLinkedinPages shows 'Personal' badge instead of org ID - botconfig saves type field; personal pages omit organization_id Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9b5bf89d11
commit
09e3406847
4 changed files with 47 additions and 6 deletions
|
|
@ -222,6 +222,13 @@
|
|||
<input type="text" id="linkedinPageLabel" class="form-control form-control-sm" placeholder="My Organisation">
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label class="form-label small">Type</label>
|
||||
<select id="linkedinPageType" class="form-select form-select-sm">
|
||||
<option value="personal">Personal Profile</option>
|
||||
<option value="organization">Organization Page</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-2" id="linkedinPageOrgIdRow">
|
||||
<label class="form-label small">Organization ID</label>
|
||||
<input type="text" id="linkedinPageOrgId" class="form-control form-control-sm font-monospace"
|
||||
placeholder="123456789">
|
||||
|
|
|
|||
|
|
@ -169,10 +169,10 @@ if ($action === 'add_linkedin_page' || $action === 'save_linkedin_page') {
|
|||
}
|
||||
if (!isset($config['linkedin'])) $config['linkedin'] = [];
|
||||
if (!isset($config['linkedin']['pages'])) $config['linkedin']['pages'] = [];
|
||||
$config['linkedin']['pages'][$name] = [
|
||||
'label' => trim($data['label'] ?? $name),
|
||||
'organization_id' => trim($data['organization_id'] ?? ''),
|
||||
];
|
||||
$type = in_array($data['type'] ?? '', ['personal', 'organization']) ? $data['type'] : 'organization';
|
||||
$entry = ['label' => trim($data['label'] ?? $name), 'type' => $type];
|
||||
if ($type === 'organization') $entry['organization_id'] = trim($data['organization_id'] ?? '');
|
||||
$config['linkedin']['pages'][$name] = $entry;
|
||||
write_config($config_path, $config);
|
||||
Audit::log($db, 'botconfig_linkedin_page_' . ($action === 'add_linkedin_page' ? 'add' : 'save'), $pid, $name);
|
||||
echo json_encode(['ok' => true]);
|
||||
|
|
|
|||
|
|
@ -3424,9 +3424,13 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
if (!names.length) { list.innerHTML = '<div class="text-muted small">No pages configured.</div>'; return; }
|
||||
list.innerHTML = names.map(name => {
|
||||
const p = pages[name];
|
||||
const isPersonal = p.type === 'personal';
|
||||
const badge = isPersonal
|
||||
? `<span class="badge bg-secondary fw-normal">Personal</span>`
|
||||
: `<code class="small text-muted">${esc(p.organization_id || '–')}</code>`;
|
||||
return `<div class="d-flex align-items-center gap-2 border rounded p-2 mb-1">
|
||||
<span class="fw-semibold small">${esc(p.label || name)}</span>
|
||||
<code class="small text-muted">${esc(p.organization_id || '–')}</code>
|
||||
${badge}
|
||||
<div class="ms-auto d-flex gap-1">
|
||||
<button class="btn btn-xs btn-outline-secondary py-0 px-1 compose-linkedin-btn"
|
||||
data-name="${esc(name)}" data-label="${esc(p.label || name)}" title="Post to LinkedIn">
|
||||
|
|
@ -3441,6 +3445,11 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
list.querySelectorAll('.del-lipage-btn').forEach(b => b.addEventListener('click', () => deleteItem('remove_linkedin_page', b.dataset.name, `Remove page "${b.dataset.name}"?`, 'Page removed')));
|
||||
}
|
||||
|
||||
function _liPageTypeToggle() {
|
||||
const isPersonal = document.getElementById('linkedinPageType').value === 'personal';
|
||||
document.getElementById('linkedinPageOrgIdRow').classList.toggle('d-none', isPersonal);
|
||||
}
|
||||
|
||||
function openLinkedinPageModal(mode, name = '') {
|
||||
const p = (mode === 'edit' && _config?.linkedin?.pages?.[name]) || {};
|
||||
document.getElementById('linkedinPageModalTitle').textContent = mode === 'add' ? 'Add LinkedIn Page' : 'Edit LinkedIn Page';
|
||||
|
|
@ -3448,21 +3457,26 @@ window.addEventListener('DOMContentLoaded', () => {
|
|||
document.getElementById('linkedinPageName').value = name;
|
||||
document.getElementById('linkedinPageName').disabled = mode === 'edit';
|
||||
document.getElementById('linkedinPageLabel').value = p.label ?? '';
|
||||
document.getElementById('linkedinPageType').value = p.type ?? 'organization';
|
||||
document.getElementById('linkedinPageOrgId').value = p.organization_id ?? '';
|
||||
document.getElementById('linkedinPageModalError').classList.add('d-none');
|
||||
_liPageTypeToggle();
|
||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('linkedinPageModal')).show();
|
||||
}
|
||||
|
||||
document.getElementById('linkedinPageType').addEventListener('change', _liPageTypeToggle);
|
||||
document.getElementById('addLinkedinPageBtn').addEventListener('click', () => openLinkedinPageModal('add'));
|
||||
document.getElementById('linkedinPageModalSaveBtn').addEventListener('click', async () => {
|
||||
const mode = document.getElementById('linkedinPageModalMode').value;
|
||||
const name = document.getElementById('linkedinPageName').value.trim();
|
||||
const type = document.getElementById('linkedinPageType').value;
|
||||
const errEl = document.getElementById('linkedinPageModalError');
|
||||
errEl.classList.add('d-none');
|
||||
if (!name) { errEl.textContent = 'Key required'; errEl.classList.remove('d-none'); return; }
|
||||
const ok = await apiPost({ action: mode === 'add' ? 'add_linkedin_page' : 'save_linkedin_page', name, data: {
|
||||
label: document.getElementById('linkedinPageLabel').value.trim(),
|
||||
organization_id: document.getElementById('linkedinPageOrgId').value.trim(),
|
||||
type,
|
||||
organization_id: type === 'organization' ? document.getElementById('linkedinPageOrgId').value.trim() : '',
|
||||
}}, errEl);
|
||||
if (ok) { bootstrap.Modal.getOrCreateInstance(document.getElementById('linkedinPageModal')).hide(); showSuccess('Page saved'); loadConfig(); }
|
||||
});
|
||||
|
|
|
|||
|
|
@ -65,11 +65,31 @@ if (!file_exists($config_path)) {
|
|||
$config = json_decode(file_get_contents($config_path), true);
|
||||
if (!is_array($config)) { echo 'Could not parse config.json. <a href="/">Go back</a>'; exit; }
|
||||
|
||||
// Fetch member info to get personal posting URN and display name
|
||||
$ch2 = curl_init('https://api.linkedin.com/v2/userinfo');
|
||||
curl_setopt_array($ch2, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $data['access_token']],
|
||||
]);
|
||||
$me = json_decode(curl_exec($ch2), true);
|
||||
curl_close($ch2);
|
||||
|
||||
if (!isset($config['linkedin'])) $config['linkedin'] = [];
|
||||
$config['linkedin']['access_token'] = $data['access_token'];
|
||||
$config['linkedin']['refresh_token'] = $data['refresh_token'] ?? null;
|
||||
$expires_in = $data['expires_in'] ?? 5184000;
|
||||
$config['linkedin']['token_expiry'] = date('c', time() + $expires_in);
|
||||
$config['linkedin']['member_id'] = $me['sub'] ?? null;
|
||||
$config['linkedin']['member_name'] = $me['name'] ?? null;
|
||||
|
||||
// Auto-create personal profile page on first connect
|
||||
if (!isset($config['linkedin']['pages']['profile'])) {
|
||||
if (!isset($config['linkedin']['pages'])) $config['linkedin']['pages'] = [];
|
||||
$config['linkedin']['pages']['profile'] = [
|
||||
'label' => isset($me['name']) ? $me['name'] : 'Personal Profile',
|
||||
'type' => 'personal',
|
||||
];
|
||||
}
|
||||
|
||||
$tmp = $config_path . '.tmp';
|
||||
file_put_contents($tmp, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue