Skip to content

Commit c665e92

Browse files
authored
feat(connector): implement social platform connectors (#47)
2 parents 0c67e4f + 32dd80c commit c665e92

File tree

19 files changed

+1091
-7
lines changed

19 files changed

+1091
-7
lines changed

.env.example

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,27 @@ SOCIAL__X_API_KEY="your_x_api_key"
162162

163163
# API key for Telegram.
164164
# SOCIAL__TELEGRAM_API_TOKEN=
165+
# SOCIAL__TELEGRAM_BOT_TOKEN=
166+
# SOCIAL__TELEGRAM_CHAT_ID=
167+
168+
# --- discord ---
169+
170+
#SOCIAL__DISCORD_BOT_TOKEN=
171+
#SOCIAL__DISCORD_GUILD_ID=
172+
#SOCIAL__DISCORD_CHANNEL_ID=
173+
174+
# --- slack ---
175+
#SOCIAL__SLACK_BOT_TOKEN=
176+
#SOCIAL__SLACK_CHANNEL_ID=
177+
178+
# --- farcaster ---
179+
# SOCIAL__FARCASTER_API_KEY=
180+
# SOCIAL__FARCASTER_API_URL=
181+
# SOCIAL__FARCASTER_SIGNER_UUID=
182+
183+
# --- GITHUB ---
184+
# SOCIAL__GITHUB_TOKEN =
185+
# SOCIAL__GITHUB_REPO =
165186

166187
# ==============================================================================
167188
# TRUSTED EXECUTION ENVIRONMENT (TEE)

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,15 @@ social = [
5353
"python-telegram-bot>=22.0",
5454
"tweepy>=4.15.0",
5555
"async_lru>=2.0.5",
56+
"slack-sdk>=3.26.2",
57+
"discord.py >=2.3.2",
58+
"httpx >=0.27.0"
5659
]
5760
tee = [
5861
"cryptography>=44.0.2",
5962
"pyjwt>=2.10.1",
6063
"pyopenssl>=25.0.0",
64+
"python-dotenv >=1.0.1"
6165
]
6266

6367
[build-system]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from abc import ABC, abstractmethod
2+
from typing import Any
3+
4+
5+
class SocialConnector(ABC):
6+
"""Abstract base class for all social platform connectors."""
7+
8+
@abstractmethod
9+
def __init__(self) -> None:
10+
"""Initialize connector (optional for future shared setup)."""
11+
12+
@property
13+
@abstractmethod
14+
def platform(self) -> str:
15+
"""The name of the platform (e.g., 'x', 'github', 'discord')."""
16+
17+
@abstractmethod
18+
async def fetch_mentions(self, query: str) -> list[dict[str, Any]]:
19+
"""
20+
Fetch public mentions or messages related to a given query.
21+
22+
Returns a list of dictionaries with at least.
23+
- platform
24+
- content
25+
- author_id
26+
- timestamp
27+
"""
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Discord Connector for Flare AI Kit."""
2+
3+
import asyncio
4+
from typing import Any
5+
6+
from discord import Client, Intents, Message, TextChannel
7+
8+
from flare_ai_kit.config import AppSettings
9+
from flare_ai_kit.social.connector import SocialConnector
10+
11+
12+
class DiscordConnector(SocialConnector):
13+
"""Discord connector implementation."""
14+
15+
def __init__(self) -> None:
16+
"""Initialize the DiscordConnector with API token and channel ID."""
17+
settings = AppSettings().social
18+
self.token: str = (
19+
settings.discord_bot_token.get_secret_value()
20+
if settings.discord_bot_token
21+
else ""
22+
)
23+
self.channel_id: int = int(
24+
settings.discord_channel_id.get_secret_value()
25+
if settings.discord_channel_id
26+
else 0
27+
)
28+
self.client: Client = Client(intents=Intents.default())
29+
self._ready_event: asyncio.Event = asyncio.Event()
30+
self._messages: list[dict[str, Any]] = []
31+
32+
# Explicitly register event handlers
33+
self.client.event(self._on_ready)
34+
self.client.event(self._on_message)
35+
36+
async def _on_ready(self) -> None:
37+
"""Handle bot ready event."""
38+
self._ready_event.set()
39+
40+
async def _on_message(self, message: Message) -> None:
41+
"""Handle new messages."""
42+
if message.author == self.client.user:
43+
return
44+
45+
self._messages.append(
46+
{
47+
"platform": "discord",
48+
"content": message.content,
49+
"author_id": str(message.author.id),
50+
"timestamp": str(message.created_at),
51+
}
52+
)
53+
54+
@property
55+
def platform(self) -> str:
56+
"""Return the platform name."""
57+
return "discord"
58+
59+
async def _start_if_needed(self) -> None:
60+
if not self.client.is_ready():
61+
self._client_task = asyncio.create_task(self.client.start(self.token))
62+
await self._ready_event.wait()
63+
64+
async def fetch_mentions(
65+
self, query: str = "", limit: int = 10
66+
) -> list[dict[str, Any]]:
67+
"""Fetch messages that mention the query."""
68+
await self._start_if_needed()
69+
await asyncio.sleep(1) # let messages collect
70+
71+
results: list[dict[str, Any]] = []
72+
for msg in self._messages:
73+
if query.lower() in msg["content"].lower():
74+
results.append(msg)
75+
if len(results) >= limit:
76+
break
77+
return results
78+
79+
async def post_message(self, content: str) -> dict[str, Any]:
80+
"""Post a message to the Discord channel."""
81+
await self._start_if_needed()
82+
channel = self.client.get_channel(self.channel_id)
83+
if isinstance(channel, TextChannel):
84+
message = await channel.send(content)
85+
return {
86+
"platform": "discord",
87+
"message_id": message.id,
88+
"content": message.content,
89+
}
90+
return {
91+
"platform": "discord",
92+
"message_id": None,
93+
"error": "Channel not found or not a text channel.",
94+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Farcaster Connector for Flare AI Kit."""
2+
3+
from typing import Any
4+
5+
import httpx
6+
7+
from flare_ai_kit.config import AppSettings
8+
from flare_ai_kit.social.connector import SocialConnector
9+
10+
11+
class FarcasterConnector(SocialConnector):
12+
"""Farcaster Connector for Flare AI Kit."""
13+
14+
def __init__(self, client: httpx.AsyncClient | None = None) -> None:
15+
"""Initialize the FarcasterConnector with API key."""
16+
settings = AppSettings().social
17+
self.api_key = (
18+
settings.farcaster_api_key.get_secret_value()
19+
if settings.farcaster_api_key
20+
else ""
21+
)
22+
self.signer_uuid = (
23+
settings.farcaster_signer_uuid.get_secret_value()
24+
if settings.farcaster_signer_uuid
25+
else ""
26+
)
27+
self.fid = (
28+
settings.farcaster_fid.get_secret_value() if settings.farcaster_fid else ""
29+
)
30+
self.api_url = (
31+
settings.farcaster_api_url.get_secret_value()
32+
if settings.farcaster_api_url
33+
else ""
34+
)
35+
self.endpoint = f"{self.api_url}/v2/farcaster/feed/search"
36+
self.client = client or httpx.AsyncClient()
37+
38+
@property
39+
def platform(self) -> str:
40+
"""Return the platform name."""
41+
return "farcaster"
42+
43+
async def fetch_mentions(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
44+
"""Fetch mentions from Farcaster based on a query."""
45+
try:
46+
response = await self.client.get(
47+
self.endpoint,
48+
params={"text": query, "limit": limit},
49+
headers={"api_key": self.api_key},
50+
)
51+
response.raise_for_status()
52+
json_data = response.json() # Already a dict in httpx
53+
casts = json_data.get("casts", [])
54+
55+
return [
56+
{
57+
"platform": self.platform,
58+
"content": cast.get("text", ""),
59+
"author_id": cast.get("author", {}).get("fid", ""),
60+
"timestamp": cast.get("timestamp", ""),
61+
}
62+
for cast in casts
63+
]
64+
except httpx.HTTPError:
65+
return []
66+
67+
async def post_message(self, content: str) -> dict[str, Any]:
68+
"""Post a message to Farcaster."""
69+
try:
70+
response = await self.client.post(
71+
f"{self.api_url}/v2/casts",
72+
headers={
73+
"Content-Type": "application/json",
74+
"x-api-key": self.api_key,
75+
},
76+
json={
77+
"text": content,
78+
"signer_uuid": self.signer_uuid,
79+
},
80+
)
81+
response.raise_for_status()
82+
83+
except httpx.HTTPError as e:
84+
return {"error": str(e)}
85+
else:
86+
return {
87+
"platform": "farcaster",
88+
"content": content,
89+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""GitHub Connector for Flare AI Kit."""
2+
3+
from typing import Any
4+
5+
import httpx
6+
7+
from flare_ai_kit.config import AppSettings
8+
from flare_ai_kit.social.connector import SocialConnector
9+
10+
11+
class GitHubConnector(SocialConnector):
12+
"""GitHub Connector for Flare AI Kit."""
13+
14+
def __init__(self) -> None:
15+
"""Initialize the GitHubConnector with API token and repository."""
16+
settings = AppSettings().social
17+
self.token = (
18+
settings.github_token.get_secret_value() if settings.github_token else ""
19+
)
20+
self.repo = (
21+
settings.github_repo.get_secret_value() if settings.github_repo else ""
22+
)
23+
self.client = httpx.AsyncClient()
24+
self.endpoint = f"https://api.github.com/repos/{self.repo}/issues/comments"
25+
26+
@property
27+
def platform(self) -> str:
28+
"""Return the platform name."""
29+
return "github"
30+
31+
async def fetch_mentions(self, query: str, limit: int = 10) -> list[dict[str, Any]]:
32+
"""Fetch issue comments containing the query."""
33+
try:
34+
response = await self.client.get(
35+
self.endpoint,
36+
headers={"Authorization": f"Bearer {self.token}"},
37+
params={"per_page": limit},
38+
)
39+
response.raise_for_status()
40+
comments = await response.json()
41+
42+
return [
43+
{
44+
"platform": self.platform,
45+
"content": comment.get("body", ""),
46+
"author_id": comment.get("user", {}).get("login", ""),
47+
"timestamp": comment.get("created_at", ""),
48+
}
49+
for comment in comments
50+
if query.lower() in comment.get("body", "").lower()
51+
]
52+
except httpx.HTTPError:
53+
return []
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Slack Connector for Flare AI Kit."""
2+
3+
import logging
4+
from typing import Any
5+
6+
from slack_sdk import WebClient
7+
from slack_sdk.errors import SlackApiError
8+
9+
from flare_ai_kit.config import AppSettings
10+
from flare_ai_kit.social.connector import SocialConnector
11+
12+
logger = logging.getLogger(__name__)
13+
logger.setLevel(logging.INFO)
14+
15+
16+
class SlackConnector(SocialConnector):
17+
"""Slack Connector for Flare AI Kit."""
18+
19+
def __init__(self, client: WebClient | None) -> None:
20+
"""Initialize the SlackConnector with API token and channel ID."""
21+
settings = AppSettings().social
22+
self.token = (
23+
settings.slack_bot_token.get_secret_value()
24+
if settings.slack_bot_token
25+
else ""
26+
)
27+
self.channel_id = (
28+
settings.slack_channel_id.get_secret_value()
29+
if settings.slack_channel_id
30+
else ""
31+
)
32+
self.client: WebClient = client or WebClient(token=self.token)
33+
34+
@property
35+
def platform(self) -> str:
36+
"""Return the platform name."""
37+
return "slack"
38+
39+
async def fetch_mentions(
40+
self, query: str = "", limit: int = 10
41+
) -> list[dict[str, Any]]:
42+
"""Fetch messages from Slack channel that match the query."""
43+
if not self.token or not self.channel_id:
44+
return []
45+
46+
try:
47+
response = self.client.conversations_history( # type: ignore[reportUnknownMemberType]
48+
channel=self.channel_id,
49+
limit=100,
50+
)
51+
messages = response.get("messages", [])
52+
53+
results = [
54+
{
55+
"platform": self.platform,
56+
"content": msg.get("text", ""),
57+
"author_id": msg.get("user", ""),
58+
"timestamp": msg.get("ts", ""),
59+
}
60+
for msg in messages
61+
if query.lower() in msg.get("text", "").lower()
62+
]
63+
64+
return results[-limit:]
65+
except SlackApiError:
66+
logger.exception("Slack connector error: %s")
67+
return []
68+
69+
def post_message(self, content: str) -> dict[str, Any]:
70+
"""Post a message to the Slack channel."""
71+
try:
72+
result = self.client.chat_postMessage( # type: ignore[reportUnknownMemberType]
73+
channel=self.channel_id, text=content
74+
)
75+
return {
76+
"platform": "slack",
77+
"message_ts": result["ts"],
78+
"content": content,
79+
}
80+
except SlackApiError as e:
81+
return {"error": str(e)}

0 commit comments

Comments
 (0)