Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

oauth_data_source.py 12KB

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