Migrate from discord to zulip
This commit is contained in:
@@ -67,8 +67,11 @@ EVE_SSO_CLIENT_ID=Client ID
|
|||||||
EVE_SSO_SECRET=Secret Key
|
EVE_SSO_SECRET=Secret Key
|
||||||
EVE_SSO_CALLBACK_URL=Callback URL (This should be the domain you are hosting at or if run locally it should be http://localhost:3000)
|
EVE_SSO_CALLBACK_URL=Callback URL (This should be the domain you are hosting at or if run locally it should be http://localhost:3000)
|
||||||
|
|
||||||
# Webhook Configuration (optional)
|
# Zulip Configuration (optional)
|
||||||
WEBHOOK_URL=Discord webhook URL for notifications
|
ZULIP_URL=https://zulip.site.quack-lab.dev/api/v1/messages
|
||||||
|
ZULIP_EMAIL=evepi-bot@zulip.site.quack-lab.dev
|
||||||
|
ZULIP_API_KEY=9LlHp6flAGWF0x5KZnyMJTM3qm4CiHkn
|
||||||
|
ZULIP_STREAM=EvePI
|
||||||
```
|
```
|
||||||
|
|
||||||
## Run locally
|
## Run locally
|
||||||
|
|||||||
@@ -170,23 +170,66 @@ export const SettingsButton = () => {
|
|||||||
<Divider sx={{ mb: 2 }} />
|
<Divider sx={{ mb: 2 }} />
|
||||||
<Typography variant="subtitle1">Webhook Notifications</Typography>
|
<Typography variant="subtitle1">Webhook Notifications</Typography>
|
||||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||||
Configure alerts for extractor expiry and storage capacity warnings.
|
Configure Zulip alerts for extractor expiry and storage capacity warnings.
|
||||||
{!webhookServerEnabled && " (Server webhook support not available - WEBHOOK_URL not configured)"}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={
|
control={
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={webhookConfig.enabled && webhookServerEnabled}
|
checked={webhookConfig.enabled}
|
||||||
onChange={handleWebhookEnabledChange}
|
onChange={handleWebhookEnabledChange}
|
||||||
disabled={!webhookServerEnabled}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Enable webhook notifications"
|
label="Enable webhook notifications"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{webhookConfig.enabled && webhookServerEnabled && (
|
{webhookConfig.enabled && (
|
||||||
<>
|
<>
|
||||||
|
<Typography variant="subtitle2" sx={{ mt: 2, mb: 1 }}>Zulip Configuration</Typography>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Zulip URL"
|
||||||
|
value={webhookConfig.zulipUrl || ''}
|
||||||
|
onChange={(e) => setWebhookConfig(prev => ({ ...prev, zulipUrl: e.target.value }))}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
placeholder="https://zulip.site.quack-lab.dev/api/v1/messages"
|
||||||
|
helperText="Zulip API endpoint URL"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Zulip Email"
|
||||||
|
value={webhookConfig.zulipEmail || ''}
|
||||||
|
onChange={(e) => setWebhookConfig(prev => ({ ...prev, zulipEmail: e.target.value }))}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
placeholder="evepi-bot@zulip.site.quack-lab.dev"
|
||||||
|
helperText="Bot email for authentication"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Zulip API Key"
|
||||||
|
type="password"
|
||||||
|
value={webhookConfig.zulipApiKey || ''}
|
||||||
|
onChange={(e) => setWebhookConfig(prev => ({ ...prev, zulipApiKey: e.target.value }))}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
placeholder="9LlHp6flAGWF0x5KZnyMJTM3qm4CiHkn"
|
||||||
|
helperText="API key for authentication"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Zulip Stream"
|
||||||
|
value={webhookConfig.zulipStream || ''}
|
||||||
|
onChange={(e) => setWebhookConfig(prev => ({ ...prev, zulipStream: e.target.value }))}
|
||||||
|
fullWidth
|
||||||
|
margin="normal"
|
||||||
|
placeholder="EvePI"
|
||||||
|
helperText="Stream name for messages"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography variant="subtitle2" sx={{ mt: 3, mb: 1 }}>Alert Thresholds</Typography>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
label="Extractor Expiry Warning"
|
label="Extractor Expiry Warning"
|
||||||
value={webhookConfig.expiryThreshold}
|
value={webhookConfig.expiryThreshold}
|
||||||
@@ -219,10 +262,33 @@ export const SettingsButton = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
const { notificationService } = require("@/utils/notificationService");
|
try {
|
||||||
const characters = JSON.parse(localStorage.getItem("characters") || "[]");
|
const response = await fetch("/api/zulip-webhook", {
|
||||||
notificationService.checkAndNotify(characters, webhookConfig);
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
type: 'test',
|
||||||
|
message: 'Test webhook from EvePI',
|
||||||
|
characterName: 'Test Character',
|
||||||
|
planetName: 'Test Planet',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
zulipUrl: webhookConfig.zulipUrl,
|
||||||
|
zulipEmail: webhookConfig.zulipEmail,
|
||||||
|
zulipApiKey: webhookConfig.zulipApiKey,
|
||||||
|
zulipStream: webhookConfig.zulipStream
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert("✅ Test webhook sent successfully!");
|
||||||
|
} else {
|
||||||
|
const error = await response.text();
|
||||||
|
alert(`❌ Test webhook failed: ${error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(`❌ Error testing webhook: ${error}`);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
sx={{ mt: 2 }}
|
sx={{ mt: 2 }}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { WebhookPayload } from '@/types/webhook';
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
) {
|
|
||||||
if (req.method !== 'POST') {
|
|
||||||
return res.status(405).json({ error: 'Method not allowed' });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const payload: WebhookPayload = req.body;
|
|
||||||
|
|
||||||
// Validate payload
|
|
||||||
if (!payload.type || !payload.message || !payload.characterName || !payload.planetName) {
|
|
||||||
return res.status(400).json({ error: 'Invalid payload' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const webhookUrl = process.env.WEBHOOK_URL;
|
|
||||||
|
|
||||||
if (!webhookUrl) {
|
|
||||||
// If no external webhook URL is configured, just log the payload for testing
|
|
||||||
console.log('🔔 Webhook payload (no external URL configured):', JSON.stringify(payload, null, 2));
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: 'Webhook logged to console (no external URL configured)',
|
|
||||||
payload
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send webhook to external URL
|
|
||||||
console.log('🔔 Sending webhook to external URL:', webhookUrl);
|
|
||||||
|
|
||||||
// Use simple format with all the details
|
|
||||||
const webhookPayload = {
|
|
||||||
content: `${payload.message}\n**Character:** ${payload.characterName}\n**Planet:** ${payload.planetName}\n**Time:** ${new Date(payload.timestamp).toLocaleString()}`
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('📤 Sending payload:', JSON.stringify(webhookPayload, null, 2));
|
|
||||||
|
|
||||||
const response = await fetch(webhookUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(webhookPayload),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorText = await response.text();
|
|
||||||
console.error('❌ Webhook failed:', response.status, response.statusText, errorText);
|
|
||||||
throw new Error(`Webhook failed with status ${response.status}: ${errorText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Webhook sent successfully to external URL');
|
|
||||||
res.status(200).json({ success: true, message: 'Webhook sent to external URL' });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Webhook error:', error);
|
|
||||||
res.status(500).json({ error: 'Failed to send webhook', details: (error as Error).message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
122
src/pages/api/zulip-webhook.ts
Normal file
122
src/pages/api/zulip-webhook.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse
|
||||||
|
) {
|
||||||
|
if (req.method !== 'POST') {
|
||||||
|
return res.status(405).json({ error: 'Method not allowed' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = req.body;
|
||||||
|
|
||||||
|
// Check if this is a test request
|
||||||
|
if (payload.type === 'test') {
|
||||||
|
const missing = [];
|
||||||
|
if (!payload.zulipUrl) missing.push('zulipUrl');
|
||||||
|
if (!payload.zulipEmail) missing.push('zulipEmail');
|
||||||
|
if (!payload.zulipApiKey) missing.push('zulipApiKey');
|
||||||
|
if (!payload.zulipStream) missing.push('zulipStream');
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
return res.status(400).json({ error: `Missing required Zulip configuration: ${missing.join(', ')}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const zulipUrl = payload.zulipUrl;
|
||||||
|
const zulipEmail = payload.zulipEmail;
|
||||||
|
const zulipApiKey = payload.zulipApiKey;
|
||||||
|
const zulipStream = payload.zulipStream;
|
||||||
|
|
||||||
|
const topic = 'Test-Webhook-Test';
|
||||||
|
const content = '🧪 **Test Webhook**\nThis is a test message from EvePI to verify Zulip integration is working correctly.\n\n**Time:** ' + new Date().toLocaleString();
|
||||||
|
|
||||||
|
const zulipPayload = new URLSearchParams();
|
||||||
|
zulipPayload.append('type', 'stream');
|
||||||
|
zulipPayload.append('to', zulipStream);
|
||||||
|
zulipPayload.append('topic', topic);
|
||||||
|
zulipPayload.append('content', content);
|
||||||
|
|
||||||
|
const auth = Buffer.from(`${zulipEmail}:${zulipApiKey}`).toString('base64');
|
||||||
|
|
||||||
|
const response = await fetch(zulipUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': `Basic ${auth}`
|
||||||
|
},
|
||||||
|
body: zulipPayload.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Zulip test webhook failed',
|
||||||
|
details: errorText,
|
||||||
|
status: response.status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.text();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: 'Test webhook sent successfully to Zulip',
|
||||||
|
result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate payload for normal webhook
|
||||||
|
if (!payload.type || !payload.message || !payload.characterName || !payload.planetName) {
|
||||||
|
return res.status(400).json({ error: 'Invalid payload' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const zulipUrl = payload.zulipUrl;
|
||||||
|
const zulipEmail = payload.zulipEmail;
|
||||||
|
const zulipApiKey = payload.zulipApiKey;
|
||||||
|
const zulipStream = payload.zulipStream;
|
||||||
|
|
||||||
|
if (!zulipUrl || !zulipEmail || !zulipApiKey || !zulipStream) {
|
||||||
|
return res.status(400).json({ error: 'Missing required Zulip configuration' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create topic in format: CharacterName-PlanetName-ActionType
|
||||||
|
const actionType = payload.type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
const topic = `${payload.characterName}-${payload.planetName}-${actionType}`;
|
||||||
|
|
||||||
|
// Format content with all the details
|
||||||
|
const content = `${payload.message}\n**Character:** ${payload.characterName}\n**Planet:** ${payload.planetName}\n**Time:** ${new Date(payload.timestamp).toLocaleString()}`;
|
||||||
|
|
||||||
|
// Create Zulip API payload
|
||||||
|
const zulipPayload = new URLSearchParams();
|
||||||
|
zulipPayload.append('type', 'stream');
|
||||||
|
zulipPayload.append('to', zulipStream);
|
||||||
|
zulipPayload.append('topic', topic);
|
||||||
|
zulipPayload.append('content', content);
|
||||||
|
|
||||||
|
console.log('🔔 Sending webhook to Zulip:', zulipUrl);
|
||||||
|
console.log('📤 Topic:', topic);
|
||||||
|
console.log('📤 Content:', content);
|
||||||
|
|
||||||
|
const response = await fetch(zulipUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': `Basic ${Buffer.from(`${zulipEmail}:${zulipApiKey}`).toString('base64')}`
|
||||||
|
},
|
||||||
|
body: zulipPayload.toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
console.error('❌ Zulip webhook failed:', response.status, response.statusText, errorText);
|
||||||
|
throw new Error(`Zulip webhook failed with status ${response.status}: ${errorText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Webhook sent successfully to Zulip');
|
||||||
|
res.status(200).json({ success: true, message: 'Webhook sent to Zulip' });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Webhook error:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to send webhook', details: (error as Error).message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,14 @@ export interface WebhookConfig {
|
|||||||
expiryThreshold: string; // ISO 8601 duration format
|
expiryThreshold: string; // ISO 8601 duration format
|
||||||
storageWarningThreshold: number; // percentage
|
storageWarningThreshold: number; // percentage
|
||||||
storageCriticalThreshold: number; // percentage
|
storageCriticalThreshold: number; // percentage
|
||||||
|
zulipUrl?: string;
|
||||||
|
zulipEmail?: string;
|
||||||
|
zulipApiKey?: string;
|
||||||
|
zulipStream?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WebhookPayload {
|
export interface WebhookPayload {
|
||||||
type: 'extractor_expiring' | 'extractor_expired' | 'storage_almost_full' | 'storage_full' | 'launchpad_almost_full' | 'launchpad_full';
|
type: 'extractor_expiring' | 'extractor_expired' | 'storage_almost_full' | 'storage_full' | 'launchpad_almost_full' | 'launchpad_full' | 'test';
|
||||||
message: string;
|
message: string;
|
||||||
characterName: string;
|
characterName: string;
|
||||||
planetName: string;
|
planetName: string;
|
||||||
@@ -18,4 +22,8 @@ export interface WebhookPayload {
|
|||||||
fillPercentage?: number;
|
fillPercentage?: number;
|
||||||
};
|
};
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
zulipUrl?: string;
|
||||||
|
zulipEmail?: string;
|
||||||
|
zulipApiKey?: string;
|
||||||
|
zulipStream?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,25 @@ import { WebhookConfig, WebhookPayload } from '@/types/webhook';
|
|||||||
import { STORAGE_IDS, LAUNCHPAD_IDS, PI_TYPES_MAP, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES } from '@/const';
|
import { STORAGE_IDS, LAUNCHPAD_IDS, PI_TYPES_MAP, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES } from '@/const';
|
||||||
import { shouldSendWebhook, markWebhookSent } from './webhookTracker';
|
import { shouldSendWebhook, markWebhookSent } from './webhookTracker';
|
||||||
|
|
||||||
const sendWebhook = async (payload: WebhookPayload): Promise<boolean> => {
|
const sendWebhook = async (payload: WebhookPayload, config: WebhookConfig): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
console.log('🔔 Sending webhook:', payload);
|
console.log('🔔 Sending webhook:', payload);
|
||||||
|
|
||||||
|
// Include Zulip configuration in the payload
|
||||||
|
const webhookPayload = {
|
||||||
|
...payload,
|
||||||
|
zulipUrl: config.zulipUrl,
|
||||||
|
zulipEmail: config.zulipEmail,
|
||||||
|
zulipApiKey: config.zulipApiKey,
|
||||||
|
zulipStream: config.zulipStream
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch('/api/webhook', {
|
const response = await fetch('/api/webhook', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(webhookPayload),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -75,7 +85,7 @@ export const checkExtractorExpiry = async (
|
|||||||
timestamp: now.toISO()
|
timestamp: now.toISO()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (await sendWebhook(payload)) {
|
if (await sendWebhook(payload, config)) {
|
||||||
markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event);
|
markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,7 +107,7 @@ export const checkExtractorExpiry = async (
|
|||||||
timestamp: now.toISO()
|
timestamp: now.toISO()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (await sendWebhook(payload)) {
|
if (await sendWebhook(payload, config)) {
|
||||||
markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event);
|
markWebhookSent(character.character.characterId, planet.planet_id, extractor.expiry_time, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,7 +155,7 @@ export const checkStorageCapacity = async (
|
|||||||
timestamp: now.toISO()
|
timestamp: now.toISO()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (await sendWebhook(payload)) {
|
if (await sendWebhook(payload, config)) {
|
||||||
markWebhookSent(character.character.characterId, planet.planet_id, webhookType, undefined, storage.type_id);
|
markWebhookSent(character.character.characterId, planet.planet_id, webhookType, undefined, storage.type_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +179,7 @@ export const checkStorageCapacity = async (
|
|||||||
timestamp: now.toISO()
|
timestamp: now.toISO()
|
||||||
};
|
};
|
||||||
|
|
||||||
if (await sendWebhook(payload)) {
|
if (await sendWebhook(payload, config)) {
|
||||||
markWebhookSent(character.character.characterId, planet.planet_id, webhookType, undefined, storage.type_id);
|
markWebhookSent(character.character.characterId, planet.planet_id, webhookType, undefined, storage.type_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user