You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

oauth_data_source.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import urllib.parse
  2. from typing import Any
  3. import requests
  4. from flask_login import current_user
  5. from extensions.ext_database import db
  6. from libs.datetime_utils import naive_utc_now
  7. from models.source import DataSourceOauthBinding
  8. class OAuthDataSource:
  9. def __init__(self, client_id: str, client_secret: str, redirect_uri: str):
  10. self.client_id = client_id
  11. self.client_secret = client_secret
  12. self.redirect_uri = redirect_uri
  13. def get_authorization_url(self):
  14. raise NotImplementedError()
  15. def get_access_token(self, code: str):
  16. raise NotImplementedError()
  17. class NotionOAuth(OAuthDataSource):
  18. _AUTH_URL = "https://api.notion.com/v1/oauth/authorize"
  19. _TOKEN_URL = "https://api.notion.com/v1/oauth/token"
  20. _NOTION_PAGE_SEARCH = "https://api.notion.com/v1/search"
  21. _NOTION_BLOCK_SEARCH = "https://api.notion.com/v1/blocks"
  22. _NOTION_BOT_USER = "https://api.notion.com/v1/users/me"
  23. def get_authorization_url(self):
  24. params = {
  25. "client_id": self.client_id,
  26. "response_type": "code",
  27. "redirect_uri": self.redirect_uri,
  28. "owner": "user",
  29. }
  30. return f"{self._AUTH_URL}?{urllib.parse.urlencode(params)}"
  31. def get_access_token(self, code: str):
  32. data = {"code": code, "grant_type": "authorization_code", "redirect_uri": self.redirect_uri}
  33. headers = {"Accept": "application/json"}
  34. auth = (self.client_id, self.client_secret)
  35. response = requests.post(self._TOKEN_URL, data=data, auth=auth, headers=headers)
  36. response_json = response.json()
  37. access_token = response_json.get("access_token")
  38. if not access_token:
  39. raise ValueError(f"Error in Notion OAuth: {response_json}")
  40. workspace_name = response_json.get("workspace_name")
  41. workspace_icon = response_json.get("workspace_icon")
  42. workspace_id = response_json.get("workspace_id")
  43. # get all authorized pages
  44. pages = self.get_authorized_pages(access_token)
  45. source_info = {
  46. "workspace_name": workspace_name,
  47. "workspace_icon": workspace_icon,
  48. "workspace_id": workspace_id,
  49. "pages": pages,
  50. "total": len(pages),
  51. }
  52. # save data source binding
  53. data_source_binding = (
  54. db.session.query(DataSourceOauthBinding)
  55. .filter(
  56. db.and_(
  57. DataSourceOauthBinding.tenant_id == current_user.current_tenant_id,
  58. DataSourceOauthBinding.provider == "notion",
  59. DataSourceOauthBinding.access_token == access_token,
  60. )
  61. )
  62. .first()
  63. )
  64. if data_source_binding:
  65. data_source_binding.source_info = source_info
  66. data_source_binding.disabled = False
  67. data_source_binding.updated_at = naive_utc_now()
  68. db.session.commit()
  69. else:
  70. new_data_source_binding = DataSourceOauthBinding(
  71. tenant_id=current_user.current_tenant_id,
  72. access_token=access_token,
  73. source_info=source_info,
  74. provider="notion",
  75. )
  76. db.session.add(new_data_source_binding)
  77. db.session.commit()
  78. def save_internal_access_token(self, access_token: str):
  79. workspace_name = self.notion_workspace_name(access_token)
  80. workspace_icon = None
  81. workspace_id = current_user.current_tenant_id
  82. # get all authorized pages
  83. pages = self.get_authorized_pages(access_token)
  84. source_info = {
  85. "workspace_name": workspace_name,
  86. "workspace_icon": workspace_icon,
  87. "workspace_id": workspace_id,
  88. "pages": pages,
  89. "total": len(pages),
  90. }
  91. # save data source binding
  92. data_source_binding = (
  93. db.session.query(DataSourceOauthBinding)
  94. .filter(
  95. db.and_(
  96. DataSourceOauthBinding.tenant_id == current_user.current_tenant_id,
  97. DataSourceOauthBinding.provider == "notion",
  98. DataSourceOauthBinding.access_token == access_token,
  99. )
  100. )
  101. .first()
  102. )
  103. if data_source_binding:
  104. data_source_binding.source_info = source_info
  105. data_source_binding.disabled = False
  106. data_source_binding.updated_at = naive_utc_now()
  107. db.session.commit()
  108. else:
  109. new_data_source_binding = DataSourceOauthBinding(
  110. tenant_id=current_user.current_tenant_id,
  111. access_token=access_token,
  112. source_info=source_info,
  113. provider="notion",
  114. )
  115. db.session.add(new_data_source_binding)
  116. db.session.commit()
  117. def sync_data_source(self, binding_id: str):
  118. # save data source binding
  119. data_source_binding = (
  120. db.session.query(DataSourceOauthBinding)
  121. .filter(
  122. db.and_(
  123. DataSourceOauthBinding.tenant_id == current_user.current_tenant_id,
  124. DataSourceOauthBinding.provider == "notion",
  125. DataSourceOauthBinding.id == binding_id,
  126. DataSourceOauthBinding.disabled == False,
  127. )
  128. )
  129. .first()
  130. )
  131. if data_source_binding:
  132. # get all authorized pages
  133. pages = self.get_authorized_pages(data_source_binding.access_token)
  134. source_info = data_source_binding.source_info
  135. new_source_info = {
  136. "workspace_name": source_info["workspace_name"],
  137. "workspace_icon": source_info["workspace_icon"],
  138. "workspace_id": source_info["workspace_id"],
  139. "pages": pages,
  140. "total": len(pages),
  141. }
  142. data_source_binding.source_info = new_source_info
  143. data_source_binding.disabled = False
  144. data_source_binding.updated_at = naive_utc_now()
  145. db.session.commit()
  146. else:
  147. raise ValueError("Data source binding not found")
  148. def get_authorized_pages(self, access_token: str):
  149. pages = []
  150. page_results = self.notion_page_search(access_token)
  151. database_results = self.notion_database_search(access_token)
  152. # get page detail
  153. for page_result in page_results:
  154. page_id = page_result["id"]
  155. page_name = "Untitled"
  156. for key in page_result["properties"]:
  157. if "title" in page_result["properties"][key] and page_result["properties"][key]["title"]:
  158. title_list = page_result["properties"][key]["title"]
  159. if len(title_list) > 0 and "plain_text" in title_list[0]:
  160. page_name = title_list[0]["plain_text"]
  161. page_icon = page_result["icon"]
  162. if page_icon:
  163. icon_type = page_icon["type"]
  164. if icon_type in {"external", "file"}:
  165. url = page_icon[icon_type]["url"]
  166. icon = {"type": "url", "url": url if url.startswith("http") else f"https://www.notion.so{url}"}
  167. else:
  168. icon = {"type": "emoji", "emoji": page_icon[icon_type]}
  169. else:
  170. icon = None
  171. parent = page_result["parent"]
  172. parent_type = parent["type"]
  173. if parent_type == "block_id":
  174. parent_id = self.notion_block_parent_page_id(access_token, parent[parent_type])
  175. elif parent_type == "workspace":
  176. parent_id = "root"
  177. else:
  178. parent_id = parent[parent_type]
  179. page = {
  180. "page_id": page_id,
  181. "page_name": page_name,
  182. "page_icon": icon,
  183. "parent_id": parent_id,
  184. "type": "page",
  185. }
  186. pages.append(page)
  187. # get database detail
  188. for database_result in database_results:
  189. page_id = database_result["id"]
  190. if len(database_result["title"]) > 0:
  191. page_name = database_result["title"][0]["plain_text"]
  192. else:
  193. page_name = "Untitled"
  194. page_icon = database_result["icon"]
  195. if page_icon:
  196. icon_type = page_icon["type"]
  197. if icon_type in {"external", "file"}:
  198. url = page_icon[icon_type]["url"]
  199. icon = {"type": "url", "url": url if url.startswith("http") else f"https://www.notion.so{url}"}
  200. else:
  201. icon = {"type": icon_type, icon_type: page_icon[icon_type]}
  202. else:
  203. icon = None
  204. parent = database_result["parent"]
  205. parent_type = parent["type"]
  206. if parent_type == "block_id":
  207. parent_id = self.notion_block_parent_page_id(access_token, parent[parent_type])
  208. elif parent_type == "workspace":
  209. parent_id = "root"
  210. else:
  211. parent_id = parent[parent_type]
  212. page = {
  213. "page_id": page_id,
  214. "page_name": page_name,
  215. "page_icon": icon,
  216. "parent_id": parent_id,
  217. "type": "database",
  218. }
  219. pages.append(page)
  220. return pages
  221. def notion_page_search(self, access_token: str):
  222. results = []
  223. next_cursor = None
  224. has_more = True
  225. while has_more:
  226. data: dict[str, Any] = {
  227. "filter": {"value": "page", "property": "object"},
  228. **({"start_cursor": next_cursor} if next_cursor else {}),
  229. }
  230. headers = {
  231. "Content-Type": "application/json",
  232. "Authorization": f"Bearer {access_token}",
  233. "Notion-Version": "2022-06-28",
  234. }
  235. response = requests.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers)
  236. response_json = response.json()
  237. results.extend(response_json.get("results", []))
  238. has_more = response_json.get("has_more", False)
  239. next_cursor = response_json.get("next_cursor", None)
  240. return results
  241. def notion_block_parent_page_id(self, access_token: str, block_id: str):
  242. headers = {
  243. "Authorization": f"Bearer {access_token}",
  244. "Notion-Version": "2022-06-28",
  245. }
  246. response = requests.get(url=f"{self._NOTION_BLOCK_SEARCH}/{block_id}", headers=headers)
  247. response_json = response.json()
  248. if response.status_code != 200:
  249. message = response_json.get("message", "unknown error")
  250. raise ValueError(f"Error fetching block parent page ID: {message}")
  251. parent = response_json["parent"]
  252. parent_type = parent["type"]
  253. if parent_type == "block_id":
  254. return self.notion_block_parent_page_id(access_token, parent[parent_type])
  255. return parent[parent_type]
  256. def notion_workspace_name(self, access_token: str):
  257. headers = {
  258. "Authorization": f"Bearer {access_token}",
  259. "Notion-Version": "2022-06-28",
  260. }
  261. response = requests.get(url=self._NOTION_BOT_USER, headers=headers)
  262. response_json = response.json()
  263. if "object" in response_json and response_json["object"] == "user":
  264. user_type = response_json["type"]
  265. user_info = response_json[user_type]
  266. if "workspace_name" in user_info:
  267. return user_info["workspace_name"]
  268. return "workspace"
  269. def notion_database_search(self, access_token: str):
  270. results = []
  271. next_cursor = None
  272. has_more = True
  273. while has_more:
  274. data: dict[str, Any] = {
  275. "filter": {"value": "database", "property": "object"},
  276. **({"start_cursor": next_cursor} if next_cursor else {}),
  277. }
  278. headers = {
  279. "Content-Type": "application/json",
  280. "Authorization": f"Bearer {access_token}",
  281. "Notion-Version": "2022-06-28",
  282. }
  283. response = requests.post(url=self._NOTION_PAGE_SEARCH, json=data, headers=headers)
  284. response_json = response.json()
  285. results.extend(response_json.get("results", []))
  286. has_more = response_json.get("has_more", False)
  287. next_cursor = response_json.get("next_cursor", None)
  288. return results