| 
									
										
										
										
											2025-06-24 23:14:52 +03:00
										 |  |  | /** | 
					
						
							|  |  |  |  * @module | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Goes through all discussions in the source repository and transfers them to the target repository. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * Limitations: | 
					
						
							|  |  |  |  * - Upon encountering a locked discussion, the script will fail. Make sure to unlock discussions before running the script. | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import { type BrowserContext, chromium } from 'playwright'; | 
					
						
							|  |  |  | import { createWriteStream, existsSync, readFileSync, writeFileSync } from 'fs'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-06-30 20:39:29 +03:00
										 |  |  | const SOURCE_URL = "https://github.com/TriliumNext/Trilium"; | 
					
						
							| 
									
										
										
										
											2025-06-24 23:14:52 +03:00
										 |  |  | const TARGET_REPOSITORY_ID = 92111509; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | const fsLog = createWriteStream('port-discussions.log', { flags: 'a' }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function login(context: BrowserContext) { | 
					
						
							|  |  |  |     const page = await context.newPage(); | 
					
						
							|  |  |  |     await page.goto('https://github.com/login'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     console.log("👤 Please log in manually in the opened browser..."); | 
					
						
							|  |  |  |     await page.waitForNavigation({ url: 'https://github.com/' }); // Wait for login
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Save storage state (cookies, localStorage, etc.)
 | 
					
						
							|  |  |  |     const storage = await context.storageState(); | 
					
						
							|  |  |  |     writeFileSync('auth.json', JSON.stringify(storage)) | 
					
						
							|  |  |  |     await page.close(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function portIssue(issue: string, context: BrowserContext) { | 
					
						
							|  |  |  |     const page = await context.newPage(); | 
					
						
							|  |  |  |     await page.goto(`${SOURCE_URL}/discussions/${issue}`); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const button = page.locator("#dialog-show-discussion-transfer-conversation"); | 
					
						
							|  |  |  |     await button.click(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const modal = page.locator("#discussion-transfer-conversation"); | 
					
						
							|  |  |  |     const modalContent = page.locator("#transfer-candidate-repos"); | 
					
						
							|  |  |  |     await modalContent.waitFor({ state: 'visible' }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     modalContent.locator(`#transfer_repository_${TARGET_REPOSITORY_ID}`).click(); | 
					
						
							|  |  |  |     const navigationPromise = page.waitForNavigation({ | 
					
						
							|  |  |  |         waitUntil: "domcontentloaded" | 
					
						
							|  |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const submitButton = modal.locator(`button[type="submit"]`); | 
					
						
							|  |  |  |     await submitButton.waitFor({ state: 'attached' }); | 
					
						
							|  |  |  |     await submitButton.click(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     await navigationPromise; | 
					
						
							|  |  |  |     console.log(`✅ Discussion ${issue} has been transferred to the target repository.`); | 
					
						
							|  |  |  |     fsLog.write(`Transferred discussion ${issue} to ${page.url()}\n`); | 
					
						
							|  |  |  |     await page.waitForTimeout(2000); // Wait for a second to ensure the transfer is complete
 | 
					
						
							|  |  |  |     await page.close(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | async function getFirstPageResults(context: BrowserContext) { | 
					
						
							|  |  |  |     const page = await context.newPage(); | 
					
						
							|  |  |  |     await page.goto(SOURCE_URL + "/discussions"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Wait for the discussions to load
 | 
					
						
							|  |  |  |     const allDiscussionLinks = (await (page.locator(`a[data-hovercard-type="discussion"]`).all())); | 
					
						
							|  |  |  |     let ids: string[] = []; | 
					
						
							|  |  |  |     for (const link of allDiscussionLinks) { | 
					
						
							|  |  |  |         const url = await link.getAttribute('href'); | 
					
						
							|  |  |  |         const number = url?.match(/\/discussions\/(\d+)/)?.[1]; | 
					
						
							|  |  |  |         ids.push(number); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     console.log(`Found ${ids.length} discussions.`); | 
					
						
							|  |  |  |     await page.close(); | 
					
						
							|  |  |  |     return ids; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | (async () => { | 
					
						
							|  |  |  |     const browser = await chromium.launch({ headless: false }); // show browser
 | 
					
						
							|  |  |  |     let storageState = undefined; | 
					
						
							|  |  |  |     if (existsSync('auth.json')) { | 
					
						
							|  |  |  |         console.log("🔑 Using existing authentication state..."); | 
					
						
							|  |  |  |         storageState = JSON.parse(readFileSync('auth.json', 'utf-8')); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const context = await browser.newContext({ storageState }); | 
					
						
							|  |  |  |     if (!storageState) { | 
					
						
							|  |  |  |         await login(context); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const travelledIds: string[] = []; | 
					
						
							|  |  |  |     let ids = await getFirstPageResults(context); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     while (ids.length > 0) { | 
					
						
							|  |  |  |         for (const id of ids) { | 
					
						
							|  |  |  |             try { | 
					
						
							|  |  |  |                 if (travelledIds.includes(id)) { | 
					
						
							|  |  |  |                     console.log(`Discussion ${id} has already been transferred.`); | 
					
						
							|  |  |  |                     process.exit(2); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 await portIssue(id, context); | 
					
						
							|  |  |  |                 travelledIds.push(id); | 
					
						
							|  |  |  |             } catch (error) { | 
					
						
							|  |  |  |                 console.error(`❌ Error transferring discussion ${id}:`, error); | 
					
						
							|  |  |  |                 process.exit(1); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         ids = await getFirstPageResults(context); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     await browser.close(); | 
					
						
							|  |  |  | })(); |