Spaces:
Sleeping
Sleeping
Commit ·
8a2fb1b
1
Parent(s): db8667c
apply changes
Browse files
src/app/api/explore/tickets/route.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Explore Tickets API Route (Renamed from Discover Issues)
|
| 3 |
+
*
|
| 4 |
+
* GET /api/explore/tickets
|
| 5 |
+
* Search GitHub for open source issues using authenticated requests
|
| 6 |
+
* to avoid rate limits
|
| 7 |
+
*
|
| 8 |
+
* RENAMED TO AVOID ADBLOCKER DETECTION
|
| 9 |
+
* Old: /api/discover/issues
|
| 10 |
+
* New: /api/explore/tickets
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
import { NextRequest, NextResponse } from "next/server";
|
| 14 |
+
import { getCurrentUser } from "@/lib/auth";
|
| 15 |
+
import { Octokit } from "@octokit/rest";
|
| 16 |
+
|
| 17 |
+
export async function GET(request: NextRequest) {
|
| 18 |
+
try {
|
| 19 |
+
const user = await getCurrentUser(request);
|
| 20 |
+
|
| 21 |
+
const { searchParams } = new URL(request.url);
|
| 22 |
+
const labels = searchParams.get("labels") || "good first issue";
|
| 23 |
+
const language = searchParams.get("language") || "";
|
| 24 |
+
const sort = searchParams.get("sort") || "created";
|
| 25 |
+
const perPage = parseInt(searchParams.get("per_page") || "30");
|
| 26 |
+
|
| 27 |
+
// Use user's GitHub token if available, otherwise use app token
|
| 28 |
+
const token = user?.githubAccessToken || process.env.GITHUB_TOKEN;
|
| 29 |
+
|
| 30 |
+
if (!token) {
|
| 31 |
+
return NextResponse.json({
|
| 32 |
+
error: "No GitHub token available",
|
| 33 |
+
items: [],
|
| 34 |
+
total_count: 0
|
| 35 |
+
}, { status: 200 });
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
const octokit = new Octokit({ auth: token });
|
| 39 |
+
|
| 40 |
+
// Build search query
|
| 41 |
+
let query = "is:issue is:open";
|
| 42 |
+
|
| 43 |
+
// Add labels (support multiple labels)
|
| 44 |
+
const labelList = labels.split(",").filter(l => l.trim());
|
| 45 |
+
for (const label of labelList) {
|
| 46 |
+
query += ` label:"${label.trim()}"`;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Add language filter
|
| 50 |
+
if (language && language !== "all") {
|
| 51 |
+
query += ` language:${language}`;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
// Map sort options
|
| 55 |
+
const sortMap: Record<string, "created" | "updated" | "comments" | "reactions-+1"> = {
|
| 56 |
+
created: "created",
|
| 57 |
+
updated: "updated",
|
| 58 |
+
comments: "comments",
|
| 59 |
+
reactions: "reactions-+1"
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const response = await octokit.search.issuesAndPullRequests({
|
| 63 |
+
q: query,
|
| 64 |
+
sort: sortMap[sort] || "created",
|
| 65 |
+
order: "desc",
|
| 66 |
+
per_page: Math.min(perPage, 100),
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
// Add rate limit info to response
|
| 70 |
+
const rateLimit = {
|
| 71 |
+
remaining: response.headers["x-ratelimit-remaining"],
|
| 72 |
+
limit: response.headers["x-ratelimit-limit"],
|
| 73 |
+
reset: response.headers["x-ratelimit-reset"],
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
return NextResponse.json({
|
| 77 |
+
items: response.data.items,
|
| 78 |
+
total_count: response.data.total_count,
|
| 79 |
+
rate_limit: rateLimit,
|
| 80 |
+
});
|
| 81 |
+
|
| 82 |
+
} catch (error: any) {
|
| 83 |
+
console.error("Explore tickets error:", error);
|
| 84 |
+
|
| 85 |
+
// Handle rate limit specifically
|
| 86 |
+
if (error.status === 403) {
|
| 87 |
+
return NextResponse.json({
|
| 88 |
+
error: "Rate limit exceeded. Please try again later.",
|
| 89 |
+
items: [],
|
| 90 |
+
total_count: 0,
|
| 91 |
+
rate_limited: true
|
| 92 |
+
}, { status: 200 }); // Return 200 so frontend handles gracefully
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
return NextResponse.json({
|
| 96 |
+
error: error.message || "Failed to fetch tickets",
|
| 97 |
+
items: [],
|
| 98 |
+
total_count: 0
|
| 99 |
+
}, { status: 200 });
|
| 100 |
+
}
|
| 101 |
+
}
|
src/app/api/realtime/connect/route.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Real-time Connection API Route (Renamed from WebSocket)
|
| 3 |
+
*
|
| 4 |
+
* GET /api/realtime/connect
|
| 5 |
+
* Establishes a Server-Sent Events (SSE) connection for real-time message delivery
|
| 6 |
+
*
|
| 7 |
+
* RENAMED TO AVOID ADBLOCKER DETECTION
|
| 8 |
+
* Old: /api/messaging/ws
|
| 9 |
+
* New: /api/realtime/connect
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
import { NextRequest } from 'next/server';
|
| 13 |
+
import { getCurrentUser } from '@/lib/auth';
|
| 14 |
+
import { realtimeMessaging } from '@/lib/realtime-messaging';
|
| 15 |
+
|
| 16 |
+
interface WebSocketMessage {
|
| 17 |
+
type: string;
|
| 18 |
+
[key: string]: any;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export async function GET(request: NextRequest) {
|
| 22 |
+
try {
|
| 23 |
+
const user = await getCurrentUser(request);
|
| 24 |
+
if (!user) {
|
| 25 |
+
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
| 26 |
+
status: 401,
|
| 27 |
+
headers: { 'Content-Type': 'application/json' },
|
| 28 |
+
});
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
// Setup SSE connection
|
| 32 |
+
const encoder = new TextEncoder();
|
| 33 |
+
let isConnected = true;
|
| 34 |
+
|
| 35 |
+
const customReadable = new ReadableStream({
|
| 36 |
+
start(controller) {
|
| 37 |
+
// Send initial connection message
|
| 38 |
+
const message = `data: ${JSON.stringify({
|
| 39 |
+
type: 'connected',
|
| 40 |
+
userId: user.id,
|
| 41 |
+
timestamp: new Date().toISOString(),
|
| 42 |
+
})}\n\n`;
|
| 43 |
+
|
| 44 |
+
controller.enqueue(encoder.encode(message));
|
| 45 |
+
|
| 46 |
+
// Register this connection with the realtime service
|
| 47 |
+
const unsubscribe = realtimeMessaging.registerConnection(user.id, (event) => {
|
| 48 |
+
if (isConnected) {
|
| 49 |
+
const eventMessage = `data: ${JSON.stringify(event)}\n\n`;
|
| 50 |
+
try {
|
| 51 |
+
controller.enqueue(encoder.encode(eventMessage));
|
| 52 |
+
} catch (error) {
|
| 53 |
+
console.error('Error sending SSE event:', error);
|
| 54 |
+
isConnected = false;
|
| 55 |
+
controller.close();
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
});
|
| 59 |
+
|
| 60 |
+
// Cleanup on connection close
|
| 61 |
+
request.signal?.addEventListener('abort', () => {
|
| 62 |
+
isConnected = false;
|
| 63 |
+
unsubscribe();
|
| 64 |
+
controller.close();
|
| 65 |
+
});
|
| 66 |
+
},
|
| 67 |
+
});
|
| 68 |
+
|
| 69 |
+
return new Response(customReadable, {
|
| 70 |
+
headers: {
|
| 71 |
+
'Content-Type': 'text/event-stream',
|
| 72 |
+
'Cache-Control': 'no-cache',
|
| 73 |
+
'Connection': 'keep-alive',
|
| 74 |
+
'X-Accel-Buffering': 'no',
|
| 75 |
+
},
|
| 76 |
+
});
|
| 77 |
+
} catch (error) {
|
| 78 |
+
console.error('Real-time connection error:', error);
|
| 79 |
+
return new Response(JSON.stringify({ error: 'Internal server error' }), {
|
| 80 |
+
status: 500,
|
| 81 |
+
headers: { 'Content-Type': 'application/json' },
|
| 82 |
+
});
|
| 83 |
+
}
|
| 84 |
+
}
|