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
|
# Change to your website URL
|
||||||
# For development: set to http://localhost:3000
|
# For development: set to http://localhost:3000
|
||||||
SITE_URL=https://memes.markto.market
|
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 BN from 'bn.js';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
import TweetUrlForm from './TweetForm';
|
||||||
|
|
||||||
interface AIServiceCardProps {
|
interface AIServiceCardProps {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
isWalletConnected: boolean
|
isWalletConnected: boolean
|
||||||
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, error?: string }>
|
onGenerate: (prompt: string) => Promise<{ imageUrl?: string, transactionSignature?: string, error?: string }>
|
||||||
priceMTM: BN
|
priceMTM: BN
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -16,6 +18,7 @@ interface GenerationState {
|
|||||||
loading: boolean
|
loading: boolean
|
||||||
processing: boolean
|
processing: boolean
|
||||||
imageUrl: string | null
|
imageUrl: string | null
|
||||||
|
transactionSignature: string | null
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +41,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
loading: false,
|
loading: false,
|
||||||
processing: false,
|
processing: false,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
|
transactionSignature: null,
|
||||||
error: null,
|
error: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -67,11 +71,12 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.imageUrl) {
|
if (result.imageUrl && result.transactionSignature) {
|
||||||
setGenerationState({
|
setGenerationState({
|
||||||
loading: false,
|
loading: false,
|
||||||
processing: false,
|
processing: false,
|
||||||
imageUrl: result.imageUrl,
|
imageUrl: result.imageUrl,
|
||||||
|
transactionSignature: result.transactionSignature,
|
||||||
error: null,
|
error: null,
|
||||||
})
|
})
|
||||||
} else {
|
} 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 baseUrl = window.location.href;
|
||||||
const ipfsImageUrl = imageUrl.split("/ipfs/")[1];
|
const ipfsImageUrl = imageUrl.split("/ipfs/")[1];
|
||||||
const memeUrl = `${baseUrl}/memes/${ipfsImageUrl}`;
|
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)}`;
|
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 (
|
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="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="p-6">
|
||||||
@ -137,7 +153,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{generationState.imageUrl && (
|
{generationState.imageUrl && generationState.transactionSignature && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<img
|
<img
|
||||||
src={generationState.imageUrl}
|
src={generationState.imageUrl}
|
||||||
@ -146,7 +162,7 @@ const AIServiceCard: React.FC<AIServiceCardProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div className="mt-4 text-center">
|
<div className="mt-4 text-center">
|
||||||
<a
|
<a
|
||||||
href={generateTwitterShareUrl(generationState.imageUrl)}
|
href={generateTwitterShareUrl(generationState.imageUrl, generationState.transactionSignature)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
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 { DataSource } from 'typeorm';
|
||||||
|
|
||||||
import { Payment } from './entity/Payment';
|
import { Payment } from './entity/Payment';
|
||||||
|
import { Tweet } from './entity/Tweets';
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource({
|
||||||
type: 'sqlite',
|
type: 'sqlite',
|
||||||
database: './database.sqlite',
|
database: './database.sqlite',
|
||||||
synchronize: true,
|
synchronize: true,
|
||||||
logging: false,
|
logging: false,
|
||||||
entities: [Payment],
|
entities: [Payment, Tweet],
|
||||||
migrations: [],
|
migrations: [],
|
||||||
subscribers: [],
|
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 {
|
export interface FluxGenerationResult {
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
error?: string
|
error?: string
|
||||||
|
transactionSignature?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FluxModelConfig {
|
export interface FluxModelConfig {
|
||||||
@ -58,7 +59,7 @@ export async function generateWithFlux(
|
|||||||
console.log('Raw Flux response:', data)
|
console.log('Raw Flux response:', data)
|
||||||
|
|
||||||
if (data.imageUrl) {
|
if (data.imageUrl) {
|
||||||
return { imageUrl: data.imageUrl }
|
return { imageUrl: data.imageUrl, transactionSignature }
|
||||||
} else {
|
} else {
|
||||||
console.error('Unexpected response structure:', data)
|
console.error('Unexpected response structure:', data)
|
||||||
throw new Error('Invalid response format from Flux API')
|
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