forked from mito-systems/sol-mem-gen
Implement API to verify tweet data
This commit is contained in:
parent
297a4c7ed8
commit
a7f255a33d
@ -14,3 +14,4 @@ PINATA_GATEWAY=
|
||||
# Change to your website URL
|
||||
# For development: set to http://localhost:3000
|
||||
SITE_URL=https://memes.markto.market
|
||||
ACCOUNT_HANDLE=mark_2_market1
|
||||
|
47
src/app/api/tweet/route.ts
Normal file
47
src/app/api/tweet/route.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { verifySignatureInTweet } from '../../../utils/verifyTweet';
|
||||
|
||||
export async function POST(req: NextRequest): Promise<NextResponse> {
|
||||
try {
|
||||
const { tweetUrl } = await req.json();
|
||||
|
||||
const url = `https://publish.twitter.com/oembed?url=${tweetUrl}&maxwidth=600`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
const { handle, memeUrl, txHash } = extractData(data.html);
|
||||
if (!handle || !memeUrl || !txHash) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Verification failed' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
const isVerified = await verifySignatureInTweet(txHash);
|
||||
const isHandleCorrect = handle === process.env.ACCOUNT_HANDLE;
|
||||
|
||||
return NextResponse.json({isVerified: isVerified && isHandleCorrect})
|
||||
} catch (error) {
|
||||
console.error('Error while verifying tweet:', error)
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to verify tweet' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const extractData = (tweet: string | object) => {
|
||||
const tweetText = typeof tweet === 'string' ? tweet : JSON.stringify(tweet);
|
||||
|
||||
const decodedTweet = tweetText.replace(/'/g, "'").replace(/"/g, '"');
|
||||
|
||||
const urlMatch = decodedTweet.match(/<a href="(https:\/\/t.co\/[^"]+)">/);
|
||||
const txHashMatch = decodedTweet.match(/TX Hash: '([^']+)'/);
|
||||
const handleMatch = decodedTweet.match(/@([A-Za-z0-9_]+)/);
|
||||
|
||||
return {
|
||||
memeUrl: urlMatch ? urlMatch[1] : null,
|
||||
txHash: txHashMatch ? txHashMatch[1].trim() : null,
|
||||
handle: handleMatch ? handleMatch[1] : null,
|
||||
};
|
||||
};
|
@ -4,11 +4,13 @@ import React, { useState } from 'react'
|
||||
import BN from 'bn.js';
|
||||
import Big from 'big.js';
|
||||
|
||||
import TweetUrlForm from './TweetForm';
|
||||
|
||||
interface AIServiceCardProps {
|
||||
title: string
|
||||
description: string
|
||||
isWalletConnected: boolean
|
||||
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }>
|
||||
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, transactionSignature?: string, error?: string }>
|
||||
priceMTM: BN
|
||||
}
|
||||
|
||||
@ -16,6 +18,7 @@ interface GenerationState {
|
||||
loading: boolean
|
||||
processing: boolean
|
||||
imageUrl: string | null
|
||||
transactionSignature: string | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
@ -38,6 +41,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
loading: false,
|
||||
processing: false,
|
||||
imageUrl: null,
|
||||
transactionSignature: null,
|
||||
error: null,
|
||||
})
|
||||
|
||||
@ -67,11 +71,12 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
return
|
||||
}
|
||||
|
||||
if (result.imageUrl) {
|
||||
if (result.imageUrl && result.transactionSignature) {
|
||||
setGenerationState({
|
||||
loading: false,
|
||||
processing: false,
|
||||
imageUrl: result.imageUrl,
|
||||
transactionSignature: result.transactionSignature,
|
||||
error: null,
|
||||
})
|
||||
} else {
|
||||
@ -86,7 +91,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const generateTwitterShareUrl = (imageUrl: string): string => {
|
||||
const generateTwitterShareUrl = (imageUrl: string, transactionSignature: string): string => {
|
||||
const baseUrl = window.location.href;
|
||||
const ipfsImageUrl = imageUrl.split("/ipfs/")[1];
|
||||
const memeUrl = `${baseUrl}/memes/${ipfsImageUrl}`;
|
||||
@ -94,6 +99,17 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
return `https://twitter.com/intent/tweet?text=Check%20out%20this%20meme%20I%20generated!&url=${encodeURIComponent(memeUrl)}`;
|
||||
};
|
||||
|
||||
// const generateTwitterShareUrl = (imageUrl: string, transactionSignature: string): string => {
|
||||
// const baseUrl = window.location.href;
|
||||
// const ipfsImageUrl = imageUrl.split("/ipfs/")[1];
|
||||
// const memeUrl = `${baseUrl}/memes/${ipfsImageUrl}`;
|
||||
|
||||
// // Ensure the entire tweet text is properly URL-encoded
|
||||
// const tweetText = `Check out this meme that I generated! TX Hash: ${transactionSignature} @mark_2_market1`;
|
||||
|
||||
// return `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(memeUrl)}`;
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
|
||||
<div className="p-6">
|
||||
@ -137,7 +153,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{generationState.imageUrl && (
|
||||
{generationState.imageUrl && generationState.transactionSignature && (
|
||||
<div className="mt-4">
|
||||
<img
|
||||
src={generationState.imageUrl}
|
||||
@ -146,7 +162,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
||||
/>
|
||||
<div className="mt-4 text-center">
|
||||
<a
|
||||
href={generateTwitterShareUrl(generationState.imageUrl)}
|
||||
href={generateTwitterShareUrl(generationState.imageUrl, generationState.transactionSignature)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block w-full bg-blue-500 hover:bg-blue-600 text-white font-semibold py-4 px-6 rounded-xl transition-all duration-200 shadow-lg hover:shadow-blue-500/25"
|
||||
|
61
src/components/TweetForm.tsx
Normal file
61
src/components/TweetForm.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
|
||||
import React, { useState } from 'react'
|
||||
|
||||
const TweetUrlForm: React.FC = () => {
|
||||
const [inputText, setInputText] = useState<string>('')
|
||||
|
||||
const handleGenerate = async (): Promise<void> => {
|
||||
try {
|
||||
const response = await fetch('/api/tweet', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ tweetUrl: inputText }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to verify tweet: ${response.statusText}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch price:', error);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full bg-gray-800/50 backdrop-blur-lg rounded-2xl shadow-xl border border-gray-700/50 mb-8">
|
||||
<div className="p-6">
|
||||
<div className="mb-4">
|
||||
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
value={inputText}
|
||||
onChange={(e) => setInputText(e.target.value)}
|
||||
placeholder="Enter the tweet URL here..."
|
||||
className="w-full bg-gray-900/50 text-gray-100 border border-gray-700 rounded-xl p-4
|
||||
placeholder-gray-500 focus:border-green-500 focus:ring-2 focus:ring-green-500/20
|
||||
focus:outline-none min-h-[120px] transition-all duration-200
|
||||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
rows={4}
|
||||
/>
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
className="w-full bg-gradient-to-r from-green-500 to-emerald-500 hover:from-green-600
|
||||
hover:to-emerald-600 text-white font-semibold py-4 px-6 rounded-xl
|
||||
transition-all duration-200 shadow-lg hover:shadow-green-500/25
|
||||
disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-none"
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TweetUrlForm
|
@ -1,13 +1,14 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { Payment } from './entity/Payment';
|
||||
import { Tweet } from './entity/Tweets';
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'sqlite',
|
||||
database: './database.sqlite',
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
entities: [Payment],
|
||||
entities: [Payment, Tweet],
|
||||
migrations: [],
|
||||
subscribers: [],
|
||||
});
|
||||
|
13
src/entity/Tweets.ts
Normal file
13
src/entity/Tweets.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class Tweet {
|
||||
@PrimaryGeneratedColumn()
|
||||
id!: number;
|
||||
|
||||
@Column({ unique: true })
|
||||
url!: string;
|
||||
|
||||
@Column({ unique: true })
|
||||
transactionSignature!: string;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
export interface FluxGenerationResult {
|
||||
imageUrl?: string
|
||||
error?: string
|
||||
transactionSignature?: string
|
||||
}
|
||||
|
||||
export interface FluxModelConfig {
|
||||
@ -58,7 +59,7 @@ export async function generateWithFlux(
|
||||
console.log('Raw Flux response:', data)
|
||||
|
||||
if (data.imageUrl) {
|
||||
return { imageUrl: data.imageUrl }
|
||||
return { imageUrl: data.imageUrl, transactionSignature }
|
||||
} else {
|
||||
console.error('Unexpected response structure:', data)
|
||||
throw new Error('Invalid response format from Flux API')
|
||||
|
25
src/utils/verifyTweet.ts
Normal file
25
src/utils/verifyTweet.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { AppDataSource } from '../data-source';
|
||||
import { Payment } from '../entity/Payment';
|
||||
import { Tweet } from '../entity/Tweets';
|
||||
|
||||
assert(process.env.NEXT_PUBLIC_SOLANA_RPC_URL, 'SOLANA_RPC_URL is required');
|
||||
|
||||
export async function verifySignatureInTweet(transactionSignature: string): Promise<boolean> {
|
||||
const paymentRepository = AppDataSource.getRepository(Payment);
|
||||
const payment = await paymentRepository.findOneBy({ transactionSignature });
|
||||
|
||||
const tweetRepository = AppDataSource.getRepository(Tweet);
|
||||
const tweet = await tweetRepository.findOneBy({ transactionSignature });
|
||||
|
||||
if (!payment) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tweet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
Loading…
Reference in New Issue
Block a user