├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── gifs ├── github.gif └── ics.gif ├── index.html ├── package-lock.json ├── package.json ├── src ├── github │ ├── pull_request.ts │ ├── query.ts │ └── review.ts ├── ics │ └── ics.ts ├── logseq │ ├── logseq.ts │ └── settings.ts └── main.ts ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Build plugin 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | env: 9 | PLUGIN_NAME: logsync 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: '19.x' 20 | - name: Build 21 | id: build 22 | run: | 23 | npm i && npm run build 24 | mkdir ${{ env.PLUGIN_NAME }} 25 | cp README.md package.json ${{ env.PLUGIN_NAME }} 26 | mv build ${{ env.PLUGIN_NAME }} 27 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 28 | ls 29 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 30 | - name: Create Release 31 | uses: ncipollo/release-action@v1 32 | id: create_release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | VERSION: ${{ github.ref }} 36 | with: 37 | allowUpdates: true 38 | draft: false 39 | prerelease: false 40 | - name: Upload zip file 41 | id: upload_zip 42 | uses: actions/upload-release-asset@v1 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | with: 46 | upload_url: ${{ steps.create_release.outputs.upload_url }} 47 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 48 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 49 | asset_content_type: application/zip 50 | - name: Upload package.json 51 | id: upload_metadata 52 | uses: actions/upload-release-asset@v1 53 | env: 54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | with: 56 | upload_url: ${{ steps.create_release.outputs.upload_url }} 57 | asset_path: ./package.json 58 | asset_name: package.json 59 | asset_content_type: application/json 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json') 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Claas Störtenbecker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Logsync 2 | 3 | ## Features 4 | 5 | - ICS 6 | - ![ICS showcase](./gifs/ics.gif) 7 | - Idempotent synchronization 8 | - Any number of calendars 9 | - Recurring events 10 | - Meeting links (google) 11 | - Event renaming 12 | - GitHub 13 | - ![GitHub showcase](./gifs/github.gif) 14 | - Idempotent synchronization 15 | - Created pull requests 16 | - Assigned review requests 17 | 18 | ## Configuration 19 | 20 | `$HOME/.logseq/settings/logsync.json` 21 | ```json 22 | { 23 | "calendars": { 24 | "some-calendar": "https://some.ics.url/basic.ics" 25 | }, 26 | "renaming": { 27 | "some-calendar": { 28 | "Some event name": "Some new event name" 29 | } 30 | }, 31 | "github-token": "ghp_...", 32 | "disabled": false 33 | } 34 | ``` 35 | 36 | ## Contributing 37 | 38 | Create a pull request or open an issue to report bugs and request features. 39 | -------------------------------------------------------------------------------- /gifs/github.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clstb/logsync/aa3c8b91ca240b70c6b4165be7a99b9fb78492b3/gifs/github.gif -------------------------------------------------------------------------------- /gifs/ics.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clstb/logsync/aa3c8b91ca240b70c6b4165be7a99b9fb78492b3/gifs/ics.gif -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Document 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "logsync", 3 | "version": "0.0.1", 4 | "author": "clstb", 5 | "description": "Sync data from various sources into Logseq", 6 | "license": "MIT", 7 | "type": "commonjs", 8 | "dependencies": { 9 | "@logseq/libs": "^0.0.14", 10 | "@types/uuid": "^9.0.1", 11 | "@typescript-eslint/typescript-estree": "^6.13.2-alpha.7", 12 | "axios": "^1.6.2", 13 | "gts": "^5.2.0", 14 | "jira.js": "^2.17.0", 15 | "luxon": "3.3.0", 16 | "microdiff": "^1.3.2", 17 | "mitt": "^3.0.0", 18 | "node-ical": "^0.17.1", 19 | "octokit": "^3.1.2", 20 | "uuid": "^9.0.0", 21 | "vite": "^5.0.10" 22 | }, 23 | "devDependencies": { 24 | "@types/luxon": "^3.2.0", 25 | "@types/node": "20.8.2", 26 | "buffer": "^5.7.1", 27 | "events": "^3.3.0", 28 | "gts": "^5.2.0", 29 | "https-browserify": "^1.0.0", 30 | "path-browserify": "^1.0.1", 31 | "punycode": "^1.4.1", 32 | "querystring-es3": "^0.2.1", 33 | "stream-browserify": "^3.0.0", 34 | "stream-http": "^3.2.0", 35 | "typescript": "^5.2.0" 36 | }, 37 | "scripts": { 38 | "tsc": "tsc", 39 | "build": "vite build", 40 | "lint": "gts lint", 41 | "clean": "gts clean", 42 | "compile": "tsc", 43 | "fix": "gts fix", 44 | "pretest": "npm run compile", 45 | "posttest": "npm run lint" 46 | }, 47 | "logseq": { 48 | "main": "build/index.html", 49 | "id": "logsync" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/github/pull_request.ts: -------------------------------------------------------------------------------- 1 | import {Block} from './logseq'; 2 | import {BlockUUID, BlockEntity} from '@logseq/libs/dist/LSPlugin.user'; 3 | import {Octokit} from 'octokit'; 4 | import {DateTime} from 'luxon'; 5 | import util from 'util'; 6 | import {v5} from 'uuid'; 7 | import {PRQuery, PRReviewQuery, PRByIdQuery} from './query'; 8 | import {Review} from './review'; 9 | 10 | const namespace = '4bd381ac-4474-4b43-ac28-17e0c6c1ebd3'; 11 | 12 | export const PullRequestState = { 13 | id: '', 14 | title: '', 15 | repository: '', 16 | state: '', 17 | url: '', 18 | created: '', 19 | updated: '', 20 | reviews: '', 21 | }; 22 | 23 | function formatDate(date: string) { 24 | const parsed = DateTime.fromISO(date); 25 | // Format date to 2023-04-13 26 | return `${parsed.toFormat('yyyy-MM-dd')}`; 27 | } 28 | 29 | export class PullRequest implements Block { 30 | constructor(obj: Record) { 31 | Object.assign(this, obj); 32 | const state = obj.state ? obj.state : {}; 33 | this.state = state as typeof PullRequestState; 34 | } 35 | 36 | page: string; 37 | blockUUID: BlockUUID; 38 | state: typeof PullRequestState; 39 | 40 | content(): string { 41 | const prefix = this.state.state === 'OPEN' ? 'TODO' : 'DONE'; 42 | return `${prefix} [${this.state.title}](${this.state.url})`; 43 | } 44 | properties(): Record { 45 | return { 46 | repository: `[[github/${this.state.repository}]]`, 47 | state: this.state.state, 48 | created: `[[${formatDate(this.state.created)}]]`, 49 | updated: `[[${formatDate(this.state.updated)}]]`, 50 | ...(this.state.reviews && { 51 | reviews: this.state.reviews 52 | .split(' ') 53 | .map(uuid => { 54 | return `((${uuid}))`; 55 | }) 56 | .join('@@html:
@@'), 57 | }), 58 | }; 59 | } 60 | async read(blockEntity: BlockEntity | null): Promise { 61 | if (!blockEntity) { 62 | blockEntity = await logseq.Editor.getBlock(this.blockUUID); 63 | } 64 | Object.keys(PullRequestState).map(key => { 65 | if (!blockEntity?.properties[`.${key}`]) return; 66 | this.state[key] = blockEntity.properties[`.${key}`]; 67 | }); 68 | } 69 | } 70 | 71 | export async function fetchPullRequests( 72 | octokit: Octokit, 73 | username: string 74 | ): Promise { 75 | const ids: string[] = []; 76 | 77 | let page = await logseq.Editor.getPage('github/pull-requests'); 78 | if (page) { 79 | const blocks = await logseq.Editor.getPageBlocksTree('github/pull-requests'); 80 | for (const block of blocks) { 81 | const pr = new PullRequest({}); 82 | pr.read(block); 83 | if (pr.state.state !== 'OPEN') continue; 84 | ids.push(pr.state.id); 85 | } 86 | } 87 | 88 | const localPullRequests = await octokit.graphql( 89 | util.format(PRByIdQuery, ids.map(id => `"${id}"`).join(',')) 90 | ); 91 | const pullRequests = await octokit.graphql(util.format(PRQuery, username)); 92 | const reviewRequests = await octokit.graphql( 93 | util.format(PRReviewQuery, username) 94 | ); 95 | 96 | const nodes = [ 97 | ...localPullRequests.nodes, 98 | ...pullRequests.search.edges.map((edge: any) => edge.node), 99 | ...reviewRequests.search.edges.map((edge: any) => edge.node), 100 | ]; 101 | 102 | const result: Record = {}; 103 | for (const node of nodes) { 104 | if (node.id in result) continue; 105 | const blockUUID = v5(node.id, namespace); 106 | const pr = new PullRequest({ 107 | page: 'github/pull-requests', 108 | blockUUID: blockUUID, 109 | state: { 110 | id: node.id, 111 | title: node.title, 112 | repository: node.repository.nameWithOwner, 113 | state: node.state, 114 | url: node.url, 115 | created: node.createdAt, 116 | updated: node.updatedAt, 117 | }, 118 | }); 119 | 120 | const reviews = []; 121 | for (const review of node.latestReviews.nodes) { 122 | const reviewUUID = v5(review.id, namespace); 123 | const reviewBlock = new Review({ 124 | page: 'github/reviews', 125 | blockUUID: reviewUUID, 126 | state: { 127 | id: review.id, 128 | state: review.state, 129 | prState: node.state, 130 | login: review.author.login, 131 | created: node.createdAt, 132 | updated: node.updatedAt, 133 | }, 134 | }); 135 | reviews.push(reviewBlock); 136 | } 137 | 138 | for (const reviewRequest of node.reviewRequests.nodes) { 139 | const reviewUUID = v5(reviewRequest.id, namespace); 140 | const reviewBlock = new Review({ 141 | page: 'github/reviews', 142 | blockUUID: reviewUUID, 143 | state: { 144 | id: reviewRequest.id, 145 | state: 'REQUESTED', 146 | prState: node.state, 147 | login: reviewRequest.requestedReviewer.login, 148 | }, 149 | }); 150 | reviews.push(reviewBlock); 151 | } 152 | 153 | pr.state.reviews = reviews.map(review => review.blockUUID).join(' '); 154 | result[pr.state.id] = pr; 155 | reviews.map(review => (result[review.state.id] = review)); 156 | } 157 | 158 | return Object.values(result); 159 | } 160 | -------------------------------------------------------------------------------- /src/github/query.ts: -------------------------------------------------------------------------------- 1 | import util from 'util'; 2 | 3 | const latestReviews = ` 4 | latestReviews(first: 100) { 5 | nodes { 6 | id 7 | state 8 | author { 9 | login 10 | } 11 | } 12 | } 13 | `; 14 | const reviewRequests = ` 15 | reviewRequests(first: 100) { 16 | nodes { 17 | id 18 | requestedReviewer { 19 | ...on User { 20 | login 21 | } 22 | } 23 | } 24 | } 25 | `; 26 | 27 | const baseQuery = ` 28 | { 29 | search(query: "%s", type: ISSUE, first: 100) { 30 | edges { 31 | node { 32 | ... on PullRequest { 33 | repository { 34 | nameWithOwner 35 | } 36 | url 37 | title 38 | id 39 | state 40 | createdAt 41 | updatedAt 42 | ${latestReviews} 43 | ${reviewRequests} 44 | } 45 | } 46 | } 47 | } 48 | } 49 | `; 50 | export const PRQuery = util.format(baseQuery, 'type:pr state:open author:%s'); 51 | export const PRReviewQuery = util.format( 52 | baseQuery, 53 | 'state:open review-requested:%s' 54 | ); 55 | export const PRByIdQuery = ` 56 | { 57 | nodes(ids: [%s]) { 58 | ... on PullRequest { 59 | repository { 60 | nameWithOwner 61 | } 62 | url 63 | title 64 | id 65 | state 66 | createdAt 67 | updatedAt 68 | ${latestReviews} 69 | ${reviewRequests} 70 | } 71 | } 72 | } 73 | `; 74 | -------------------------------------------------------------------------------- /src/github/review.ts: -------------------------------------------------------------------------------- 1 | import {Block} from './logseq'; 2 | import {BlockUUID, BlockEntity} from '@logseq/libs/dist/LSPlugin.user'; 3 | 4 | export const ReviewState = { 5 | id: '', 6 | state: '', 7 | prState: '', 8 | login: '', 9 | created: '', 10 | updated: '', 11 | }; 12 | 13 | export class Review implements Block { 14 | constructor(obj: Record) { 15 | Object.assign(this, obj); 16 | const state = obj.state ? obj.state : {}; 17 | this.state = state as typeof ReviewState; 18 | } 19 | 20 | page: string; 21 | blockUUID: BlockUUID; 22 | state: typeof ReviewState; 23 | 24 | content(): string { 25 | let [prefix, suffix] = ['TODO ', '']; 26 | switch (this.state.state) { 27 | case 'APPROVED': 28 | prefix = 'DONE '; 29 | break; 30 | case 'CHANGES_REQUESTED': 31 | suffix = ' ⭕'; 32 | break; 33 | case 'REQUESTED': 34 | suffix = ' 🔸'; 35 | break; 36 | case 'COMMENTED': 37 | suffix = ' 💬'; 38 | break; 39 | } 40 | 41 | if (this.state.prState !== 'OPEN') { 42 | prefix = 'DONE '; 43 | } 44 | 45 | return `${prefix}[[github/${this.state.login}]]${suffix}`; 46 | } 47 | properties(): Record { 48 | return {}; 49 | } 50 | async read(blockEntity: BlockEntity | null): Promise { 51 | if (!blockEntity) { 52 | blockEntity = await logseq.Editor.getBlock(this.blockUUID); 53 | } 54 | Object.keys(ReviewState).map(key => { 55 | if (!blockEntity?.properties[`.${key}`]) return; 56 | this.state[key] = blockEntity.properties[`.${key}`]; 57 | }); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ics/ics.ts: -------------------------------------------------------------------------------- 1 | import {Block} from './logseq'; 2 | import {BlockUUID} from '@logseq/libs/dist/LSPlugin.user'; 3 | import axios from 'axios'; 4 | import ical from 'node-ical'; 5 | import {DateTime} from 'luxon'; 6 | import {v5} from 'uuid'; 7 | 8 | const namespace = 'dd13a47c-c074-4ef9-9676-66792035d4be'; 9 | 10 | export const EventState = { 11 | title: '', 12 | start: '', 13 | end: '', 14 | meeting: '', 15 | }; 16 | 17 | function formatDate(date: string) { 18 | const parsed = DateTime.fromISO(date); 19 | // Format date to 2023-04-13 09:00 20 | const time = parsed.toFormat('HH:mm'); 21 | return `[[${parsed.toFormat('yyyy-MM-dd')}]] ${time}`; 22 | } 23 | 24 | export class Event implements Block { 25 | constructor(obj: Record) { 26 | Object.assign(this, obj); 27 | const state = obj.state ? obj.state : {}; 28 | this.state = state as typeof EventState; 29 | } 30 | 31 | page: string; 32 | blockUUID: BlockUUID; 33 | state: typeof EventState; 34 | 35 | content(): string { 36 | return `${this.state.title}\n`; 37 | } 38 | properties(): Record { 39 | return { 40 | start: formatDate(this.state.start), 41 | end: formatDate(this.state.end), 42 | ...(this.state.meeting && {meeting: this.state.meeting}), 43 | }; 44 | } 45 | async read(blockEntity: BlockEntity | null): Promise { 46 | if (!blockEntity) { 47 | blockEntity = await logseq.Editor.getBlock(this.blockUUID); 48 | } 49 | Object.keys(EventState).map(key => { 50 | if (!blockEntity?.properties[`.${key}`]) return; 51 | this.state[key] = blockEntity.properties[`.${key}`]; 52 | }); 53 | } 54 | } 55 | 56 | export async function fetchEvents( 57 | name: string, 58 | url: string, 59 | renaming: Record 60 | ): Promise { 61 | const today = DateTime.local(); 62 | const response = await axios.get(url); 63 | const parsed = ical.parseICS(response.data); 64 | const events: Event[] = []; 65 | for (const key in parsed) { 66 | const event = parsed[key]; 67 | 68 | if (event.type !== 'VEVENT') continue; 69 | 70 | if (event.rrule) { 71 | if (DateTime.fromJSDate(event.rrule.options.until) < today) continue; 72 | } else { 73 | if (DateTime.fromJSDate(event.start) < today) continue; 74 | } 75 | 76 | let start = DateTime.fromJSDate(event.start); 77 | const duration = DateTime.fromJSDate(event.end).diff(start); 78 | if (event.rrule) { 79 | const rrule = event.rrule; 80 | const currentDate = event.rrule.after(today.toJSDate(), true); 81 | if (!currentDate) { 82 | continue; 83 | } 84 | // Get the timezone identifier from the rrule object 85 | const tzid = rrule.origOptions.tzid; 86 | 87 | // Get the original start date and offset from the rrule object 88 | const originalDate = new Date(rrule.origOptions.dtstart); 89 | const originalTzDate = DateTime.fromJSDate(originalDate, {zone: tzid}); 90 | const originalOffset = originalTzDate.offset; 91 | 92 | const currentTzDate = DateTime.fromJSDate(currentDate, {zone: tzid}); 93 | const currentOffset = currentTzDate.offset; 94 | 95 | // Calculate the difference between the current offset and the original offset 96 | const offsetDiff = currentOffset - originalOffset; 97 | 98 | // Adjust the start date by the offset difference to get the corrected start date 99 | currentDate.setHours(currentDate.getHours() - offsetDiff / 60); 100 | start = DateTime.fromJSDate(currentDate); 101 | } 102 | 103 | start = start.set({second: 0, millisecond: 0}); 104 | let end = start.plus(duration); 105 | end = end.set({second: 0, millisecond: 0}); 106 | 107 | const blockUUID = v5(event.uid, namespace); 108 | 109 | let title = event.summary.replace(/\//g, '|'); 110 | if (renaming[title]) { 111 | title = renaming[title]; 112 | } 113 | 114 | let meetingMatches = undefined; 115 | if (event.description) { 116 | meetingMatches = event.description.match( 117 | /(https:\/\/meet\.google\.com\/[\w-]+)/ 118 | ); 119 | } 120 | 121 | events.push( 122 | new Event({ 123 | page: `calendar/${name}`, 124 | blockUUID: blockUUID, 125 | state: { 126 | title: title, 127 | start: start.toISO(), 128 | end: end.toISO(), 129 | ...(meetingMatches && {meeting: meetingMatches[1]}), 130 | }, 131 | }) 132 | ); 133 | } 134 | return events; 135 | } 136 | -------------------------------------------------------------------------------- /src/logseq/logseq.ts: -------------------------------------------------------------------------------- 1 | import {BlockUUID, BlockEntity} from '@logseq/libs/dist/LSPlugin.user'; 2 | 3 | export interface Block { 4 | page: string; 5 | blockUUID: BlockUUID; 6 | state: Record; 7 | 8 | content(): string; 9 | properties(): Record; 10 | read(blockEntity: BlockEntity | null): Promise; 11 | } 12 | 13 | export async function write(blocks: Block[]) { 14 | const pages = [...new Set(blocks.map(b => b.page))]; 15 | 16 | const pageMap = {}; 17 | for (const page of pages) { 18 | let pageEntity = await logseq.Editor.getPage(page); 19 | if (!pageEntity) { 20 | pageEntity = await logseq.Editor.createPage( 21 | page, 22 | {}, 23 | { 24 | redirect: false, 25 | createFirstBlock: false, 26 | journal: false, 27 | } 28 | ); 29 | } 30 | pageMap[page] = pageEntity; 31 | } 32 | 33 | for (const block of blocks) { 34 | const properties = block.properties(); 35 | for (const key in block.state) { 36 | properties[`.${key}`] = block.state[key]; 37 | } 38 | 39 | const blockEntity = await logseq.Editor.getBlock(block.blockUUID); 40 | if (blockEntity) { 41 | await logseq.Editor.updateBlock(block.blockUUID, block.content(), { 42 | properties: properties, 43 | }); 44 | } else { 45 | const page = pageMap[block.page]; 46 | await logseq.Editor.insertBlock(page.uuid, block.content(), { 47 | customUUID: block.blockUUID, 48 | properties: properties, 49 | }); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/logseq/settings.ts: -------------------------------------------------------------------------------- 1 | import {SettingSchemaDesc} from '@logseq/libs/dist/LSPlugin.user'; 2 | 3 | export const settingsSchema: SettingSchemaDesc[] = [ 4 | { 5 | key: 'calendars', 6 | type: 'object', 7 | title: 'Calendars', 8 | description: 'Key value pairs of calendar name and ics url', 9 | default: {}, 10 | }, 11 | { 12 | key: 'renaming', 13 | type: 'object', 14 | title: 'Renaming', 15 | description: 16 | 'Key value pairs of calendar name and object mapping old to new event names', 17 | default: {}, 18 | }, 19 | { 20 | key: 'github-token', 21 | type: 'string', 22 | title: 'GitHub Token', 23 | description: 'GitHub API Token', 24 | default: '', 25 | }, 26 | { 27 | key: 'repository-blacklist', 28 | type: 'string', 29 | title: 'Repository Blacklist', 30 | description: 'Comma separated list of repository names to ignore', 31 | default: '', 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import '@logseq/libs'; 2 | import {settingsSchema} from './logseq/settings'; 3 | import {fetchEvents} from './ics/ics'; 4 | import {fetchPullRequests} from './github/pull_request'; 5 | import {write} from './logseq/logseq'; 6 | import {Octokit} from 'octokit'; 7 | 8 | async function main() { 9 | logseq.useSettingsSchema(settingsSchema); 10 | const calendars = logseq.settings['calendars']; 11 | const renaming = logseq.settings['renaming']; 12 | const githubToken = logseq.settings['github-token']; 13 | 14 | function createModel() { 15 | return { 16 | sync: async () => { 17 | for (const name in calendars) { 18 | const renames = renaming[name] ? renaming[name] : {}; 19 | const events = await fetchEvents(name, calendars[name], renames); 20 | await write(events); 21 | } 22 | 23 | if (githubToken) { 24 | const octokit = new Octokit({auth: githubToken}); 25 | const { 26 | data: {login}, 27 | } = await octokit.rest.users.getAuthenticated(); 28 | const pullRequests = await fetchPullRequests(octokit, login); 29 | 30 | let blacklist = logseq.settings['repository-blacklist'].split(','); 31 | let filtered = [] 32 | for (let pr of pullRequests) { 33 | if (!blacklist.includes(pr.state.repository)) { 34 | filtered.push(pr); 35 | } 36 | } 37 | 38 | await write(filtered); 39 | } 40 | }, 41 | }; 42 | } 43 | 44 | logseq.provideModel(createModel()); 45 | 46 | logseq.App.registerUIItem('toolbar', { 47 | key: 'logsync', 48 | template: ` 49 | 50 | 51 | 52 | `, 53 | }); 54 | } 55 | 56 | logseq.ready(main).catch(console.error); 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/tsconfig-google.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "build" 6 | }, 7 | "include": [ 8 | "src/**/*.ts", 9 | "test/**/*.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite'; 2 | 3 | // https://vitejs.dev/config/ 4 | export default defineConfig({ 5 | base: './', 6 | build: { 7 | outDir: 'build', 8 | sourcemap: true, 9 | }, 10 | }); 11 | --------------------------------------------------------------------------------