hyperpocket 0.0.3__py3-none-any.whl → 0.1.9__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- hyperpocket/auth/README.md +3 -3
- hyperpocket/auth/__init__.py +0 -8
- hyperpocket/auth/gumloop/context.py +13 -0
- hyperpocket/auth/gumloop/token_context.py +15 -0
- hyperpocket/auth/gumloop/token_handler.py +66 -0
- hyperpocket/auth/gumloop/token_schema.py +8 -0
- hyperpocket/auth/linear/token_context.py +1 -1
- hyperpocket/auth/notion/README.md +28 -0
- hyperpocket/auth/notion/context.py +15 -0
- hyperpocket/auth/notion/token_context.py +14 -0
- hyperpocket/auth/notion/token_handler.py +65 -0
- hyperpocket/auth/notion/token_schema.py +10 -0
- hyperpocket/auth/provider.py +8 -5
- hyperpocket/auth/reddit/context.py +15 -0
- hyperpocket/auth/reddit/oauth2_context.py +32 -0
- hyperpocket/auth/reddit/oauth2_handler.py +151 -0
- hyperpocket/auth/reddit/oauth2_schema.py +18 -0
- hyperpocket/auth/slack/token_context.py +1 -1
- hyperpocket/builtin.py +63 -0
- hyperpocket/cli/__main__.py +12 -0
- hyperpocket/cli/auth.py +83 -0
- hyperpocket/cli/codegen/auth/__init__.py +13 -0
- hyperpocket/cli/codegen/auth/auth_context_template.py +16 -0
- hyperpocket/cli/codegen/auth/auth_token_context_template.py +16 -0
- hyperpocket/cli/codegen/auth/auth_token_handler_template.py +69 -0
- hyperpocket/cli/codegen/auth/auth_token_schema_template.py +12 -0
- hyperpocket/cli/codegen/auth/server_auth_template.py +18 -0
- hyperpocket/cli/eject.py +19 -0
- hyperpocket/cli/sync.py +5 -5
- hyperpocket/config/settings.py +14 -12
- hyperpocket/futures/futurestore.py +0 -1
- hyperpocket/pocket_auth.py +25 -5
- hyperpocket/pocket_core.py +264 -0
- hyperpocket/pocket_main.py +127 -174
- hyperpocket/prompts.py +6 -8
- hyperpocket/repository/__init__.py +2 -2
- hyperpocket/repository/lock.py +71 -1
- hyperpocket/repository/lockfile.py +19 -13
- hyperpocket/repository/repository.py +26 -1
- hyperpocket/server/auth/__init__.py +0 -6
- hyperpocket/server/auth/gumloop.py +16 -0
- hyperpocket/server/auth/notion.py +19 -0
- hyperpocket/server/auth/reddit.py +16 -0
- hyperpocket/server/server.py +56 -20
- hyperpocket/server/tool/dto/script.py +15 -2
- hyperpocket/server/tool/wasm.py +20 -8
- hyperpocket/session/README.md +2 -2
- hyperpocket/session/in_memory.py +18 -5
- hyperpocket/session/interface.py +14 -0
- hyperpocket/session/redis.py +29 -5
- hyperpocket/tool/README.md +16 -12
- hyperpocket/tool/__init__.py +4 -3
- hyperpocket/tool/function/README.md +39 -10
- hyperpocket/tool/function/__init__.py +2 -0
- hyperpocket/tool/function/annotation.py +2 -1
- hyperpocket/tool/function/tool.py +108 -29
- hyperpocket/tool/tool.py +100 -28
- hyperpocket/tool/wasm/README.md +27 -5
- hyperpocket/tool/wasm/browser.py +2 -7
- hyperpocket/tool/wasm/script.py +40 -1
- hyperpocket/tool/wasm/templates/python.py +32 -14
- hyperpocket/tool/wasm/tool.py +21 -18
- hyperpocket/tool_like.py +5 -0
- hyperpocket/util/__init__.py +1 -1
- hyperpocket/util/extract_func_param_desc_from_docstring.py +4 -4
- hyperpocket/util/function_to_model.py +5 -2
- hyperpocket/util/json_schema_to_model.py +47 -26
- {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/METADATA +107 -88
- hyperpocket-0.1.9.dist-info/RECORD +137 -0
- {hyperpocket-0.0.3.dist-info → hyperpocket-0.1.9.dist-info}/WHEEL +1 -1
- hyperpocket-0.1.9.dist-info/entry_points.txt +2 -0
- hyperpocket/auth/README.KR.md +0 -309
- hyperpocket/auth/slack/tests/test_oauth2_handler.py +0 -32
- hyperpocket/auth/slack/tests/test_token_handler.py +0 -23
- hyperpocket/auth/tests/test_google_oauth2_handler.py +0 -147
- hyperpocket/auth/tests/test_slack_oauth2_handler.py +0 -147
- hyperpocket/auth/tests/test_slack_token_handler.py +0 -66
- hyperpocket/external/__init__.py +0 -7
- hyperpocket/external/github_client.py +0 -19
- hyperpocket/session/README.KR.md +0 -62
- hyperpocket/session/tests/test_in_memory.py +0 -145
- hyperpocket/session/tests/test_redis.py +0 -151
- hyperpocket/tests/test_pocket.py +0 -116
- hyperpocket/tests/test_pocket_auth.py +0 -982
- hyperpocket/tool/README.KR.md +0 -68
- hyperpocket/tool/builtins/__init__.py +0 -0
- hyperpocket/tool/builtins/example/__init__.py +0 -0
- hyperpocket/tool/builtins/example/add_tool.py +0 -18
- hyperpocket/tool/function/README.KR.md +0 -159
- hyperpocket/tool/tests/test_function_tool.py +0 -266
- hyperpocket/tool/wasm/README.KR.md +0 -144
- hyperpocket-0.0.3.dist-info/RECORD +0 -130
- hyperpocket-0.0.3.dist-info/entry_points.txt +0 -3
- /hyperpocket/auth/{slack/tests → gumloop}/__init__.py +0 -0
- /hyperpocket/auth/{tests → notion}/__init__.py +0 -0
- /hyperpocket/{session/tests → auth/reddit}/__init__.py +0 -0
- /hyperpocket/{tests → cli/codegen}/__init__.py +0 -0
hyperpocket/auth/README.md
CHANGED
@@ -236,15 +236,15 @@ class AuthHandlerInterface(ABC):
|
|
236
236
|
- This class perform the actual prepare, authenticate, and refresh steps.
|
237
237
|
|
238
238
|
5. Add a new enum value to the `AuthProvider` Enum(only implement new auth provider)
|
239
|
-
- If a new `AuthProvider` is added, update the `AuthProvider` enum in `
|
239
|
+
- If a new `AuthProvider` is added, update the `AuthProvider` enum in `hyperpocket/auth/provider.py`
|
240
240
|
|
241
241
|
6. Add new auth callback server endpoint
|
242
|
-
- Add the endpoint under `
|
242
|
+
- Add the endpoint under `hyperpocket/server/auth/` packages.
|
243
243
|
- Declare an appropriate `APIRouter` in the package. Pocket will automatically register the endpoint during
|
244
244
|
initialization.
|
245
245
|
|
246
246
|
7. Add test code(optional)
|
247
|
-
- Add the test code under `
|
247
|
+
- Add the test code under `hyperpocket/auth/tests/` packages.
|
248
248
|
|
249
249
|
---
|
250
250
|
|
hyperpocket/auth/__init__.py
CHANGED
@@ -1,14 +1,6 @@
|
|
1
1
|
from hyperpocket.auth.context import AuthContext
|
2
|
-
from hyperpocket.auth.github.context import GitHubAuthContext
|
3
|
-
from hyperpocket.auth.github.oauth2_context import GitHubOAuth2AuthContext
|
4
|
-
from hyperpocket.auth.github.token_context import GitHubTokenAuthContext
|
5
|
-
from hyperpocket.auth.google.context import GoogleAuthContext
|
6
|
-
from hyperpocket.auth.google.oauth2_context import GoogleOAuth2AuthContext
|
7
2
|
from hyperpocket.auth.handler import AuthHandlerInterface
|
8
|
-
from hyperpocket.auth.linear.token_context import LinearTokenAuthContext
|
9
3
|
from hyperpocket.auth.provider import AuthProvider
|
10
|
-
from hyperpocket.auth.slack.oauth2_context import SlackOAuth2AuthContext
|
11
|
-
from hyperpocket.auth.slack.token_context import SlackTokenAuthContext
|
12
4
|
from hyperpocket.util.find_all_leaf_class_in_package import find_all_leaf_class_in_package
|
13
5
|
|
14
6
|
PREBUILT_AUTH_HANDLERS = find_all_leaf_class_in_package("hyperpocket.auth", AuthHandlerInterface)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from hyperpocket.auth.context import AuthContext
|
2
|
+
|
3
|
+
|
4
|
+
class GumLoopContext(AuthContext):
|
5
|
+
_ACCESS_TOKEN_KEY: str = "GUMLOOP_TOKEN"
|
6
|
+
|
7
|
+
def to_dict(self) -> dict[str, str]:
|
8
|
+
return {self._ACCESS_TOKEN_KEY: self.access_token}
|
9
|
+
|
10
|
+
def to_profiled_dict(self, profile: str) -> dict[str, str]:
|
11
|
+
return {
|
12
|
+
f"{profile.upper()}_{self._ACCESS_TOKEN_KEY}": self.access_token,
|
13
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from hyperpocket.auth.gumloop.context import GumLoopContext
|
2
|
+
from hyperpocket.auth.gumloop.token_schema import GumloopTokenResponse
|
3
|
+
|
4
|
+
|
5
|
+
class GumLoopTokenContext(GumLoopContext):
|
6
|
+
|
7
|
+
@classmethod
|
8
|
+
def from_gumloop_token_response(cls, response: GumloopTokenResponse):
|
9
|
+
description = "Gumloop Token Context logged in"
|
10
|
+
|
11
|
+
return cls(
|
12
|
+
access_token=response.access_token,
|
13
|
+
description=description,
|
14
|
+
expires_at=None
|
15
|
+
)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
from urllib.parse import urljoin, urlencode
|
3
|
+
|
4
|
+
from hyperpocket.auth import AuthHandlerInterface, AuthContext, AuthProvider
|
5
|
+
from hyperpocket.auth.gumloop.token_context import GumLoopTokenContext
|
6
|
+
from hyperpocket.auth.gumloop.token_schema import GumloopTokenRequest, GumloopTokenResponse
|
7
|
+
from hyperpocket.auth.schema import AuthenticateRequest
|
8
|
+
from hyperpocket.config import config
|
9
|
+
from hyperpocket.futures import FutureStore
|
10
|
+
|
11
|
+
|
12
|
+
class GumloopTokenAuthHandler(AuthHandlerInterface):
|
13
|
+
name: str = "gumloop-token"
|
14
|
+
description: str = "This handler is used to authenticate users using the gumloop token"
|
15
|
+
scoped: bool = False
|
16
|
+
|
17
|
+
_TOKEN_URL = urljoin(config.public_base_url + "/", f"{config.callback_url_rewrite_prefix}/auth/token")
|
18
|
+
|
19
|
+
@staticmethod
|
20
|
+
def provider() -> AuthProvider:
|
21
|
+
return AuthProvider.GUMLOOP
|
22
|
+
|
23
|
+
@staticmethod
|
24
|
+
def provider_default() -> bool:
|
25
|
+
return True
|
26
|
+
|
27
|
+
@staticmethod
|
28
|
+
def recommended_scopes() -> set[str]:
|
29
|
+
return set()
|
30
|
+
|
31
|
+
def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> AuthenticateRequest:
|
32
|
+
return GumloopTokenRequest()
|
33
|
+
|
34
|
+
def prepare(self, auth_req: AuthenticateRequest, thread_id: str, profile: str, future_uid: str, *args,
|
35
|
+
**kwargs) -> str:
|
36
|
+
redirect_uri = urljoin(
|
37
|
+
config.public_base_url + "/",
|
38
|
+
f"{config.callback_url_rewrite_prefix}/auth/gumloop/token/callback",
|
39
|
+
)
|
40
|
+
auth_url = self._make_auth_url(auth_req=auth_req, redirect_uri=redirect_uri, state=future_uid)
|
41
|
+
FutureStore.create_future(future_uid, data={
|
42
|
+
"redirect_uri": redirect_uri,
|
43
|
+
"thread_id": thread_id,
|
44
|
+
"profile": profile,
|
45
|
+
})
|
46
|
+
|
47
|
+
return f'User needs to authenticate using the following URL: {auth_url}'
|
48
|
+
|
49
|
+
async def authenticate(self, auth_req: AuthenticateRequest, future_uid: str, *args, **kwargs) -> AuthContext:
|
50
|
+
future_data = FutureStore.get_future(future_uid)
|
51
|
+
access_token = await future_data.future
|
52
|
+
|
53
|
+
response = GumloopTokenResponse(access_token=access_token)
|
54
|
+
context = GumLoopTokenContext.from_gumloop_token_response(response)
|
55
|
+
|
56
|
+
return context
|
57
|
+
|
58
|
+
async def refresh(self, auth_req: AuthenticateRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
|
59
|
+
raise Exception("gumloop token doesn't support refresh")
|
60
|
+
|
61
|
+
def _make_auth_url(self, auth_req: AuthenticateRequest, redirect_uri: str, state: str):
|
62
|
+
params = {
|
63
|
+
"redirect_uri": redirect_uri,
|
64
|
+
"state": state,
|
65
|
+
}
|
66
|
+
return f"{self._TOKEN_URL}?{urlencode(params)}"
|
@@ -6,7 +6,7 @@ class LinearTokenAuthContext(LinearAuthContext):
|
|
6
6
|
|
7
7
|
@classmethod
|
8
8
|
def from_linear_token_response(cls, response: LinearTokenResponse):
|
9
|
-
description =
|
9
|
+
description = 'Linear Token Context logged in'
|
10
10
|
|
11
11
|
return cls(
|
12
12
|
access_token=response.access_token,
|
@@ -0,0 +1,28 @@
|
|
1
|
+
### Notion Token Auth
|
2
|
+
|
3
|
+
This module provides authentication using Notion tokens.
|
4
|
+
|
5
|
+
1. To use this authentication in your tool, include the following in your `config.toml`:
|
6
|
+
|
7
|
+
```toml
|
8
|
+
[auth]
|
9
|
+
auth_provider = "notion"
|
10
|
+
auth_handler = "notion-token"
|
11
|
+
scopes = []
|
12
|
+
```
|
13
|
+
|
14
|
+
2. To use it with `function_tool`, you can define your function as follows:
|
15
|
+
|
16
|
+
```python
|
17
|
+
from hyperpocket.tool import function_tool
|
18
|
+
from hyperpocket.auth import AuthProvider
|
19
|
+
|
20
|
+
|
21
|
+
@function_tool(
|
22
|
+
auth_provider=AuthProvider.NOTION
|
23
|
+
)
|
24
|
+
def my_function(**kwargs):
|
25
|
+
token = kwargs["NOTION_TOKEN"]
|
26
|
+
|
27
|
+
# ...
|
28
|
+
```
|
@@ -0,0 +1,15 @@
|
|
1
|
+
|
2
|
+
from hyperpocket.auth.context import AuthContext
|
3
|
+
|
4
|
+
class NotionAuthContext(AuthContext):
|
5
|
+
_ACCESS_TOKEN_KEY: str = "NOTION_TOKEN"
|
6
|
+
|
7
|
+
def to_dict(self) -> dict[str, str]:
|
8
|
+
return {
|
9
|
+
self._ACCESS_TOKEN_KEY: self.access_token,
|
10
|
+
}
|
11
|
+
|
12
|
+
def to_profiled_dict(self, profile: str) -> dict[str, str]:
|
13
|
+
return {
|
14
|
+
f"{profile.upper()}_{self._ACCESS_TOKEN_KEY}": self.access_token,
|
15
|
+
}
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from hyperpocket.auth.notion.context import NotionAuthContext
|
2
|
+
from hyperpocket.auth.notion.token_schema import NotionTokenResponse
|
3
|
+
|
4
|
+
|
5
|
+
class NotionTokenAuthContext(NotionAuthContext):
|
6
|
+
@classmethod
|
7
|
+
def from_notion_token_response(cls, response: NotionTokenResponse):
|
8
|
+
description = 'Notion Token Context logged in'
|
9
|
+
|
10
|
+
return cls(
|
11
|
+
access_token=response.access_token,
|
12
|
+
description=description,
|
13
|
+
expires_at=None
|
14
|
+
)
|
@@ -0,0 +1,65 @@
|
|
1
|
+
|
2
|
+
from typing import Optional
|
3
|
+
from urllib.parse import urljoin, urlencode
|
4
|
+
|
5
|
+
from hyperpocket.auth import AuthProvider
|
6
|
+
from hyperpocket.auth.context import AuthContext
|
7
|
+
from hyperpocket.auth.handler import AuthHandlerInterface
|
8
|
+
from hyperpocket.auth.notion.token_context import NotionTokenAuthContext
|
9
|
+
from hyperpocket.auth.notion.token_schema import NotionTokenResponse, NotionTokenRequest
|
10
|
+
from hyperpocket.config import config
|
11
|
+
from hyperpocket.futures import FutureStore
|
12
|
+
|
13
|
+
|
14
|
+
class NotionTokenAuthHandler(AuthHandlerInterface):
|
15
|
+
name: str = "notion-token"
|
16
|
+
description: str = "This handler is used to authenticate users using the Notion token."
|
17
|
+
scoped: bool = False
|
18
|
+
|
19
|
+
_TOKEN_URL: str = urljoin(config.public_base_url + "/", f"{config.callback_url_rewrite_prefix}/auth/token")
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def provider() -> AuthProvider:
|
23
|
+
return AuthProvider.NOTION
|
24
|
+
|
25
|
+
@staticmethod
|
26
|
+
def recommended_scopes() -> set[str]:
|
27
|
+
return set()
|
28
|
+
|
29
|
+
def prepare(self, auth_req: NotionTokenRequest, thread_id: str, profile: str,
|
30
|
+
future_uid: str, *args, **kwargs) -> str:
|
31
|
+
redirect_uri = urljoin(
|
32
|
+
config.public_base_url + "/",
|
33
|
+
f"{config.callback_url_rewrite_prefix}/auth/notion/token/callback",
|
34
|
+
)
|
35
|
+
url = self._make_auth_url(auth_req=auth_req, redirect_uri=redirect_uri, state=future_uid)
|
36
|
+
FutureStore.create_future(future_uid, data={
|
37
|
+
"redirect_uri": redirect_uri,
|
38
|
+
"thread_id": thread_id,
|
39
|
+
"profile": profile,
|
40
|
+
})
|
41
|
+
|
42
|
+
return f'User needs to authenticate using the following URL: {url}'
|
43
|
+
|
44
|
+
async def authenticate(self, auth_req: NotionTokenRequest, future_uid: str, *args, **kwargs) -> AuthContext:
|
45
|
+
future_data = FutureStore.get_future(future_uid)
|
46
|
+
access_token = await future_data.future
|
47
|
+
|
48
|
+
response = NotionTokenResponse(access_token=access_token)
|
49
|
+
context = NotionTokenAuthContext.from_notion_token_response(response)
|
50
|
+
|
51
|
+
return context
|
52
|
+
|
53
|
+
async def refresh(self, auth_req: NotionTokenRequest, context: AuthContext, *args, **kwargs) -> AuthContext:
|
54
|
+
raise Exception("Notion token doesn't support refresh")
|
55
|
+
|
56
|
+
def _make_auth_url(self, auth_req: NotionTokenRequest, redirect_uri: str, state: str):
|
57
|
+
params = {
|
58
|
+
"redirect_uri": redirect_uri,
|
59
|
+
"state": state,
|
60
|
+
}
|
61
|
+
auth_url = f"{self._TOKEN_URL}?{urlencode(params)}"
|
62
|
+
return auth_url
|
63
|
+
|
64
|
+
def make_request(self, auth_scopes: Optional[list[str]] = None, **kwargs) -> NotionTokenRequest:
|
65
|
+
return NotionTokenRequest()
|
hyperpocket/auth/provider.py
CHANGED
@@ -2,11 +2,14 @@ from enum import Enum
|
|
2
2
|
|
3
3
|
|
4
4
|
class AuthProvider(Enum):
|
5
|
-
SLACK =
|
6
|
-
LINEAR =
|
7
|
-
GITHUB =
|
8
|
-
GOOGLE =
|
9
|
-
CALENDLY =
|
5
|
+
SLACK = "slack"
|
6
|
+
LINEAR = "linear"
|
7
|
+
GITHUB = "github"
|
8
|
+
GOOGLE = "google"
|
9
|
+
CALENDLY = "calendly"
|
10
|
+
NOTION = "notion"
|
11
|
+
REDDIT = "reddit"
|
12
|
+
GUMLOOP = "gumloop"
|
10
13
|
|
11
14
|
@classmethod
|
12
15
|
def get_auth_provider(cls, auth_provider_name: str) -> "AuthProvider":
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from hyperpocket.auth.context import AuthContext
|
2
|
+
|
3
|
+
|
4
|
+
class RedditAuthContext(AuthContext):
|
5
|
+
_ACCESS_TOKEN_KEY: str = "REDDIT_BOT_TOKEN"
|
6
|
+
|
7
|
+
def to_dict(self) -> dict[str, str]:
|
8
|
+
return {
|
9
|
+
self._ACCESS_TOKEN_KEY: self.access_token,
|
10
|
+
}
|
11
|
+
|
12
|
+
def to_profiled_dict(self, profile: str) -> dict[str, str]:
|
13
|
+
return {
|
14
|
+
f"{profile.upper()}_{self._ACCESS_TOKEN_KEY}": self.access_token,
|
15
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
from datetime import datetime, timedelta, timezone
|
2
|
+
from typing import Optional
|
3
|
+
|
4
|
+
from pydantic import Field
|
5
|
+
|
6
|
+
from hyperpocket.auth.reddit.context import RedditAuthContext
|
7
|
+
from hyperpocket.auth.reddit.oauth2_schema import RedditOAuth2Response
|
8
|
+
|
9
|
+
|
10
|
+
class RedditOAuth2AuthContext(RedditAuthContext):
|
11
|
+
refresh_token: Optional[str] = Field(default=None, description="refresh token")
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def from_reddit_oauth2_response(cls, response: RedditOAuth2Response):
|
15
|
+
now = datetime.now(tz=timezone.utc)
|
16
|
+
|
17
|
+
access_token = response.access_token
|
18
|
+
refresh_token = response.refresh_token
|
19
|
+
expires_in = response.expires_in
|
20
|
+
|
21
|
+
if expires_in:
|
22
|
+
expires_at = now + timedelta(seconds=expires_in)
|
23
|
+
else:
|
24
|
+
expires_at = None
|
25
|
+
|
26
|
+
return cls(
|
27
|
+
access_token=access_token,
|
28
|
+
refresh_token=refresh_token,
|
29
|
+
expires_at=expires_at,
|
30
|
+
description="Reddit OAuth2 authentication context",
|
31
|
+
detail=response,
|
32
|
+
)
|
@@ -0,0 +1,151 @@
|
|
1
|
+
import base64
|
2
|
+
from os import access
|
3
|
+
from typing import Optional
|
4
|
+
from urllib.parse import urljoin, urlencode
|
5
|
+
|
6
|
+
import httpx
|
7
|
+
|
8
|
+
from hyperpocket.auth import AuthProvider
|
9
|
+
from hyperpocket.auth.context import AuthContext
|
10
|
+
from hyperpocket.auth.handler import AuthHandlerInterface
|
11
|
+
from hyperpocket.auth.reddit.oauth2_context import RedditOAuth2AuthContext
|
12
|
+
from hyperpocket.auth.reddit.oauth2_schema import (
|
13
|
+
RedditOAuth2Response,
|
14
|
+
RedditOAuth2Request,
|
15
|
+
)
|
16
|
+
from hyperpocket.config import config as config
|
17
|
+
from hyperpocket.futures import FutureStore
|
18
|
+
|
19
|
+
|
20
|
+
class RedditOAuth2AuthHandler(AuthHandlerInterface):
|
21
|
+
_REDDIT_OAUTH_URL: str = "https://www.reddit.com/api/v1/authorize"
|
22
|
+
_REDDIT_TOKEN_URL: str = "https://www.reddit.com/api/v1/access_token"
|
23
|
+
|
24
|
+
name: str = "reddit-oauth2"
|
25
|
+
description: str = "This handler is used to authenticate users using the Reddit OAuth2 authentication method."
|
26
|
+
scoped: bool = True
|
27
|
+
|
28
|
+
@staticmethod
|
29
|
+
def provider() -> AuthProvider:
|
30
|
+
return AuthProvider.REDDIT
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def provider_default() -> bool:
|
34
|
+
return True
|
35
|
+
|
36
|
+
@staticmethod
|
37
|
+
def recommended_scopes() -> set[str]:
|
38
|
+
if config.auth.reddit.use_recommended_scope:
|
39
|
+
recommended_scopes = {"account", "identity", "read"}
|
40
|
+
else:
|
41
|
+
recommended_scopes = {}
|
42
|
+
return recommended_scopes
|
43
|
+
|
44
|
+
def prepare(
|
45
|
+
self,
|
46
|
+
auth_req: RedditOAuth2Request,
|
47
|
+
thread_id: str,
|
48
|
+
profile: str,
|
49
|
+
future_uid: str,
|
50
|
+
*args,
|
51
|
+
**kwargs,
|
52
|
+
) -> str:
|
53
|
+
redirect_uri = urljoin(
|
54
|
+
config.public_base_url + "/",
|
55
|
+
f"{config.callback_url_rewrite_prefix}/auth/reddit/oauth2/callback",
|
56
|
+
)
|
57
|
+
print(f"redirect_uri: {redirect_uri}")
|
58
|
+
auth_url = self._make_auth_url(
|
59
|
+
req=auth_req, redirect_uri=redirect_uri, state=future_uid
|
60
|
+
)
|
61
|
+
|
62
|
+
FutureStore.create_future(
|
63
|
+
future_uid,
|
64
|
+
data={
|
65
|
+
"redirect_uri": redirect_uri,
|
66
|
+
"thread_id": thread_id,
|
67
|
+
"profile": profile,
|
68
|
+
},
|
69
|
+
)
|
70
|
+
|
71
|
+
return f"User needs to authenticate using the following URL: {auth_url}"
|
72
|
+
|
73
|
+
async def authenticate(
|
74
|
+
self, auth_req: RedditOAuth2Request, future_uid: str, *args, **kwargs
|
75
|
+
) -> AuthContext:
|
76
|
+
future_data = FutureStore.get_future(future_uid)
|
77
|
+
auth_code = await future_data.future
|
78
|
+
|
79
|
+
basic_auth = f"{auth_req.client_id}:{auth_req.client_secret}"
|
80
|
+
basic_auth_encoded = base64.b64encode(basic_auth.encode()).decode()
|
81
|
+
async with httpx.AsyncClient() as client:
|
82
|
+
resp = await client.post(
|
83
|
+
url=self._REDDIT_TOKEN_URL,
|
84
|
+
data={
|
85
|
+
"code": auth_code,
|
86
|
+
"redirect_uri": future_data.data["redirect_uri"],
|
87
|
+
"grant_type": "authorization_code",
|
88
|
+
},
|
89
|
+
headers={
|
90
|
+
"Authorization": f"Basic {basic_auth_encoded}",
|
91
|
+
},
|
92
|
+
)
|
93
|
+
if resp.status_code != 200:
|
94
|
+
raise Exception(f"failed to authenticate. status_code : {resp.status_code}")
|
95
|
+
|
96
|
+
resp_json = resp.json()
|
97
|
+
|
98
|
+
resp_typed = RedditOAuth2Response(**resp_json)
|
99
|
+
return RedditOAuth2AuthContext.from_reddit_oauth2_response(resp_typed)
|
100
|
+
|
101
|
+
async def refresh(
|
102
|
+
self, auth_req: RedditOAuth2Request, context: AuthContext, *args, **kwargs
|
103
|
+
) -> AuthContext:
|
104
|
+
reddit_context: RedditOAuth2AuthContext = context
|
105
|
+
refresh_token = reddit_context.refresh_token
|
106
|
+
|
107
|
+
async with httpx.AsyncClient() as client:
|
108
|
+
resp = await client.post(
|
109
|
+
url=self._REDDIT_TOKEN_URL,
|
110
|
+
data={
|
111
|
+
"client_id": config.auth.reddit.client_id,
|
112
|
+
"client_secret": config.auth.reddit.client_secret,
|
113
|
+
"grant_type": "refresh_token",
|
114
|
+
"refresh_token": refresh_token,
|
115
|
+
},
|
116
|
+
)
|
117
|
+
|
118
|
+
if resp.status_code != 200:
|
119
|
+
raise Exception(f"failed to refresh. status_code : {resp.status_code}")
|
120
|
+
|
121
|
+
resp_json = resp.json()
|
122
|
+
|
123
|
+
new_resp: RedditOAuth2Response = RedditOAuth2Response(
|
124
|
+
access_token=resp_json["access_token"],
|
125
|
+
token_type=resp_json["token_type"],
|
126
|
+
expires_in=resp_json["expires_in"],
|
127
|
+
scope=resp_json["scope"],
|
128
|
+
)
|
129
|
+
|
130
|
+
return RedditOAuth2AuthContext.from_reddit_oauth2_response(new_resp)
|
131
|
+
|
132
|
+
def _make_auth_url(self, req: RedditOAuth2Request, redirect_uri: str, state: str):
|
133
|
+
params = {
|
134
|
+
"scope": ",".join(req.auth_scopes),
|
135
|
+
"client_id": req.client_id,
|
136
|
+
"redirect_uri": redirect_uri,
|
137
|
+
"state": state,
|
138
|
+
"response_type": "code",
|
139
|
+
"duration": "permanent",
|
140
|
+
}
|
141
|
+
auth_url = f"{self._REDDIT_OAUTH_URL}?{urlencode(params)}"
|
142
|
+
return auth_url
|
143
|
+
|
144
|
+
def make_request(
|
145
|
+
self, auth_scopes: Optional[list[str]] = None, **kwargs
|
146
|
+
) -> RedditOAuth2Request:
|
147
|
+
return RedditOAuth2Request(
|
148
|
+
auth_scopes=auth_scopes,
|
149
|
+
client_id=config.auth.reddit.client_id,
|
150
|
+
client_secret=config.auth.reddit.client_secret,
|
151
|
+
)
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from hyperpocket.auth.schema import AuthenticateRequest, AuthenticateResponse
|
6
|
+
|
7
|
+
|
8
|
+
class RedditOAuth2Request(AuthenticateRequest):
|
9
|
+
client_id: str
|
10
|
+
client_secret: str
|
11
|
+
|
12
|
+
|
13
|
+
class RedditOAuth2Response(AuthenticateResponse):
|
14
|
+
access_token: Optional[str] = None
|
15
|
+
token_type: Optional[str] = None
|
16
|
+
refresh_token: Optional[str] = None
|
17
|
+
expires_in: Optional[int] = None
|
18
|
+
scope: Optional[str] = None
|
@@ -5,7 +5,7 @@ from hyperpocket.auth.slack.token_schema import SlackTokenResponse
|
|
5
5
|
class SlackTokenAuthContext(SlackAuthContext):
|
6
6
|
@classmethod
|
7
7
|
def from_slack_token_response(cls, response: SlackTokenResponse):
|
8
|
-
description =
|
8
|
+
description = 'Slack Token Context logged in'
|
9
9
|
|
10
10
|
return cls(
|
11
11
|
access_token=response.access_token,
|
hyperpocket/builtin.py
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
from typing import List
|
2
|
+
|
3
|
+
from hyperpocket.auth import AuthProvider
|
4
|
+
from hyperpocket.pocket_auth import PocketAuth
|
5
|
+
from hyperpocket.tool import from_func, Tool
|
6
|
+
|
7
|
+
|
8
|
+
def get_builtin_tools(pocket_auth: PocketAuth) -> List[Tool]:
|
9
|
+
"""
|
10
|
+
Get Builtin Tools
|
11
|
+
|
12
|
+
Builtin Tool can access to Pocket Core.
|
13
|
+
"""
|
14
|
+
|
15
|
+
def __get_current_thread_session_state(thread_id: str = "default") -> str:
|
16
|
+
"""
|
17
|
+
This tool retrieves the current session state list for the specified thread.
|
18
|
+
|
19
|
+
The tool should only be called when a user explicitly requests to view their session information.
|
20
|
+
|
21
|
+
The output includes a list of session states in the format:
|
22
|
+
- `[AUTH PROVIDER] [state] [scopes, ...] : explanation`
|
23
|
+
|
24
|
+
It does not contain any sensitive information.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
- thread_id (str): Thread ID
|
28
|
+
|
29
|
+
Returns:
|
30
|
+
- str: A list of session states describing the current authentication status.
|
31
|
+
|
32
|
+
This tool ensures transparency about the current session but must respect user-driven intent and should never be called automatically or without a specific user request.
|
33
|
+
"""
|
34
|
+
session_list = pocket_auth.list_session_state(thread_id)
|
35
|
+
return str(session_list)
|
36
|
+
|
37
|
+
def __delete_session(auth_provider_name: str, thread_id: str = "default", profile: str = "default") -> str:
|
38
|
+
"""
|
39
|
+
This tool deletes a saved session for a specified authentication provider in the given thread and profile.
|
40
|
+
|
41
|
+
The tool should only be called when a user explicitly requests to delete a session.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
- auth_provider_name (str): The name of the authentication provider for the session to be deleted.
|
45
|
+
- thread_id (str): Thread ID
|
46
|
+
- profile (str): Profile
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
- str: A flag indicating whether the session was successfully deleted.
|
50
|
+
|
51
|
+
This tool should only be used in response to a user's explicit intent to manage their sessions. Automatic or unauthorized invocation is strictly prohibited.
|
52
|
+
"""
|
53
|
+
|
54
|
+
auth_provider = AuthProvider.get_auth_provider(auth_provider_name)
|
55
|
+
is_deleted = pocket_auth.delete_session(auth_provider, thread_id, profile)
|
56
|
+
return str(is_deleted)
|
57
|
+
|
58
|
+
builtin_tools = [
|
59
|
+
from_func(func=__get_current_thread_session_state),
|
60
|
+
from_func(func=__delete_session),
|
61
|
+
]
|
62
|
+
|
63
|
+
return builtin_tools
|
hyperpocket/cli/__main__.py
CHANGED
@@ -1,12 +1,24 @@
|
|
1
1
|
import click
|
2
2
|
|
3
3
|
from hyperpocket.cli.pull import pull
|
4
|
+
from hyperpocket.cli.sync import sync
|
5
|
+
from hyperpocket.cli.eject import eject
|
6
|
+
from hyperpocket.cli.auth import create_token_auth_template
|
4
7
|
|
5
8
|
|
6
9
|
@click.group()
|
7
10
|
def cli():
|
8
11
|
pass
|
9
12
|
|
13
|
+
@click.group()
|
14
|
+
def devtool():
|
15
|
+
pass
|
16
|
+
|
17
|
+
cli.add_command(devtool)
|
18
|
+
devtool.add_command(create_token_auth_template)
|
19
|
+
|
10
20
|
cli.add_command(pull)
|
21
|
+
cli.add_command(sync)
|
22
|
+
cli.add_command(eject)
|
11
23
|
|
12
24
|
cli()
|