oauth_data_source.py 11 KB

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