Enable mobile compatibility with seamless OAuth deep-link

- Flip isDesktopOnly to false in manifest.json; the runtime code uses
  only platform-safe APIs (requestUrl, crypto.subtle, URLSearchParams).
- Register obsidian://ticktick-callback protocol handler so the OAuth
  redirect returns directly into Obsidian on desktop and mobile,
  removing the manual copy-paste step.
- Verify state parameter against tempState to mitigate CSRF, and clear
  tempCodeVerifier/tempState after a successful exchange.
- Update Vercel callback pages (callback.html, index.html) to redirect
  to the obsidian:// deep link instead of displaying the code as text.
  Note: the deployed Vercel page must be redeployed for users to see
  the new behavior.
- Drop the now-vestigial "Authorization code" paste field from the
  settings UI.
This commit is contained in:
Claude 2026-05-10 23:41:32 +00:00
parent 3fae30c62a
commit 23eba63ff7
No known key found for this signature in database
7 changed files with 54 additions and 37 deletions

View file

@ -7,19 +7,29 @@
<body>
<h1>Authorization Complete</h1>
<p id="message">Processing...</p>
<p id="fallback" style="display:none">
If Obsidian did not open automatically,
<a id="deeplink" href="#">tap here to return to Obsidian</a>.
</p>
<script>
// Parse query parameters from the URL
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const messageEl = document.getElementById('message');
if (code && state) {
document.getElementById('message').innerText =
`Authorization successful! Your code is: ${code}. Please copy this code and paste it into Obsidian.`;
const target = 'obsidian://ticktick-callback'
+ '?code=' + encodeURIComponent(code)
+ '&state=' + encodeURIComponent(state);
messageEl.innerText = 'Returning you to Obsidian…';
const fallback = document.getElementById('fallback');
const link = document.getElementById('deeplink');
link.href = target;
fallback.style.display = 'block';
window.location.replace(target);
} else {
document.getElementById('message').innerText =
'Authorization failed or was canceled. Please try again.';
messageEl.innerText = 'Authorization failed or was canceled. Please try again.';
}
</script>
</body>
</html>
</html>

View file

@ -7,19 +7,29 @@
<body>
<h1>Authorization Complete</h1>
<p id="message">Processing...</p>
<p id="fallback" style="display:none">
If Obsidian did not open automatically,
<a id="deeplink" href="#">tap here to return to Obsidian</a>.
</p>
<script>
// Parse query parameters from the URL
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const messageEl = document.getElementById('message');
if (code && state) {
document.getElementById('message').innerText =
`Authorization successful!\nYour code is: ${code}\nPlease copy this code and paste it into Obsidian.`;
const target = 'obsidian://ticktick-callback'
+ '?code=' + encodeURIComponent(code)
+ '&state=' + encodeURIComponent(state);
messageEl.innerText = 'Returning you to Obsidian…';
const fallback = document.getElementById('fallback');
const link = document.getElementById('deeplink');
link.href = target;
fallback.style.display = 'block';
window.location.replace(target);
} else {
document.getElementById('message').innerText =
'Authorization failed or was canceled. Please try again.';
messageEl.innerText = 'Authorization failed or was canceled. Please try again.';
}
</script>
</body>
</html>
</html>

10
main.js
View file

@ -3,10 +3,8 @@ THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
var f=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var E=Object.getOwnPropertyNames;var _=Object.prototype.hasOwnProperty;var U=(r,e)=>{for(var t in e)f(r,t,{get:e[t],enumerable:!0})},A=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of E(e))!_.call(r,s)&&s!==t&&f(r,s,{get:()=>e[s],enumerable:!(n=v(e,s))||n.enumerable});return r};var I=r=>A(f({},"__esModule",{value:!0}),r);var j={};U(j,{default:()=>k});module.exports=I(j);var y=require("obsidian"),o=require("obsidian");var c=require("obsidian"),w={accessToken:"",clientId:"",clientSecret:"",redirectUri:"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",selectionMode:"line",tagPosition:"append"},u=class extends c.PluginSettingTab{constructor(e,t){super(e,t),this.plugin=t}display(){let{containerEl:e}=this;e.empty(),new c.Setting(e).setName("Client id").setDesc("Enter your TickTick Client ID from the Developer Console.").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async n=>{this.plugin.settings.clientId=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client secret").setDesc("Enter your TickTick Client Secret (input is masked).").addText(t=>(t.inputEl.type="password",t.setPlaceholder("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022").setValue(this.plugin.settings.clientSecret||"").onChange(async n=>{this.plugin.settings.clientSecret=n.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Enter the Redirect URI registered in your TickTick Developer Portal. (Default: https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app)").addText(t=>t.setPlaceholder("https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").setValue(this.plugin.settings.redirectUri||"").onChange(async n=>{this.plugin.settings.redirectUri=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Selection mode").setDesc("Choose whether the command captures only the current line or the entire paragraph.").addDropdown(t=>t.addOption("line","Current line").addOption("paragraph","Entire paragraph").setValue(this.plugin.settings.selectionMode).onChange(async n=>{this.plugin.settings.selectionMode=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Choose whether the #ticktick tag is added to the beginning or end of the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async n=>{this.plugin.settings.tagPosition=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Access token").setDesc("Your current access token (read-only).").addText(t=>t.setPlaceholder("Access token will appear here").setValue(this.plugin.settings.accessToken||"").onChange(async n=>{this.plugin.settings.accessToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Refresh token").setDesc("Stored refresh token (for debugging purposes).").addText(t=>t.setPlaceholder("Refresh token").setValue(this.plugin.settings.refreshToken||"").onChange(async n=>{this.plugin.settings.refreshToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Connect to TickTick").setDesc("Click to open the OAuth authorization URL in your browser.").addButton(async t=>{t.setButtonText("Connect").onClick(async()=>{await this.plugin.startAuthFlow()})}),new c.Setting(e).setName("Authorization code").setDesc("Paste the authorization code from TickTick here to obtain an access token.").addText(t=>t.setPlaceholder("Enter authorization code").onChange(async n=>{n.trim()&&(await this.plugin.exchangeAuthCodeForToken(n.trim()),t.setValue(""),this.display())}))}};function m(r){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t="";for(let n=0;n<r;n++)t+=e.charAt(Math.floor(Math.random()*e.length));return t}function D(r,e){return{text:r.getLine(e),line:e}}function N(r,e){let t=e,n=e,s=r.lineCount();for(;t>0&&r.getLine(t-1).trim()!=="";)t--;for(;n<s-1&&r.getLine(n+1).trim()!=="";)n++;let a="";for(let i=t;i<=n;i++)a+=r.getLine(i)+`
`;return{text:a.trim(),start:t,end:n}}async function R(){let r=m(64),t=new TextEncoder().encode(r),n=await crypto.subtle.digest("SHA-256",t),s=(0,y.arrayBufferToBase64)(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return{codeVerifier:r,codeChallenge:s}}function T(r,e,t){console.log(JSON.stringify({timestamp:new Date().toISOString(),plugin:"ticktick-integration",action:e,...t,clientId:r.settings.clientId?"***":void 0,accessToken:r.settings.accessToken?"***":void 0}))}var k=class extends o.Plugin{async onload(){console.log("Loading TickTick Plugin"),await this.loadSettings(),this.addSettingTab(new u(this.app,this)),this.addCommand({id:"create-ticktick-task",name:"Create TickTick task",editorCallback:async(e,t)=>{let n=e.getCursor();if(!t.file){new o.Notice("No file found for the current view!");return}let s,a,i;if(this.settings.selectionMode==="paragraph"){let{text:h,start:p,end:P}=N(e,n.line);s=h,a=p,i=P}else{let{text:h,line:p}=D(e,n.line);s=h,a=p,i=p}if(!s.trim()){new o.Notice("No text found on the current line!");return}let d=m(8),l=this.settings.tagPosition==="prepend"?`#ticktick ${s} ^${d}`:`${s} #ticktick ^${d}`;e.replaceRange(l,{line:a,ch:0},{line:i,ch:e.getLine(i).length});let g=this.app.vault.getName(),S=t.file.path,x=`obsidian://advanced-uri?vault=${encodeURIComponent(g)}&filepath=${encodeURIComponent(S)}&block=${encodeURIComponent(d)}`,C=`${s}
var f=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var E=Object.prototype.hasOwnProperty;var A=(r,e)=>{for(var t in e)f(r,t,{get:e[t],enumerable:!0})},I=(r,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of _(e))!E.call(r,s)&&s!==t&&f(r,s,{get:()=>e[s],enumerable:!(n=v(e,s))||n.enumerable});return r};var U=r=>I(f({},"__esModule",{value:!0}),r);var j={};A(j,{default:()=>k});module.exports=U(j);var y=require("obsidian"),o=require("obsidian");var c=require("obsidian"),w={accessToken:"",clientId:"",clientSecret:"",redirectUri:"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",selectionMode:"line",tagPosition:"append"},u=class extends c.PluginSettingTab{constructor(e,t){super(e,t),this.plugin=t}display(){let{containerEl:e}=this;e.empty(),new c.Setting(e).setName("Client id").setDesc("Enter your TickTick Client ID from the Developer Console.").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async n=>{this.plugin.settings.clientId=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client secret").setDesc("Enter your TickTick Client Secret (input is masked).").addText(t=>(t.inputEl.type="password",t.setPlaceholder("\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022").setValue(this.plugin.settings.clientSecret||"").onChange(async n=>{this.plugin.settings.clientSecret=n.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Enter the Redirect URI registered in your TickTick Developer Portal. (Default: https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app)").addText(t=>t.setPlaceholder("https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").setValue(this.plugin.settings.redirectUri||"").onChange(async n=>{this.plugin.settings.redirectUri=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Selection mode").setDesc("Choose whether the command captures only the current line or the entire paragraph.").addDropdown(t=>t.addOption("line","Current line").addOption("paragraph","Entire paragraph").setValue(this.plugin.settings.selectionMode).onChange(async n=>{this.plugin.settings.selectionMode=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Choose whether the #ticktick tag is added to the beginning or end of the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async n=>{this.plugin.settings.tagPosition=n,await this.plugin.saveSettings()})),new c.Setting(e).setName("Access token").setDesc("Your current access token (read-only).").addText(t=>t.setPlaceholder("Access token will appear here").setValue(this.plugin.settings.accessToken||"").onChange(async n=>{this.plugin.settings.accessToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Refresh token").setDesc("Stored refresh token (for debugging purposes).").addText(t=>t.setPlaceholder("Refresh token").setValue(this.plugin.settings.refreshToken||"").onChange(async n=>{this.plugin.settings.refreshToken=n.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Connect to TickTick").setDesc("Opens the OAuth authorization page in your browser. After approving, you will be returned to Obsidian automatically.").addButton(async t=>{t.setButtonText("Connect").onClick(async()=>{await this.plugin.startAuthFlow()})})}};function m(r){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",t="";for(let n=0;n<r;n++)t+=e.charAt(Math.floor(Math.random()*e.length));return t}function D(r,e){return{text:r.getLine(e),line:e}}function N(r,e){let t=e,n=e,s=r.lineCount();for(;t>0&&r.getLine(t-1).trim()!=="";)t--;for(;n<s-1&&r.getLine(n+1).trim()!=="";)n++;let a="";for(let i=t;i<=n;i++)a+=r.getLine(i)+`
`;return{text:a.trim(),start:t,end:n}}async function R(){let r=m(64),t=new TextEncoder().encode(r),n=await crypto.subtle.digest("SHA-256",t),s=(0,y.arrayBufferToBase64)(n).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return{codeVerifier:r,codeChallenge:s}}function T(r,e,t){console.log(JSON.stringify({timestamp:new Date().toISOString(),plugin:"ticktick-integration",action:e,...t,clientId:r.settings.clientId?"***":void 0,accessToken:r.settings.accessToken?"***":void 0}))}var k=class extends o.Plugin{async onload(){console.log("Loading TickTick Plugin"),await this.loadSettings(),this.addSettingTab(new u(this.app,this)),this.registerObsidianProtocolHandler("ticktick-callback",async e=>{if(!e.code){new o.Notice("TickTick auth failed: no authorization code returned.");return}if(!this.settings.tempState||e.state!==this.settings.tempState){new o.Notice("TickTick auth failed: state mismatch. Please try connecting again.");return}await this.exchangeAuthCodeForToken(e.code)}),this.addCommand({id:"create-ticktick-task",name:"Create TickTick task",editorCallback:async(e,t)=>{let n=e.getCursor();if(!t.file){new o.Notice("No file found for the current view!");return}let s,a,i;if(this.settings.selectionMode==="paragraph"){let{text:g,start:p,end:P}=N(e,n.line);s=g,a=p,i=P}else{let{text:g,line:p}=D(e,n.line);s=g,a=p,i=p}if(!s.trim()){new o.Notice("No text found on the current line!");return}let d=m(8),l=this.settings.tagPosition==="prepend"?`#ticktick ${s} ^${d}`:`${s} #ticktick ^${d}`;e.replaceRange(l,{line:a,ch:0},{line:i,ch:e.getLine(i).length});let h=this.app.vault.getName(),S=t.file.path,b=`obsidian://advanced-uri?vault=${encodeURIComponent(h)}&filepath=${encodeURIComponent(S)}&block=${encodeURIComponent(d)}`,x=`${s}
[Open in Obsidian](${x})`,b=s.length>50?s.substring(0,50)+"...":s;await this.ensureFreshToken();try{(await this.createTicktickTask(b,C)).success?new o.Notice("TickTick task created successfully!"):new o.Notice("Failed to create TickTick task.")}catch(h){T(this,"task_creation_failed",{error:h.message}),new o.Notice(`Failed to create task: ${h.message}
Check console for details.`)}}})}onunload(){console.log("Unloading TickTick Plugin")}async startAuthFlow(){let e=this.settings.clientId;if(!e){new o.Notice("Please enter your Client ID in the settings.");return}let t=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",n=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:s,codeChallenge:a}=await R(),i=m(32);this.settings.tempCodeVerifier=s,this.settings.tempState=i,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(t)}&response_type=code&scope=${n}&code_challenge=${a}&code_challenge_method=S256&state=${i}`;window.open(d,"_blank"),new o.Notice("OAuth flow initiated. Please complete it in your browser.")}async exchangeAuthCodeForToken(e){let t="https://ticktick.com/oauth/token",n=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",s=this.settings.clientId,a=this.settings.clientSecret,i=this.settings.tempCodeVerifier;if(!i||!s||!a){new o.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:n,client_id:s,client_secret:a,code_verifier:i});try{let l=await(0,o.requestUrl)({url:t,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:d.toString()});if(l.status===200){let g=l.json;g.refresh_token?this.settings.refreshToken=g.refresh_token:(console.warn("Refresh token not received during token exchange."),new o.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=g.access_token,this.settings.tokenExpiry=Date.now()+g.expires_in*1e3*.85,await this.saveSettings(),new o.Notice("TickTick access token obtained successfully!")}else console.error("Token exchange error (status):",l.status),console.log("response.json:",l.json),console.log("response.text:",l.text),new o.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new o.Notice("Error during token exchange.")}}async refreshAccessToken(){if(!this.settings.refreshToken)throw new Error("No refresh token available. Please reconnect.");let e="https://ticktick.com/oauth/token",t=this.settings.clientId,n=this.settings.clientSecret,s=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:t,client_secret:n});try{let a=await(0,o.requestUrl)({url:e,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:s.toString()});if(a.status===200){let i=a.json;i.refresh_token&&(this.settings.refreshToken=i.refresh_token),this.settings.accessToken=i.access_token,this.settings.tokenExpiry=Date.now()+i.expires_in*1e3*.85,await this.saveSettings(),new o.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",a.text),new Error("Failed to refresh token")}catch(a){throw console.error("Error refreshing access token:",a),a}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){T(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new o.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,t){let n=this.settings.accessToken;if(!n)return new o.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let s="https://api.ticktick.com/open/v1/task",a={title:e,content:t};try{T(this,"task_creation_request",{endpoint:s,title:e});let i=await(0,o.requestUrl)({url:s,method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(a)});return i.status===200?{success:!0}:(console.error("TickTick API error (status):",i.status),console.log("response.json:",i.json),console.log("response.text:",i.text),{success:!1})}catch(i){return console.error("TickTick API error:",i),{success:!1}}}async loadSettings(){this.settings=Object.assign({},w,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};
/* nosourcemap */
[Open in Obsidian](${b})`,C=s.length>50?s.substring(0,50)+"...":s;await this.ensureFreshToken();try{(await this.createTicktickTask(C,x)).success?new o.Notice("TickTick task created successfully!"):new o.Notice("Failed to create TickTick task.")}catch(g){T(this,"task_creation_failed",{error:g.message}),new o.Notice(`Failed to create task: ${g.message}
Check console for details.`)}}})}onunload(){console.log("Unloading TickTick Plugin")}async startAuthFlow(){let e=this.settings.clientId;if(!e){new o.Notice("Please enter your Client ID in the settings.");return}let t=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",n=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:s,codeChallenge:a}=await R(),i=m(32);this.settings.tempCodeVerifier=s,this.settings.tempState=i,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(t)}&response_type=code&scope=${n}&code_challenge=${a}&code_challenge_method=S256&state=${i}`;window.open(d,"_blank"),new o.Notice("OAuth flow initiated. Please complete it in your browser.")}async exchangeAuthCodeForToken(e){let t="https://ticktick.com/oauth/token",n=this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app",s=this.settings.clientId,a=this.settings.clientSecret,i=this.settings.tempCodeVerifier;if(!i||!s||!a){new o.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:n,client_id:s,client_secret:a,code_verifier:i});try{let l=await(0,o.requestUrl)({url:t,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:d.toString()});if(l.status===200){let h=l.json;h.refresh_token?this.settings.refreshToken=h.refresh_token:(console.warn("Refresh token not received during token exchange."),new o.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=h.access_token,this.settings.tokenExpiry=Date.now()+h.expires_in*1e3*.85,this.settings.tempCodeVerifier=void 0,this.settings.tempState=void 0,await this.saveSettings(),new o.Notice("TickTick access token obtained successfully!")}else console.error("Token exchange error (status):",l.status),console.log("response.json:",l.json),console.log("response.text:",l.text),new o.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new o.Notice("Error during token exchange.")}}async refreshAccessToken(){if(!this.settings.refreshToken)throw new Error("No refresh token available. Please reconnect.");let e="https://ticktick.com/oauth/token",t=this.settings.clientId,n=this.settings.clientSecret,s=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:t,client_secret:n});try{let a=await(0,o.requestUrl)({url:e,method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:s.toString()});if(a.status===200){let i=a.json;i.refresh_token&&(this.settings.refreshToken=i.refresh_token),this.settings.accessToken=i.access_token,this.settings.tokenExpiry=Date.now()+i.expires_in*1e3*.85,await this.saveSettings(),new o.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",a.text),new Error("Failed to refresh token")}catch(a){throw console.error("Error refreshing access token:",a),a}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){T(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new o.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,t){let n=this.settings.accessToken;if(!n)return new o.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let s="https://api.ticktick.com/open/v1/task",a={title:e,content:t};try{T(this,"task_creation_request",{endpoint:s,title:e});let i=await(0,o.requestUrl)({url:s,method:"POST",headers:{Authorization:`Bearer ${n}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(a)});return i.status===200?{success:!0}:(console.error("TickTick API error (status):",i.status),console.log("response.json:",i.json),console.log("response.text:",i.text),{success:!1})}catch(i){return console.error("TickTick API error:",i),{success:!1}}}async loadSettings(){this.settings=Object.assign({},w,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};

14
main.ts
View file

@ -83,6 +83,18 @@ export default class TickTickPlugin extends Plugin {
await this.loadSettings();
this.addSettingTab(new TickTickSettingTab(this.app, this));
this.registerObsidianProtocolHandler('ticktick-callback', async (params) => {
if (!params.code) {
new Notice('TickTick auth failed: no authorization code returned.');
return;
}
if (!this.settings.tempState || params.state !== this.settings.tempState) {
new Notice('TickTick auth failed: state mismatch. Please try connecting again.');
return;
}
await this.exchangeAuthCodeForToken(params.code);
});
// Command: Create TickTick task
this.addCommand({
id: 'create-ticktick-task',
@ -218,6 +230,8 @@ export default class TickTickPlugin extends Plugin {
this.settings.accessToken = data.access_token;
this.settings.tokenExpiry = Date.now() + (data.expires_in * 1000 * 0.85);
this.settings.tempCodeVerifier = undefined;
this.settings.tempState = undefined;
await this.saveSettings();
new Notice('TickTick access token obtained successfully!');
} else {

View file

@ -6,6 +6,6 @@
"description": "Create TickTick tasks from text blocks with automatic Obsidian URI links. Requires Advanced URI plugin.",
"author": "Muxin Li",
"authorUrl": "https://github.com/heymoosh",
"isDesktopOnly": true,
"isDesktopOnly": false,
"main": "main.js"
}

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"version": "1.1.0",
"license": "MIT",
"dependencies": {
"express": "^4.21.2"

View file

@ -131,26 +131,11 @@ export class TickTickSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Connect to TickTick')
.setDesc('Click to open the OAuth authorization URL in your browser.')
.setDesc('Opens the OAuth authorization page in your browser. After approving, you will be returned to Obsidian automatically.')
.addButton(async (button) => {
button.setButtonText('Connect').onClick(async () => {
await this.plugin.startAuthFlow();
});
});
new Setting(containerEl)
.setName('Authorization code')
.setDesc('Paste the authorization code from TickTick here to obtain an access token.')
.addText(text =>
text
.setPlaceholder('Enter authorization code')
.onChange(async (code) => {
if (code.trim()) {
await this.plugin.exchangeAuthCodeForToken(code.trim());
text.setValue('');
this.display();
}
})
);
}
}