-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor to support multiple accounts (#187)
* Add basic structure for multiple accounts * Add octokit * Move note fetching from gitnews to octokit in main process * Remove gitnews as dependency * Temporarily disable marking as read * Change note fetching to use actual account info * Add comment avatar to each note * Add subject data to notes * Make both comment and subject required but provide comment fallback This matches the behavior of gitnews. * Add gitnewsAccountId to each note * Add mark as read functionality using Octokit * Fix markNoficationRead for default account * Add proxy support for github requests * Add input form for account proxy * Fix baseUrl for Github Enterprise Server * Add support for socks proxies * Log github fetch errors to main logger * Add account edit page * Remove editing for main token * Re-fetch when accounts change * Add migration from old token to new account * Improve logging about notifications retrieved
- Loading branch information
1 parent
4ed1885
commit 34f2d48
Showing
22 changed files
with
858 additions
and
334 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,10 +14,15 @@ | |
"package": "electron-forge package", | ||
"make": "electron-forge make" | ||
}, | ||
"keywords": ["menu", "github", "notifications"], | ||
"keywords": [ | ||
"menu", | ||
"github", | ||
"notifications" | ||
], | ||
"author": "Payton Swick <[email protected]>", | ||
"license": "MIT", | ||
"dependencies": { | ||
"@octokit/rest": "^21.0.2", | ||
"@types/semver": "^7.5.8", | ||
"date-fns": "^3.6.0", | ||
"debug": "^4.3.7", | ||
|
@@ -29,7 +34,7 @@ | |
"electron-squirrel-startup": "^1.0.1", | ||
"electron-store": "^8.2.0", | ||
"electron-unhandled": "^5.0.0", | ||
"gitnews": "^3.1.3", | ||
"fetch-socks": "^1.3.0", | ||
"gridicons": "^3.4.2", | ||
"jest": "^29.7.0", | ||
"jest-environment-jsdom": "^29.7.0", | ||
|
@@ -41,7 +46,8 @@ | |
"redux-logger": "^3.0.6", | ||
"redux-persist": "^6.0.0", | ||
"semver": "^7.6.3", | ||
"source-map-support": "^0.5.19" | ||
"source-map-support": "^0.5.19", | ||
"undici": "^6.19.8" | ||
}, | ||
"devDependencies": { | ||
"@babel/plugin-proposal-class-properties": "^7.18.6", | ||
|
@@ -86,6 +92,8 @@ | |
} | ||
}, | ||
"lint-staged": { | ||
"*.{js,ts,tsx}": ["prettier --write"] | ||
"*.{js,ts,tsx}": [ | ||
"prettier --write" | ||
] | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,176 @@ | ||
import type { AccountInfo, Note, NoteReason } from '../../shared-types'; | ||
import { Octokit } from '@octokit/rest'; | ||
import { fetch as undiciFetch, ProxyAgent } from 'undici'; | ||
import { socksDispatcher } from 'fetch-socks'; | ||
import { logMessage } from './logging'; | ||
|
||
const userAgent = 'gitnews-menubar'; | ||
const mainGithubApiUrl = 'https://api.github.com'; | ||
|
||
function getSocksVersionFromProtocol(protocol: string): 4 | 5 { | ||
const lastCharNum = parseInt(protocol.charAt(-1), 10); | ||
if (Number.isInteger(lastCharNum) && lastCharNum === 4) { | ||
return 4; | ||
} | ||
return 5; | ||
} | ||
|
||
function makeProxyDispatcher(proxyUrl: string) { | ||
if (proxyUrl.startsWith('socks')) { | ||
// eg: `socks5://user:pass@host:port` (user and pass are optional so | ||
// `socks5://host:port` also works) | ||
const proxyUrlData = new URL(proxyUrl); | ||
const socksVersion = getSocksVersionFromProtocol(proxyUrlData.protocol); | ||
// FIXME: support user and pass | ||
const slashParts = proxyUrl.split('/'); | ||
const hostParts = slashParts.at(-1)?.split('@').at(-1)?.split(':') ?? []; | ||
const socksHost = hostParts[0]; | ||
const socksPort = hostParts[1]; | ||
if (socksVersion && socksHost && socksPort) { | ||
return socksDispatcher({ | ||
type: socksVersion, | ||
host: socksHost, | ||
port: parseInt(socksPort, 10), | ||
}); | ||
} | ||
} | ||
return new ProxyAgent(proxyUrl); | ||
} | ||
|
||
function makeProxyFetch(proxyUrl: string) { | ||
return (url: string, options: any) => { | ||
return undiciFetch(url, { | ||
...options, | ||
dispatcher: makeProxyDispatcher(proxyUrl), | ||
}); | ||
}; | ||
} | ||
|
||
function createOctokit(account: AccountInfo) { | ||
const options = { | ||
auth: account.apiKey, | ||
baseUrl: getBaseUrlForServer(account), | ||
userAgent, | ||
}; | ||
if (account.proxyUrl) { | ||
const proxyFetch = makeProxyFetch(account.proxyUrl); | ||
return new Octokit({ | ||
...options, | ||
request: { | ||
fetch: proxyFetch, | ||
}, | ||
}); | ||
} | ||
return new Octokit(options); | ||
} | ||
|
||
function getBaseUrlForServer(account: AccountInfo): string | undefined { | ||
if (account.serverUrl === mainGithubApiUrl) { | ||
return undefined; | ||
} | ||
// GitHub Enterprise Servers use this URL structure: | ||
// https://github.com/octokit/octokit.js/?tab=readme-ov-file#octokit-api-client | ||
const serverUrl = account.serverUrl.replace(/\/$/, ''); | ||
return `${serverUrl}/api/v3`; | ||
} | ||
|
||
function getOctokitRequestPathFromUrl( | ||
account: AccountInfo, | ||
urlString: string | ||
): string { | ||
const baseUrl = getBaseUrlForServer(account) ?? mainGithubApiUrl; | ||
return urlString.replace(baseUrl, ''); | ||
} | ||
|
||
export async function markNotficationAsRead( | ||
note: Note, | ||
account: AccountInfo | ||
): Promise<void> { | ||
const octokit = createOctokit(account); | ||
const path = getOctokitRequestPathFromUrl(account, note.url); | ||
try { | ||
await octokit.request(`PATCH ${path}`, { | ||
thread_id: note.id, | ||
}); | ||
} catch (error) { | ||
logMessage( | ||
`Failed to mark notification read for ${path} (${note.url})`, | ||
'error' | ||
); | ||
return; | ||
} | ||
} | ||
|
||
export async function fetchNotificationsForAccount( | ||
account: AccountInfo | ||
): Promise<Note[]> { | ||
const octokit = createOctokit(account); | ||
const notificationsResponse = | ||
await octokit.rest.activity.listNotificationsForAuthenticatedUser({ | ||
all: false, | ||
}); | ||
|
||
const notes: Note[] = []; | ||
|
||
for (const notification of notificationsResponse.data) { | ||
let commentAvatar: string; | ||
let commentHtmlUrl: string; | ||
const commentPath = getOctokitRequestPathFromUrl( | ||
account, | ||
notification.subject.latest_comment_url ?? notification.subject.url | ||
); | ||
try { | ||
const comment = await octokit.request(`GET ${commentPath}`, {}); | ||
commentAvatar = comment.data.user.avatar_url; | ||
commentHtmlUrl = comment.data.html_url; | ||
} catch (error) { | ||
logMessage( | ||
`Failed to fetch comment for ${commentPath} (${notification.subject.latest_comment_url ?? notification.subject.url})`, | ||
'error' | ||
); | ||
continue; | ||
} | ||
|
||
let noteState: string; | ||
let noteMerged: boolean; | ||
let subjectHtmlUrl: string; | ||
const subjectPath = getOctokitRequestPathFromUrl( | ||
account, | ||
notification.subject.url | ||
); | ||
try { | ||
const subject = await octokit.request(`GET ${subjectPath}`, {}); | ||
noteState = subject.data.state; | ||
noteMerged = subject.data.merged; | ||
subjectHtmlUrl = subject.data.html_url; | ||
} catch (error) { | ||
logMessage( | ||
`Failed to fetch comment for ${subjectPath} (${notification.subject.url})`, | ||
'error' | ||
); | ||
continue; | ||
} | ||
|
||
notes.push({ | ||
gitnewsAccountId: account.id, | ||
id: notification.id, | ||
url: notification.url, | ||
title: notification.subject.title, | ||
unread: notification.unread, | ||
repositoryFullName: notification.repository.full_name, | ||
commentUrl: commentHtmlUrl, | ||
updatedAt: notification.updated_at, | ||
repositoryName: notification.repository.name, | ||
type: notification.subject.type, | ||
subjectUrl: subjectHtmlUrl, | ||
commentAvatar: commentAvatar ?? notification.repository.owner.avatar_url, | ||
repositoryOwnerAvatar: notification.repository.owner.avatar_url, | ||
api: { | ||
subject: { state: noteState, merged: noteMerged }, | ||
notification: { reason: notification.reason as NoteReason }, | ||
}, | ||
}); | ||
} | ||
|
||
return notes; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import log from 'electron-log'; | ||
import debugFactory from 'debug'; | ||
import { isLoggingEnabled } from './main-store'; | ||
|
||
const debug = debugFactory('gitnews-menubar'); | ||
|
||
// Only use this function for logging! | ||
export function logMessage( | ||
message: string, | ||
level: 'info' | 'warn' | 'error' | ||
): void { | ||
debug(message); | ||
if (!isLoggingEnabled()) { | ||
return; | ||
} | ||
switch (level) { | ||
case 'info': | ||
log.info(message); | ||
break; | ||
case 'warn': | ||
log.warn(message); | ||
break; | ||
case 'error': | ||
log.error(message); | ||
break; | ||
default: | ||
log.error(`Unknown log level '${level}': ${message}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.