mirror of
https://github.com/heymoosh/ticktick-quick-add-obsidian.git
synced 2026-07-22 05:43:55 +00:00
TickTick's OAuth server requires byte-exact match between the redirect_uri the plugin sends and the value registered in the developer portal. Users who register the URI with a trailing slash (common on iOS paste, or by hand) get rejected with: error="invalid_grant" error_description="Invalid redirect: ...vercel.app does not match one of the registered values: [...vercel.app/]" (Reported in issue #2.) Normalize the redirect URI by stripping trailing slashes and whitespace at all four use/save sites: - main.ts: startAuthFlow() and exchangeAuthCodeForToken() clean the URI before sending — protects existing users with a slash already saved - settings.ts: both Redirect URI inputs strip on save so the persisted value matches what's displayed - settings.ts: both descriptions now warn against trailing slashes - README: new Troubleshooting entry for the invalid_grant error https://claude.ai/code/session_01V2fsiatACzjUCNwSEhwER9
10 lines
15 KiB
JavaScript
10 lines
15 KiB
JavaScript
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
*/
|
|
|
|
var m=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var _=Object.getOwnPropertyNames;var A=Object.prototype.hasOwnProperty;var U=(o,e)=>{for(var n in e)m(o,n,{get:e[n],enumerable:!0})},N=(o,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of _(e))!A.call(o,t)&&t!==n&&m(o,t,{get:()=>e[t],enumerable:!(i=I(e,t))||i.enumerable});return o};var D=o=>N(m({},"__esModule",{value:!0}),o);var M={};U(M,{default:()=>k});module.exports=D(M);var S=require("obsidian"),r=require("obsidian");var c=require("obsidian"),f={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,n){super(e,n),this.plugin=n}display(){let{containerEl:e}=this;e.empty(),this.plugin.settings.accessToken?this.renderConnectedView(e):this.renderSetupView(e)}renderSetupView(e){e.createEl("h2",{text:"Set up TickTick"}),e.createEl("p",{text:"Connect this plugin to your TickTick account in two steps. You only need to do this once."}),new c.Setting(e).setName("Step 1 \u2014 Enter your TickTick app credentials").setHeading();let n=e.createEl("p");n.appendText("Sign in to the ");let i=n.createEl("a",{text:"TickTick Developer Portal",href:"https://developer.ticktick.com/"});i.setAttr("target","_blank"),i.setAttr("rel","noopener"),n.appendText(", create an app, and copy its Client ID and Client Secret into the fields below. In the portal, set the app's Redirect URI to the value shown in the Redirect URI field."),new c.Setting(e).setName("Client ID").addText(t=>t.setPlaceholder("Your Client ID").setValue(this.plugin.settings.clientId||"").onChange(async s=>{this.plugin.settings.clientId=s.trim(),await this.plugin.saveSettings()})),new c.Setting(e).setName("Client Secret").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 s=>{this.plugin.settings.clientSecret=s.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(e).setName("Redirect URI").setDesc("Copy this exact value into your TickTick app's Redirect URI field in the developer portal. Do not add a trailing slash \u2014 the URIs must match exactly. Only change it here if you self-host the callback page.").addText(t=>t.setPlaceholder(f.redirectUri).setValue(this.plugin.settings.redirectUri||"").onChange(async s=>{this.plugin.settings.redirectUri=s.trim().replace(/\/+$/,""),await this.plugin.saveSettings()})),new c.Setting(e).setName("Step 2 \u2014 Authorize").setHeading(),e.createEl("p",{text:"Tap Connect, then tap the link in the popup to sign in to TickTick and approve access. The callback page will either return you to Obsidian automatically, or show you an authorization code \u2014 if you see a code, copy it and paste it in the field below."}),new c.Setting(e).setName("Connect to TickTick").addButton(t=>{t.setButtonText("Connect").setCta().onClick(async()=>{await this.plugin.startAuthFlow()})}),new c.Setting(e).setName("Authorization code").setDesc("Only needed if the callback page showed you a code instead of returning you to Obsidian automatically.").addText(t=>t.setPlaceholder("Paste authorization code").onChange(async s=>{s.trim()&&(await this.plugin.exchangeAuthCodeForToken(s.trim()),t.setValue(""),this.display())}))}renderConnectedView(e){e.createEl("h2",{text:"Connected to TickTick"}),e.createEl("p",{text:'Use the "Create TickTick task" command from any note to send the current line or paragraph to TickTick.'}),new c.Setting(e).setName("Plugin behavior").setHeading(),new c.Setting(e).setName("Selection mode").setDesc("Capture 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 s=>{this.plugin.settings.selectionMode=s,await this.plugin.saveSettings()})),new c.Setting(e).setName("Tag position").setDesc("Where the #ticktick tag is added relative to the text.").addDropdown(t=>t.addOption("append","Append (end)").addOption("prepend","Prepend (beginning)").setValue(this.plugin.settings.tagPosition).onChange(async s=>{this.plugin.settings.tagPosition=s,await this.plugin.saveSettings()}));let n=e.createEl("details",{cls:"ticktick-advanced"});n.style.marginTop="2em";let i=n.createEl("summary",{text:"Advanced \u2014 Connection details"});i.style.cursor="pointer",i.style.fontWeight="600",i.style.padding="0.5em 0",new c.Setting(n).setName("Reconnect").setDesc("Re-run the OAuth flow. Use if your token can no longer be refreshed.").addButton(t=>{t.setButtonText("Reconnect").onClick(async()=>{await this.plugin.startAuthFlow()})}),new c.Setting(n).setName("Authorization code").setDesc("Manual fallback if reconnecting did not return you to Obsidian automatically.").addText(t=>t.setPlaceholder("Paste authorization code").onChange(async s=>{s.trim()&&(await this.plugin.exchangeAuthCodeForToken(s.trim()),t.setValue(""),this.display())})),new c.Setting(n).setName("Disconnect").setDesc("Clear stored tokens. You will need to authorize again to create tasks.").addButton(t=>{t.setButtonText("Disconnect").setWarning().onClick(async()=>{this.plugin.settings.accessToken="",this.plugin.settings.refreshToken=void 0,this.plugin.settings.tokenExpiry=void 0,await this.plugin.saveSettings(),new c.Notice("Disconnected from TickTick."),this.display()})}),new c.Setting(n).setName("Client ID").addText(t=>t.setValue(this.plugin.settings.clientId||"").onChange(async s=>{this.plugin.settings.clientId=s.trim(),await this.plugin.saveSettings()})),new c.Setting(n).setName("Client Secret").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 s=>{this.plugin.settings.clientSecret=s.trim(),await this.plugin.saveSettings()}),t)),new c.Setting(n).setName("Redirect URI").setDesc("Must match the value registered in your TickTick app exactly, with no trailing slash.").addText(t=>t.setValue(this.plugin.settings.redirectUri||"").onChange(async s=>{this.plugin.settings.redirectUri=s.trim().replace(/\/+$/,""),await this.plugin.saveSettings()})),new c.Setting(n).setName("Access token").setDesc("Stored access token (read-only).").addText(t=>{t.inputEl.readOnly=!0,t.setValue(this.plugin.settings.accessToken||"")}),new c.Setting(n).setName("Refresh token").setDesc("Stored refresh token (read-only).").addText(t=>{t.inputEl.readOnly=!0,t.setValue(this.plugin.settings.refreshToken||"")})}};function y(o){let e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",n="";for(let i=0;i<o;i++)n+=e.charAt(Math.floor(Math.random()*e.length));return n}function R(o,e){return{text:o.getLine(e),line:e}}function V(o,e){let n=e,i=e,t=o.lineCount();for(;n>0&&o.getLine(n-1).trim()!=="";)n--;for(;i<t-1&&o.getLine(i+1).trim()!=="";)i++;let s="";for(let a=n;a<=i;a++)s+=o.getLine(a)+`
|
|
`;return{text:s.trim(),start:n,end:i}}function O(o){return o.replace(/!\[([^\]]*)\]\([^)]*\)/g,"$1").replace(/\[([^\]]+)\]\([^)]*\)/g,"$1").replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g,"$2").replace(/\[\[([^\]]+)\]\]/g,"$1").replace(/^\s*(?:>\s*|#{1,6}\s+|[-*+]\s+\[[ xX]\]\s+|[-*+]\s+|\d+\.\s+)+/,"").replace(/__([^_]+)__/g,"$1").replace(/\*\*?|~~|==|`/g,"").replace(/\s+/g," ").trim()}async function $(){let o=y(64),n=new TextEncoder().encode(o),i=await crypto.subtle.digest("SHA-256",n),t=(0,S.arrayBufferToBase64)(i).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,"");return{codeVerifier:o,codeChallenge:t}}var x=class extends r.Modal{constructor(n,i){super(n);this.authUrl=i}onOpen(){let{contentEl:n}=this;n.empty(),n.createEl("h2",{text:"Authorize TickTick"}),n.createEl("p",{text:"Tap the link below to open the TickTick login page. After you approve access, you will be returned to Obsidian automatically."});let i=n.createEl("a",{text:"Open TickTick login",href:this.authUrl});i.setAttr("target","_blank"),i.setAttr("rel","noopener"),i.style.display="inline-block",i.style.marginTop="0.5em",i.style.fontWeight="bold"}onClose(){this.contentEl.empty()}};function w(o,e,n){console.log(JSON.stringify({timestamp:new Date().toISOString(),plugin:"ticktick-integration",action:e,...n,clientId:o.settings.clientId?"***":void 0,accessToken:o.settings.accessToken?"***":void 0}))}var k=class extends r.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 r.Notice("TickTick auth failed: no authorization code returned.");return}if(!this.settings.tempState||e.state!==this.settings.tempState){new r.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",icon:"lucide-circle-check-big",editorCallback:(e,n)=>this.createTaskFromEditor(e,n)}),this.registerEvent(this.app.workspace.on("file-menu",(e,n)=>{if(!(n instanceof r.TFile)||n.extension!=="md")return;let i=this.app.workspace.getActiveViewOfType(r.MarkdownView);!i||!i.editor||i.file!==n||e.addItem(t=>{t.setTitle("Create TickTick task").setIcon("lucide-circle-check-big").onClick(async()=>{await this.createTaskFromEditor(i.editor,i)})})}))}async createTaskFromEditor(e,n){let i=e.getCursor();if(!n.file){new r.Notice("No file found for the current view!");return}let t,s,a;if(this.settings.selectionMode==="paragraph"){let{text:h,start:g,end:E}=V(e,i.line);t=h,s=g,a=E}else{let{text:h,line:g}=R(e,i.line);t=h,s=g,a=g}if(!t.trim()){new r.Notice("No text found on the current line!");return}let d=y(8),l=this.settings.tagPosition==="prepend"?`#ticktick ${t} ^${d}`:`${t} #ticktick ^${d}`;e.replaceRange(l,{line:s,ch:0},{line:a,ch:e.getLine(a).length});let p=this.app.vault.getName(),C=n.file.path,b=`obsidian://advanced-uri?vault=${encodeURIComponent(p)}&filepath=${encodeURIComponent(C)}&block=${encodeURIComponent(d)}`,v=`${t}
|
|
|
|
[Open in Obsidian](${b})`,T=O(t),P=T.length>50?T.substring(0,50)+"...":T;await this.ensureFreshToken();try{(await this.createTicktickTask(P,v)).success?new r.Notice("TickTick task created successfully!"):new r.Notice("Failed to create TickTick task.")}catch(h){w(this,"task_creation_failed",{error:h.message}),new r.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 r.Notice("Please enter your Client ID in the settings.");return}let n=(this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").trim().replace(/\/+$/,""),i=encodeURIComponent("tasks:read tasks:write"),{codeVerifier:t,codeChallenge:s}=await $(),a=y(32);this.settings.tempCodeVerifier=t,this.settings.tempState=a,await this.saveSettings();let d=`https://ticktick.com/oauth/authorize?client_id=${e}&redirect_uri=${encodeURIComponent(n)}&response_type=code&scope=${i}&code_challenge=${s}&code_challenge_method=S256&state=${a}`;new x(this.app,d).open()}async exchangeAuthCodeForToken(e){let n="https://ticktick.com/oauth/token",i=(this.settings.redirectUri||"https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app").trim().replace(/\/+$/,""),t=this.settings.clientId,s=this.settings.clientSecret,a=this.settings.tempCodeVerifier;if(!a||!t||!s){new r.Notice("Missing required credentials. Please update your settings.");return}let d=new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:i,client_id:t,client_secret:s,code_verifier:a});try{let l=await(0,r.requestUrl)({url:n,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 p=l.json;p.refresh_token?this.settings.refreshToken=p.refresh_token:(console.warn("Refresh token not received during token exchange."),new r.Notice("Refresh token not received. Re-authentication may be required more often.")),this.settings.accessToken=p.access_token,this.settings.tokenExpiry=Date.now()+p.expires_in*1e3*.85,this.settings.tempCodeVerifier=void 0,this.settings.tempState=void 0,await this.saveSettings(),new r.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 r.Notice("Failed to obtain access token.")}catch(l){console.error("Error during token exchange:",l),new r.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",n=this.settings.clientId,i=this.settings.clientSecret,t=new URLSearchParams({grant_type:"refresh_token",refresh_token:this.settings.refreshToken,client_id:n,client_secret:i});try{let s=await(0,r.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:t.toString()});if(s.status===200){let a=s.json;a.refresh_token&&(this.settings.refreshToken=a.refresh_token),this.settings.accessToken=a.access_token,this.settings.tokenExpiry=Date.now()+a.expires_in*1e3*.85,await this.saveSettings(),new r.Notice("Access token refreshed successfully!")}else throw console.error("Refresh token error:",s.text),new Error("Failed to refresh token")}catch(s){throw console.error("Error refreshing access token:",s),s}}async ensureFreshToken(){if(this.settings.tokenExpiry&&Date.now()>this.settings.tokenExpiry){w(this,"token_expired",{tokenExpiry:this.settings.tokenExpiry});try{await this.refreshAccessToken()}catch(e){throw new r.Notice("Reauthentication required. Please reconnect to TickTick."),e}}}async createTicktickTask(e,n){let i=this.settings.accessToken;if(!i)return new r.Notice("No access token found. Please connect to TickTick in the settings."),{success:!1};let t="https://api.ticktick.com/open/v1/task",s={title:e,content:n};try{w(this,"task_creation_request",{endpoint:t,title:e});let a=await(0,r.requestUrl)({url:t,method:"POST",headers:{Authorization:`Bearer ${i}`,"Content-Type":"application/json","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)"},body:JSON.stringify(s)});return a.status===200?{success:!0}:(console.error("TickTick API error (status):",a.status),console.log("response.json:",a.json),console.log("response.text:",a.text),{success:!1})}catch(a){return console.error("TickTick API error:",a),{success:!1}}}async loadSettings(){this.settings=Object.assign({},f,await this.loadData())}async saveSettings(){await this.saveData(this.settings)}};
|