滴答清单接口使用

我预备使用这种方式去规划和完成我每天的任务:

  1. 在昨天晚上或今天早上,去在滴答清单上安排任务(这一步或许也给它用脚本自动化)
  2. 从滴答清单上拉取任务信息,打印下来
  3. 然后今天晚上或明天早上,把任务完成情况落实到滴答清单上,继续安排任务

关于安排任务,要参考这阶段,这周的计划,以及参考之前的每天的执行情况。但这是后话。


这里必须要抽象出来滴答清单的接口,这里也记一下滴答清单的 Token 的获取方式。

滴答清单的文档见 https://developer.dida365.com/docs#/openapi

关于 Token 获取

滴答清单的 Token 的有效期是六个月,所以这里直接手动获取它,等它过期后再来一次即可。滴答清单走的是 OAuth2 协议,所以还比较麻烦。本想写一下 OAuth2 协议的逻辑和设计思路的,但算了,我该换角色了

首先,作为开发者,需要创建一个“App”(OAuth2 协议中的,不是真的应用,在文档界面右上角去操作),去记录 client_id 和 client_secret,这里把 redirect url 设置成 https://localhost

然后,用浏览器访问这整个链接,其中替换相应内容:

<https://dida365.com/oauth/authorize?scope=tasks:write tasks:read&client_id=<你的 clientId>&state=hellohello&redirect_uri=https://localhost&response_type=code>

访问链接后按提示操作,成功后会跳转到形如 https://localhost/?code=57SyIH&state=hellohello这样的链接,记录这里的 code 57SyIH

然后,执行下面的 curl:

1
2
3
4
5
6
curl --location --request POST 'https://dida365.com/oauth/token' \
-u '<你的 client_id>:<你的 client_key>' \
--data-urlencode 'code=<上面得到的 code>' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'scope=tasks:write tasks:read' \
--data-urlencode 'redirect_uri=https://localhost'

结果是个 JSON,中间的access_token字段即 TOKEN,后续使用时直接塞到 Authorization 字段中,前缀 Bearer。

这个执行似乎偶尔会失败……加-v后会成功。

接口抽象

下面使用 python 的 requests 库和 pydantic 库去定义接口抽象,学习 Spring Boot 的命名方式去定义相应的 Template。下面的代码中假设 token 存在DIDA_TOKEN环境变量中。

观察到,有三个实体:Project 和 Task 和 ChecklistItem,Project 即特定的清单(清单文件夹不算),Task 则是特定任务,任务包含零个或多个 ChecklistItem。

Project 实际上应当包含 Column,Column 才包含 Task,但……Task 没有引用 Column。容易猜想,新增的 Task 都是放在“默认”的 Task 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
from datetime import datetime
from enum import Enum, IntEnum
import os
from typing import Optional

from pydantic import BaseModel, ConfigDict, TypeAdapter
import requests

def to_snake(string: str) -> str:
import re
return re.sub(r'(?<!^)(?=[A-Z])', '_', string).lower()

class SubtaskStatus(IntEnum):
"""Subtask status enumeration."""
NORMAL = 0
COMPLETED = 1

class TaskStatus(IntEnum):
"""Task status enumeration."""
NORMAL = 0
COMPLETED = 2

class TaskPriority(IntEnum):
"""Task priority enumeration."""
NONE = 0
LOW = 1
MEDIUM = 3
HIGH = 5

class ViewMode(str, Enum):
"""Enumeration for project view modes."""
LIST = "list"
KANBAN = "kanban"
TIMELINE = "timeline"

class Permission(str, Enum):
"""Enumeration for project permissions."""
READ = "read"
WRITE = "write"
COMMENT = "comment"

class ProjectKind(str, Enum):
"""Enumeration for project kinds."""
TASK = "TASK"
NOTE = "NOTE"

class ChecklistItem(BaseModel):
"""Model representing a subtask."""

model_config = ConfigDict(alias_generator=to_snake, populate_by_name=True)

id: str # Subtask identifier
title: str # Subtask title
status: SubtaskStatus # The completion status of subtask
completed_time: Optional[datetime] = None # Subtask completed time in "yyyy-MM-dd'T'HH:mm:ssZ"
is_all_day: bool = False # All day
sort_order: int = 0 # Subtask sort order
start_date: Optional[datetime] = None # Subtask start date time in "yyyy-MM-dd'T'HH:mm:ssZ"
time_zone: Optional[str] = None # Subtask timezone

class Task(BaseModel):
"""Model representing a task."""
id: str # Task identifier
title: str # Task title
project_id: str = '' # Task project id
is_all_day: bool = False # All day
completed_time: Optional[datetime] = None # Task completed time in "yyyy-MM-dd'T'HH:mm:ssZ"
content: Optional[str] = None # Task content
desc: Optional[str] = None # Task description of checklist
due_date: Optional[datetime] = None # Task due date time in "yyyy-MM-dd'T'HH:mm:ssZ"
items: Optional[list[ChecklistItem]] = None # Subtasks of Task
priority: TaskPriority = TaskPriority.NONE # Task priority (0, 1, 3, 5)
reminders: Optional[list[str]] = None # List of reminder triggers
repeat_flag: Optional[str] = None # Recurring rules of task
sort_order: int = 0 # Task sort order
start_date: Optional[datetime] = None # Start date time in "yyyy-MM-dd'T'HH:mm:ssZ"
status: TaskStatus = TaskStatus.NORMAL # Task completion status (0, 2)
time_zone: Optional[str] = None # Task timezone

class Project(BaseModel):
"""Model representing a project."""
id: str # Project identifier
name: str # Project name
color: str = '' # Project color
sort_order: int = 0 # Order value (int64)
closed: bool = False # Project closed status
group_id: Optional[str] = None # Project group identifier
view_mode: ViewMode = ViewMode.LIST # View mode ("list", "kanban", "timeline")
permission: Permission = Permission.READ # Permission level ("read", "write", "comment")
kind: ProjectKind = ProjectKind.TASK # Project type ("TASK" or "NOTE")

class Column(BaseModel):
"""Model representing a column in a project."""
id: str # Column identifier
project_id: str # Project identifier
name: str # Column name
sort_order: int # Order value (int64)

# Main ProjectData Model
class ProjectData(BaseModel):
project: Project
tasks: list[Task] = []
# columns: list[Column] = [] # useless and buggy

class DidaTemplate:
"""
Abstraction of the open API of dida365.com.

不包含任何业务逻辑,只完全地反映它的接口,但只按照自己的需求抽象我需要用到的接口(我不会通过代码去完成任务,只添加和查询任务!)
"""
def __init__(self) -> None:
self._token = os.environ['DIDA_TOKEN']
self._base_url = 'https://dida365.com'
self._headers = {
'Authorization': f'Bearer {self._token}',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0'
}

def get_task(self, project_id: str, task_id: str) -> Task:
resp = requests.get(f'{self._base_url}/open/v1/project/{project_id}/task/{task_id}', headers=self._headers)
resp.raise_for_status()
return Task.model_validate_json(resp.content)

def create_task(self, task: Task) -> Task:
resp = requests.post(f'{self._base_url}/open/v1/task', headers=self._headers, json=task.model_dump_json())
resp.raise_for_status()
return Task.model_validate_json(resp.content)

def list_project(self) -> list[Project]:
resp = requests.get(f'{self._base_url}/open/v1/project', headers=self._headers)
resp.raise_for_status()
return TypeAdapter(list[Project]).validate_json(resp.content)

def get_project(self, project_id: str) -> ProjectData:
resp = requests.get(f'{self._base_url}/open/v1/project/{project_id}/data', headers=self._headers)
resp.raise_for_status()
return ProjectData.model_validate_json(resp.content)

if __name__ == '__main__':
template = DidaTemplate()
for proj in template.list_project():
proj_detail = template.get_project(proj.id)
for task in proj_detail.tasks:
print(task.title)

Over~