Compare commits
3 Commits
extension-
...
extension-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b7f308528 | ||
|
|
95ee3fa61d | ||
|
|
d82d59cbe4 |
@@ -42,5 +42,17 @@
|
|||||||
},
|
},
|
||||||
"description": "Toggle InsightReply Sidebar"
|
"description": "Toggle InsightReply Sidebar"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
{
|
||||||
|
"resources": [
|
||||||
|
"src/assets/*",
|
||||||
|
"assets/*"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"https://twitter.com/*",
|
||||||
|
"https://x.com/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,13 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, inject, computed, type Ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
// Injected reactive refs from sidebar-mount.ts
|
||||||
tweetData?: {
|
const currentTweetData = inject<Ref<any>>('currentTweetData', ref(null))
|
||||||
id: string;
|
const isVisible = inject<Ref<boolean>>('sidebarVisible', ref(false))
|
||||||
author: string;
|
|
||||||
content: string;
|
// Computed getter for template convenience
|
||||||
stats: {
|
const tweetData = computed(() => currentTweetData.value)
|
||||||
replies: string;
|
|
||||||
retweets: string;
|
|
||||||
likes: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const isVisible = ref(false) // Start hidden, wait for trigger
|
|
||||||
const selectedStrategy = ref('Insightful')
|
const selectedStrategy = ref('Insightful')
|
||||||
const generatedReplies = ref<Array<{strategy: string, content: string}>>([])
|
const generatedReplies = ref<Array<{strategy: string, content: string}>>([])
|
||||||
const isGenerating = ref(false)
|
const isGenerating = ref(false)
|
||||||
@@ -29,30 +22,21 @@ const defaultStrategies = [
|
|||||||
|
|
||||||
const strategies = ref([...defaultStrategies])
|
const strategies = ref([...defaultStrategies])
|
||||||
|
|
||||||
onMounted(() => {
|
// Fetch custom strategies on mount
|
||||||
// Listen for toggle messages directly in the component
|
chrome.runtime.sendMessage({ type: 'FETCH_CUSTOM_STRATEGIES' }, (response) => {
|
||||||
chrome.runtime.onMessage.addListener((message) => {
|
if (response && response.success && response.data) {
|
||||||
if (message.type === 'TOGGLE_SIDEBAR') {
|
const customStrategies = response.data.map((s: any) => ({
|
||||||
isVisible.value = !isVisible.value
|
id: s.strategy_key,
|
||||||
} else if (message.type === 'SHOW_INSIGHT') {
|
label: s.label,
|
||||||
isVisible.value = true
|
icon: '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>'
|
||||||
}
|
}))
|
||||||
})
|
strategies.value = [...defaultStrategies, ...customStrategies]
|
||||||
|
}
|
||||||
chrome.runtime.sendMessage({ type: 'FETCH_CUSTOM_STRATEGIES' }, (response) => {
|
|
||||||
if (response && response.success && response.data) {
|
|
||||||
const customStrategies = response.data.map((s: any) => ({
|
|
||||||
id: s.strategy_key,
|
|
||||||
label: s.label,
|
|
||||||
icon: '<path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z"/>'
|
|
||||||
}))
|
|
||||||
strategies.value = [...defaultStrategies, ...customStrategies]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const generate = () => {
|
const generate = () => {
|
||||||
if (!props.tweetData) return
|
if (!tweetData.value) return
|
||||||
|
|
||||||
isGenerating.value = true
|
isGenerating.value = true
|
||||||
generatedReplies.value = []
|
generatedReplies.value = []
|
||||||
@@ -68,7 +52,7 @@ const generate = () => {
|
|||||||
chrome.runtime.sendMessage({
|
chrome.runtime.sendMessage({
|
||||||
type: 'GENERATE_REPLY',
|
type: 'GENERATE_REPLY',
|
||||||
payload: {
|
payload: {
|
||||||
tweetContent: props.tweetData!.content,
|
tweetContent: tweetData.value!.content,
|
||||||
strategy: selectedStrategy.value,
|
strategy: selectedStrategy.value,
|
||||||
identity: finalIdentity
|
identity: finalIdentity
|
||||||
}
|
}
|
||||||
@@ -93,7 +77,7 @@ const copyToClipboard = async (reply: any) => {
|
|||||||
showProfileTip.value = true
|
showProfileTip.value = true
|
||||||
setTimeout(() => { showProfileTip.value = false }, 7000)
|
setTimeout(() => { showProfileTip.value = false }, 7000)
|
||||||
|
|
||||||
if (!props.tweetData || !props.tweetData.id) return;
|
if (!tweetData.value || !tweetData.value.id) return;
|
||||||
|
|
||||||
chrome.storage.local.get(['jwt_token'], async (result) => {
|
chrome.storage.local.get(['jwt_token'], async (result) => {
|
||||||
if (result.jwt_token) {
|
if (result.jwt_token) {
|
||||||
@@ -107,7 +91,7 @@ const copyToClipboard = async (reply: any) => {
|
|||||||
'Authorization': `Bearer ${token}`
|
'Authorization': `Bearer ${token}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
tweet_id: props.tweetData?.id,
|
tweet_id: tweetData.value?.id,
|
||||||
strategy_type: reply.strategy || 'General',
|
strategy_type: reply.strategy || 'General',
|
||||||
content: reply.content,
|
content: reply.content,
|
||||||
language: 'en'
|
language: 'en'
|
||||||
@@ -123,28 +107,28 @@ const copyToClipboard = async (reply: any) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<transition
|
<transition
|
||||||
enter-active-class="transition duration-500 ease-out transform"
|
enter-active-class="transition-transform duration-500 ease-[cubic-bezier(0.2,0.8,0.2,1)]"
|
||||||
enter-from-class="translate-x-full opacity-0 scale-95"
|
enter-from-class="translate-x-full"
|
||||||
enter-to-class="translate-x-0 opacity-100 scale-100"
|
enter-to-class="translate-x-0"
|
||||||
leave-active-class="transition duration-400 ease-in transform"
|
leave-active-class="transition-transform duration-400 ease-[cubic-bezier(0.2,0.8,0.2,1)]"
|
||||||
leave-from-class="translate-x-0 opacity-100 scale-100"
|
leave-from-class="translate-x-0"
|
||||||
leave-to-class="translate-x-full opacity-0 scale-95"
|
leave-to-class="translate-x-full"
|
||||||
>
|
>
|
||||||
<div v-if="isVisible" class="fixed right-6 top-1/2 -translate-y-1/2 w-[380px] max-h-[90vh] flex flex-col glass-card rounded-[24px] shadow-[0_32px_64px_rgba(0,0,0,0.5)] text-[#f8fafc] font-sans z-[2147483647] overflow-hidden border border-white/5 selection:bg-rose-500/30">
|
<div v-if="isVisible" class="fixed right-0 top-0 bottom-0 w-[400px] flex flex-col bg-zinc-950/90 backdrop-blur-[40px] border-l border-white/10 shadow-[-10px_0_40px_rgba(0,0,0,0.5)] text-[#f8fafc] font-sans z-[2147483647] overflow-hidden selection:bg-rose-500/30">
|
||||||
|
|
||||||
<!-- Premium Header -->
|
<!-- Premium Header -->
|
||||||
<div class="px-6 py-5 flex justify-between items-center bg-white/[0.02] border-b border-white/5">
|
<div class="px-6 py-5 flex justify-between items-center bg-white/[0.02] border-b border-white/5 shrink-0">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="p-2 rounded-lg bg-rose-500/10 border border-rose-500/20">
|
<div class="p-2 rounded-xl bg-gradient-to-br from-rose-500 to-rose-600 shadow-lg shadow-rose-900/20">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="text-rose-500"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="text-white"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="text-sm font-bold tracking-tight block">InsightReply</span>
|
<span class="text-[15px] font-black tracking-tight block text-white drop-shadow-sm">InsightReply</span>
|
||||||
<span class="text-[9px] text-zinc-500 uppercase tracking-[0.2em] font-bold">Spatial Copilot</span>
|
<span class="text-[9px] text-zinc-400 uppercase tracking-[0.2em] font-bold">Spatial Copilot</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button @click="isVisible = false" class="p-2 rounded-full hover:bg-white/10 transition-apple group">
|
<button @click="isVisible = false" class="p-2.5 rounded-full bg-white/5 hover:bg-rose-500/20 hover:text-rose-400 border border-white/5 transition-apple group">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-zinc-500 group-hover:text-white transition-apple"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="text-zinc-400 group-hover:text-rose-400 transition-apple"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp, ref } from 'vue'
|
||||||
import Sidebar from './Sidebar.vue'
|
import Sidebar from './Sidebar.vue'
|
||||||
import '../assets/tailwind.css' // We might need to handle this specially for Shadow DOM
|
|
||||||
|
|
||||||
const MOUNT_ID = 'insight-reply-sidebar-root'
|
const MOUNT_ID = 'insight-reply-sidebar-root'
|
||||||
|
|
||||||
function initSidebar(tweetData?: any) {
|
// Shared reactive tweet data that the Sidebar component will receive
|
||||||
let host = document.getElementById(MOUNT_ID)
|
const currentTweetData = ref<any>(null)
|
||||||
|
const sidebarVisible = ref(false)
|
||||||
|
|
||||||
if (host) return;
|
let isMounted = false
|
||||||
|
let isMounting = false
|
||||||
|
|
||||||
|
async function initSidebar() {
|
||||||
|
if (isMounted || isMounting) return
|
||||||
|
|
||||||
|
let host = document.getElementById(MOUNT_ID)
|
||||||
|
if (host) return
|
||||||
|
|
||||||
|
isMounting = true
|
||||||
|
|
||||||
// 1. Create Host Element
|
// 1. Create Host Element
|
||||||
host = document.createElement('div')
|
host = document.createElement('div')
|
||||||
@@ -39,26 +48,85 @@ function initSidebar(tweetData?: any) {
|
|||||||
`
|
`
|
||||||
shadowRoot.appendChild(container)
|
shadowRoot.appendChild(container)
|
||||||
|
|
||||||
// 4. Inject Styles
|
// 4. Inject Styles (Bypassing X.com CSP via fetch)
|
||||||
const injectStyles = () => {
|
const injectStyles = async () => {
|
||||||
const styles = document.querySelectorAll('style, link[rel="stylesheet"]')
|
try {
|
||||||
styles.forEach(style => {
|
// Check for Vite dev mode styles first
|
||||||
shadowRoot.appendChild(style.cloneNode(true))
|
const devStyles = document.querySelectorAll('style[data-vite-dev-id]')
|
||||||
})
|
if (devStyles.length > 0) {
|
||||||
}
|
devStyles.forEach(style => shadowRoot.appendChild(style.cloneNode(true)))
|
||||||
injectStyles()
|
console.log('[InsightReply] Injected dev styles into shadow DOM')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Create Vue App
|
// Production mode: fetch the CSS file directly to bypass rigid CSP
|
||||||
const app = createApp(Sidebar, { tweetData })
|
// CRXJS usually puts built CSS in assets/index-[hash].css
|
||||||
|
const cssUrl = chrome.runtime.getURL('assets/index.css')
|
||||||
|
// In Vite dev mode the raw file might be available
|
||||||
|
const devUrl = chrome.runtime.getURL('src/assets/tailwind.css')
|
||||||
|
|
||||||
|
const urlToFetch = chrome.runtime.id.includes('extension') ? cssUrl : devUrl
|
||||||
|
|
||||||
|
const response = await fetch(urlToFetch)
|
||||||
|
if (response.ok) {
|
||||||
|
const cssText = await response.text()
|
||||||
|
const styleEl = document.createElement('style')
|
||||||
|
styleEl.textContent = cssText
|
||||||
|
shadowRoot.appendChild(styleEl)
|
||||||
|
console.log('[InsightReply] Successfully injected fetched CSS into shadow DOM')
|
||||||
|
} else {
|
||||||
|
console.warn('[InsightReply] Failed to fetch CSS file directly, falling back to basic link tag')
|
||||||
|
const linkEl = document.createElement('link')
|
||||||
|
linkEl.rel = 'stylesheet'
|
||||||
|
linkEl.href = chrome.runtime.getURL('src/assets/tailwind.css')
|
||||||
|
shadowRoot.appendChild(linkEl)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[InsightReply] Error injecting styles:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject and wait
|
||||||
|
await injectStyles()
|
||||||
|
|
||||||
|
// 5. Create Vue App with reactive provide
|
||||||
|
const app = createApp(Sidebar)
|
||||||
|
|
||||||
|
isMounting = false
|
||||||
|
app.provide('currentTweetData', currentTweetData)
|
||||||
|
app.provide('sidebarVisible', sidebarVisible)
|
||||||
app.mount(container)
|
app.mount(container)
|
||||||
|
|
||||||
|
isMounted = true
|
||||||
|
console.log('[InsightReply] Sidebar mounted successfully')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure it mounts on load if needed, but primarily triggered by messages
|
function showSidebar(tweetData?: any) {
|
||||||
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
initSidebar()
|
||||||
if (message.type === 'SHOW_INSIGHT' || message.type === 'TOGGLE_SIDEBAR') {
|
|
||||||
initSidebar(message.payload);
|
if (tweetData) {
|
||||||
// The component itself listens for TOGGLE_SIDEBAR to show/hide
|
currentTweetData.value = tweetData
|
||||||
sendResponse({ received: true });
|
console.log('[InsightReply] Tweet data updated:', tweetData.content?.substring(0, 60))
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
});
|
sidebarVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
initSidebar()
|
||||||
|
sidebarVisible.value = !sidebarVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for messages from background script
|
||||||
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
|
console.log('[InsightReply SidebarMount] Received message:', message.type)
|
||||||
|
|
||||||
|
if (message.type === 'SHOW_INSIGHT') {
|
||||||
|
showSidebar(message.payload)
|
||||||
|
sendResponse({ received: true })
|
||||||
|
} else if (message.type === 'TOGGLE_SIDEBAR') {
|
||||||
|
toggleSidebar()
|
||||||
|
sendResponse({ received: true })
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ const loadHotTweets = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (response.status === 429) {
|
||||||
|
error.value = '请求频率达到上限,请稍后再试。Free 用户每日限额 10 次 API 调用。'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Opportunity discovery failed (${response.status})`)
|
throw new Error(`Opportunity discovery failed (${response.status})`)
|
||||||
}
|
}
|
||||||
@@ -46,7 +51,7 @@ const loadHotTweets = async () => {
|
|||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
tweets.value = data || []
|
tweets.value = data || []
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
error.value = err.message || 'Quantum network sync error.'
|
error.value = err.message || 'Network connection error.'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -53,6 +54,7 @@ func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
userID := ctx.Value("userID").(string)
|
userID := ctx.Value("userID").(string)
|
||||||
|
log.Printf("[AIHandler] Generate request from userID=%s strategy=%q provider=%q model=%q", userID, body.Strategy, body.Provider, body.Model)
|
||||||
|
|
||||||
// Fetch Product Profile Context
|
// Fetch Product Profile Context
|
||||||
var productContext string
|
var productContext string
|
||||||
@@ -79,9 +81,11 @@ func (h *AIHandler) Generate(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
replyString, err := h.svc.GenerateReply(ctx, body.TweetContent, productContext, body.Identity, body.Provider, body.Model)
|
replyString, err := h.svc.GenerateReply(ctx, body.TweetContent, productContext, body.Identity, body.Provider, body.Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[AIHandler] ERROR GenerateReply for userID=%s: %v", userID, err)
|
||||||
SendError(w, http.StatusBadGateway, 5002, "Failed to generate AI reply: "+err.Error())
|
SendError(w, http.StatusBadGateway, 5002, "Failed to generate AI reply: "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("[AIHandler] GenerateReply success for userID=%s, reply length=%d", userID, len(replyString))
|
||||||
|
|
||||||
// Clean up potential markdown wrappers from LLM output
|
// Clean up potential markdown wrappers from LLM output
|
||||||
cleanReply := strings.TrimSpace(replyString)
|
cleanReply := strings.TrimSpace(replyString)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/zs/InsightReply/internal/repository"
|
"github.com/zs/InsightReply/internal/repository"
|
||||||
@@ -17,15 +18,18 @@ func NewTweetHandler(repo *repository.TweetRepository) *TweetHandler {
|
|||||||
|
|
||||||
// GetHotTweets returns the top heating tweets spanning across all tracking targets
|
// GetHotTweets returns the top heating tweets spanning across all tracking targets
|
||||||
func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
|
func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
|
||||||
// Standardize to take the top 50 hottest tweets that haven't been manually marked as processed
|
log.Printf("[TweetHandler] GetHotTweets called from %s", r.RemoteAddr)
|
||||||
|
|
||||||
tweets, err := h.repo.GetTopHeatingTweets(50)
|
tweets, err := h.repo.GetTopHeatingTweets(50)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[TweetHandler] ERROR GetTopHeatingTweets: %v", err)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to retrieve hot tweets"})
|
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to retrieve hot tweets"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[TweetHandler] GetHotTweets returning %d tweets", len(tweets))
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(tweets)
|
json.NewEncoder(w).Encode(tweets)
|
||||||
}
|
}
|
||||||
@@ -34,15 +38,18 @@ func (h *TweetHandler) GetHotTweets(w http.ResponseWriter, r *http.Request) {
|
|||||||
func (h *TweetHandler) GetSearchTweets(w http.ResponseWriter, r *http.Request) {
|
func (h *TweetHandler) GetSearchTweets(w http.ResponseWriter, r *http.Request) {
|
||||||
keyword := r.URL.Query().Get("keyword")
|
keyword := r.URL.Query().Get("keyword")
|
||||||
handle := r.URL.Query().Get("handle")
|
handle := r.URL.Query().Get("handle")
|
||||||
|
log.Printf("[TweetHandler] SearchTweets called: keyword=%q handle=%q", keyword, handle)
|
||||||
|
|
||||||
tweets, err := h.repo.SearchTweets(keyword, handle, 50)
|
tweets, err := h.repo.SearchTweets(keyword, handle, 50)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[TweetHandler] ERROR SearchTweets: %v", err)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to search tweets"})
|
json.NewEncoder(w).Encode(map[string]interface{}{"error": "failed to search tweets"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[TweetHandler] SearchTweets returning %d tweets", len(tweets))
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(tweets)
|
json.NewEncoder(w).Encode(tweets)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -49,6 +50,7 @@ func RateLimit(db *gorm.DB) func(http.Handler) http.Handler {
|
|||||||
// For now, fallback to generic tight limit for anonymous usage
|
// For now, fallback to generic tight limit for anonymous usage
|
||||||
ipLimiter := getLimiter(r.RemoteAddr, "Free")
|
ipLimiter := getLimiter(r.RemoteAddr, "Free")
|
||||||
if !ipLimiter.Allow() {
|
if !ipLimiter.Allow() {
|
||||||
|
log.Printf("[RateLimit] 429 for anonymous IP=%s path=%s", r.RemoteAddr, r.URL.Path)
|
||||||
http.Error(w, `{"code":429, "message":"Too Many Requests: Rate limit exceeded"}`, http.StatusTooManyRequests)
|
http.Error(w, `{"code":429, "message":"Too Many Requests: Rate limit exceeded"}`, http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -72,6 +74,7 @@ func RateLimit(db *gorm.DB) func(http.Handler) http.Handler {
|
|||||||
|
|
||||||
limiter := getLimiter(userID, tier)
|
limiter := getLimiter(userID, tier)
|
||||||
if !limiter.Allow() {
|
if !limiter.Allow() {
|
||||||
|
log.Printf("[RateLimit] 429 for userID=%s tier=%s path=%s", userID, tier, r.URL.Path)
|
||||||
http.Error(w, `{"code":429, "message":"Too Many Requests: Daily quota or rate limit exceeded"}`, http.StatusTooManyRequests)
|
http.Error(w, `{"code":429, "message":"Too Many Requests: Daily quota or rate limit exceeded"}`, http.StatusTooManyRequests)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user