├── .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 | - 
7 | - Idempotent synchronization
8 | - Any number of calendars
9 | - Recurring events
10 | - Meeting links (google)
11 | - Event renaming
12 | - GitHub
13 | - 
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 |
--------------------------------------------------------------------------------