9 changed files with 379 additions and 54 deletions
@ -0,0 +1,23 @@ |
|||||||
|
versions: |
||||||
|
'0.3.1': |
||||||
|
- 'Media attachments rendering in all feeds and views' |
||||||
|
- 'NIP-92/NIP-94 image tags support' |
||||||
|
- 'Blossom support for media attachments' |
||||||
|
- 'OP-only view added to feed' |
||||||
|
- 'Text to Speech (TTS) support added, for the languages: Czech, German, British English, American English, Spanish, French, Dutch, Polish, Russian, Turkish, and Chinese' |
||||||
|
'0.3.0': |
||||||
|
- 'Version history modal added to event menu' |
||||||
|
- 'Event and npub deletion/reporting added' |
||||||
|
- 'User and event reporting added' |
||||||
|
- 'Profile settings added' |
||||||
|
- 'Finished implementing themes' |
||||||
|
- 'Add Edit/Clone of all events' |
||||||
|
'0.2.1': |
||||||
|
- 'Themes added: Fog, Forum, Socialmedia, and Terminal' |
||||||
|
- 'Find page added: search for content by hashtags, users, and content, full-text search' |
||||||
|
'0.2.0': |
||||||
|
- 'Version management and update system' |
||||||
|
- 'About page with product information' |
||||||
|
- 'Improved user experience with automatic routing to About page on first visit and after updates' |
||||||
|
- 'Enhanced settings page with About button' |
||||||
|
- 'Better version tracking and display' |
||||||
@ -0,0 +1,92 @@ |
|||||||
|
/** |
||||||
|
* Changelog service - loads and provides access to version changelog |
||||||
|
*/ |
||||||
|
|
||||||
|
import yaml from 'js-yaml'; |
||||||
|
|
||||||
|
export interface ChangelogData { |
||||||
|
versions: Record<string, string[]>; |
||||||
|
} |
||||||
|
|
||||||
|
let cachedChangelog: ChangelogData | null = null; |
||||||
|
|
||||||
|
/** |
||||||
|
* Load changelog from YAML file |
||||||
|
*/ |
||||||
|
export async function loadChangelog(): Promise<ChangelogData> { |
||||||
|
if (cachedChangelog) { |
||||||
|
return cachedChangelog; |
||||||
|
} |
||||||
|
|
||||||
|
try { |
||||||
|
const response = await fetch('/changelog.yaml', { cache: 'no-store' }); |
||||||
|
if (!response.ok) { |
||||||
|
throw new Error(`Failed to fetch changelog: ${response.statusText}`); |
||||||
|
} |
||||||
|
|
||||||
|
const yamlText = await response.text(); |
||||||
|
const parsed = yaml.load(yamlText) as ChangelogData; |
||||||
|
|
||||||
|
// Validate structure
|
||||||
|
if (!parsed || !parsed.versions || typeof parsed.versions !== 'object') { |
||||||
|
throw new Error('Invalid changelog format'); |
||||||
|
} |
||||||
|
|
||||||
|
cachedChangelog = parsed; |
||||||
|
return cachedChangelog; |
||||||
|
} catch (error) { |
||||||
|
console.error('Failed to load changelog:', error); |
||||||
|
// Return empty changelog on error
|
||||||
|
return { versions: {} }; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get changelog for a specific version |
||||||
|
*/ |
||||||
|
export async function getChangelogForVersion(version: string): Promise<string[]> { |
||||||
|
const changelog = await loadChangelog(); |
||||||
|
return changelog.versions[version] || []; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get all versions in the changelog, sorted by version number (newest first) |
||||||
|
*/ |
||||||
|
export async function getAllVersions(): Promise<string[]> { |
||||||
|
const changelog = await loadChangelog(); |
||||||
|
const versions = Object.keys(changelog.versions); |
||||||
|
|
||||||
|
// Sort versions in descending order (newest first)
|
||||||
|
return versions.sort((a, b) => { |
||||||
|
const partsA = a.split('.').map(Number); |
||||||
|
const partsB = b.split('.').map(Number); |
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { |
||||||
|
const partA = partsA[i] || 0; |
||||||
|
const partB = partsB[i] || 0; |
||||||
|
if (partA !== partB) { |
||||||
|
return partB - partA; // Descending order
|
||||||
|
} |
||||||
|
} |
||||||
|
return 0; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get the newest version from the changelog |
||||||
|
*/ |
||||||
|
export async function getNewestVersion(): Promise<string | null> { |
||||||
|
const versions = await getAllVersions(); |
||||||
|
return versions.length > 0 ? versions[0] : null; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get changelog for the newest version |
||||||
|
*/ |
||||||
|
export async function getNewestChangelog(): Promise<string[]> { |
||||||
|
const newestVersion = await getNewestVersion(); |
||||||
|
if (!newestVersion) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
return getChangelogForVersion(newestVersion); |
||||||
|
} |
||||||
@ -0,0 +1,137 @@ |
|||||||
|
/** |
||||||
|
* GitHub API helper with rate limit detection and token fallback |
||||||
|
*/ |
||||||
|
|
||||||
|
import { sessionManager } from './auth/session-manager.js'; |
||||||
|
import { loadEncryptedApiKey } from './security/api-key-storage.js'; |
||||||
|
|
||||||
|
/** |
||||||
|
* Check if a GitHub API response indicates rate limiting |
||||||
|
*/ |
||||||
|
function isRateLimited(response: Response): boolean { |
||||||
|
// Check status code
|
||||||
|
if (response.status === 403 || response.status === 429) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
// Check rate limit headers
|
||||||
|
const remaining = response.headers.get('X-RateLimit-Remaining'); |
||||||
|
if (remaining !== null && parseInt(remaining, 10) === 0) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Get GitHub token from settings if available |
||||||
|
* Returns null if token is not available or cannot be decrypted |
||||||
|
*/ |
||||||
|
async function getGitHubToken(): Promise<string | null> { |
||||||
|
try { |
||||||
|
const session = sessionManager.getSession(); |
||||||
|
if (!session) { |
||||||
|
// No session, can't decrypt token
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// Try to get password from session (for nsec/anonymous methods)
|
||||||
|
let password: string | undefined = session.password; |
||||||
|
|
||||||
|
// If no password in session, try to get from sessionStorage (for nsec/anonymous)
|
||||||
|
if (!password && typeof window !== 'undefined') { |
||||||
|
try { |
||||||
|
const encryptedPassword = sessionStorage.getItem(`aitherboard_password_${session.pubkey}`); |
||||||
|
if (encryptedPassword) { |
||||||
|
// Decrypt password from sessionStorage
|
||||||
|
const key = session.pubkey.slice(0, 16); |
||||||
|
const encryptedBytes = atob(encryptedPassword); |
||||||
|
let decrypted = ''; |
||||||
|
for (let i = 0; i < encryptedBytes.length; i++) { |
||||||
|
const keyChar = key.charCodeAt(i % key.length); |
||||||
|
const encChar = encryptedBytes.charCodeAt(i); |
||||||
|
decrypted += String.fromCharCode(encChar ^ keyChar); |
||||||
|
} |
||||||
|
password = decrypted; |
||||||
|
} |
||||||
|
} catch { |
||||||
|
// Failed to decrypt password from sessionStorage
|
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If still no password, we can't decrypt the token
|
||||||
|
if (!password) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// Try to load the GitHub token
|
||||||
|
const token = await loadEncryptedApiKey('github.token', password); |
||||||
|
return token; |
||||||
|
} catch (error) { |
||||||
|
console.warn('Failed to get GitHub token:', error); |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Make a GitHub API request with automatic token fallback on rate limiting |
||||||
|
* @param url - GitHub API URL |
||||||
|
* @param options - Fetch options (headers will be merged) |
||||||
|
* @returns Response object |
||||||
|
*/ |
||||||
|
export async function fetchGitHubApi( |
||||||
|
url: string, |
||||||
|
options: RequestInit = {} |
||||||
|
): Promise<Response> { |
||||||
|
// First attempt without token
|
||||||
|
let response = await fetch(url, { |
||||||
|
...options, |
||||||
|
headers: { |
||||||
|
'Accept': 'application/vnd.github.v3+json', |
||||||
|
...options.headers |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Check if rate limited
|
||||||
|
if (isRateLimited(response)) { |
||||||
|
console.log('GitHub API rate limit detected, attempting to use saved token...'); |
||||||
|
|
||||||
|
// Try to get token from settings
|
||||||
|
const token = await getGitHubToken(); |
||||||
|
|
||||||
|
if (token) { |
||||||
|
console.log('Using saved GitHub token for authenticated request'); |
||||||
|
|
||||||
|
// Retry with token (use Bearer for compatibility with both classic and fine-grained tokens)
|
||||||
|
response = await fetch(url, { |
||||||
|
...options, |
||||||
|
headers: { |
||||||
|
'Accept': 'application/vnd.github.v3+json', |
||||||
|
'Authorization': `Bearer ${token}`, |
||||||
|
...options.headers |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
// Check if still rate limited (token might also be exhausted)
|
||||||
|
if (isRateLimited(response)) { |
||||||
|
const resetTime = response.headers.get('X-RateLimit-Reset'); |
||||||
|
if (resetTime) { |
||||||
|
const resetDate = new Date(parseInt(resetTime, 10) * 1000); |
||||||
|
console.warn(`GitHub API rate limit still exceeded even with token. Resets at: ${resetDate.toISOString()}`); |
||||||
|
} else { |
||||||
|
console.warn('GitHub API rate limit exceeded even with token'); |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
const resetTime = response.headers.get('X-RateLimit-Reset'); |
||||||
|
if (resetTime) { |
||||||
|
const resetDate = new Date(parseInt(resetTime, 10) * 1000); |
||||||
|
console.warn(`GitHub API rate limit exceeded. No token available. Resets at: ${resetDate.toISOString()}`); |
||||||
|
} else { |
||||||
|
console.warn('GitHub API rate limit exceeded. No token available in settings.'); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return response; |
||||||
|
} |
||||||
Loading…
Reference in new issue