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:
Bashy 2026-05-28 13:25:19 +03:00
parent 9b5bf89d11
commit 09e3406847
4 changed files with 47 additions and 6 deletions

View file

@ -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">

View file

@ -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]);

View file

@ -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(); }
});

View file

@ -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));