<Project-2 Twitter> Flask: Twitter API Oauth 2.0 验证 在网页上发贴

这两周正在学python,第二次拼接 code 。最终目标是发贴,做个美食,中国游的账号。

laptop: windows 11 home. 

项目结构:

app.py 主程序

config.ini  这里存所有的 keys Tokens 还有 callback url

目录 Templates 用于存 index.html

目录 static 存放 script.js 与  style.css 幻想着漂亮的界面与功能

干这活儿,就两步:

第一步 有个laptop 注册个发堆账号 x稻糠母   稻糠母 = .com

第二步 注册 twitter 开发平台账号 https://developer.x.com/enx稻糠母https://developer.x.com/en

我使用的是:自由计划(free plan)功能就几个:发贴、删帖、转发    第二天结束从 Oauth 1.0a 又回到原点,在这里做个笔记。

前期准备

安装 Python ,  还有以下必要的库  pip install 库名
  • hashlib, base64, os:生成 code_verifiercode_challenge,用于 OAuth 2.0 PKCE 流程。
  • requests:用于与 Twitter API 进行 HTTP 请求。
  • configparser:用于从 config.ini 文件读取配置。
  • OAuth1:用于 OAuth 1.0a 认证(用于媒体上传)。
  • Flask:创建 Web 应用。
  • BeautifulSoup:用于解析 HTML(目前没有使用)。

目录结构

Twitter/                     # 项目根目录
│
├── app.py                   # Flask 应用程序主文件,负责展示RSS内容
├── config.ini               # 存储API密钥等配置信息的文件
├── static/                  # 静态文件目录
│   ├── style.css            # 存储CSS样式文件
│   └── script.js            # JavaScript
│
└── templates/               # Flask HTML 模板文件夹
    └── index.html           # 网页展示的HTML模板

目录说明:

  1. Twitter/:项目根目录,包含应用的主要文件。
  2. app.py:Flask 应用的核心文件,运行时会启动本地服务器,通过网页展示 RSS 内容,并执行 CSV 文件的读取和自动更新操作。
  3. config.ini:用于存储 API 密钥等敏感信息。
  4. static/:存储静态文件,如 CSS 和 JavaScript 文件,负责网页的样式和功能实现。
  5. templates/:Flask 的 HTML 模板文件夹。

主程序 app.py

app.py 代码:

import hashlib
import base64
import os
import requests
import configparser
from requests_oauthlib import OAuth1
from flask import Flask, request, render_template, jsonify, redirect, url_for, session
from bs4 import BeautifulSoup

# 创建 Flask 应用
app = Flask(__name__)

# 定义上传媒体的函数
def upload_media_to_twitter(media_file, media_type):
    # 媒体上传 URL
    url = "https://upload.twitter.com/1.1/media/upload.json"
    files = {'media': media_file}
    data = {
        'media_category': 'tweet_image' if media_type == 'image' else 'tweet_video'
    }

    # 获取 OAuth 认证信息
    access_token = session.get('access_token')
    headers = {
        'Authorization': f'Bearer {access_token}'
    }

    try:
        # 发送上传请求
        response = requests.post(url, headers=headers, files=files, data=data)
        response.raise_for_status()

        # 从响应中提取 media_id
        media_id = response.json().get('media_id_string')
        return media_id
    except requests.exceptions.RequestException as e:
        print(f"Error uploading media: {e}")
        print(f"Response content: {response.content}")
        raise e



# 读取 config.ini 文件中的 Twitter API 凭证
def load_config():
    config = configparser.RawConfigParser()
    try:
        config.read('config.ini')
        return config
    except Exception as e:
        print(f"Failed to load config.ini: {str(e)}")
        return None
    
config = load_config()

# 从配置文件中获取 secret_key 和 Twitter API 凭证
app.secret_key = config.get('flask', 'secret_key')

consumer_key = config.get('twitter', 'consumer_key')
consumer_secret = config.get('twitter', 'consumer_secret')
client_id = config.get('twitter', 'client_id')
client_secret = config.get('twitter', 'client_secret')
access_token = config.get('twitter', 'access_token')
access_token_secret = config.get('twitter', 'access_token_secret')
redirect_url = config.get('twitter', 'redirect_url')  
scopes = "tweet.read tweet.write users.read offline.access"


# 创建 OAuth 1.0a 认证对象
auth = OAuth1(consumer_key, consumer_secret, access_token, access_token_secret)

#OAuth 2.0 登录部分登录部分

# 生成 code_verifier 和 code_challenge
def generate_code_verifier_and_challenge():
    code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode('utf-8')
    code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest()
    code_challenge = base64.urlsafe_b64encode(code_challenge).rstrip(b'=').decode('utf-8')
    return code_verifier, code_challenge

code_verifier, code_challenge = generate_code_verifier_and_challenge()


# 生成授权 Oauth 2.0 URL 
def build_auth_url(client_id, redirect_url, scopes, code_challenge):
    auth_url = (
        f"https://twitter.com/i/oauth2/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_url}&"
        f"scope={scopes}&state=state&code_challenge={code_challenge}&code_challenge_method=S256"
    )
    return auth_url


# 在使用之前生成 auth_url

auth_url = build_auth_url(client_id, redirect_url, scopes, code_challenge)
print(f"Auth URL: {auth_url}") # 调试用,可以直接访问该 URL


@app.route('/')
def index():
    access_token = session.get('access_token')
    if not access_token:
        return redirect(auth_url)  # 如果没有 access_token,重定向到 Twitter 授权页面
    return render_template('index.html', auth_url=auth_url, access_token=access_token)


@app.route('/callback')
def callback():
    code = request.args.get('code')
    if not code:
        return "Error: No authorization code provided", 400
    
    print(f"Authorization code received: {code}")

    try:
        access_token = get_access_token(code)
        session['access_token'] = access_token  # 将 access_token 存储在会话中
        return redirect(url_for('index'))  # 重定向到主页
    except Exception as ex:
        return f"An error occurred: {str(ex)}", 500

@app.route('/fetch_content', methods=['POST'])
def fetch_content():
    url = request.json.get('url')
    if not url:
        return jsonify({'error': 'No URL provided'}), 400
    
    try:
        response = requests.get(url)
        response.raise_for_status()
        response.encoding = 'utf-8'
        
        soup = BeautifulSoup(response.text, 'html.parser')
        title = soup.title.string if soup.title else 'No title found'
        images = [img['src'] for img in soup.find_all('img') if 'src' in img.attrs]
        
        return jsonify({'title': title, 'images': images})
    except requests.RequestException as e:
        return jsonify({'error': str(e)}), 500

def get_access_token(code):
    token_url = 'https://api.twitter.com/2/oauth2/token'
    data = {
        'client_id': client_id,
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': redirect_url,
        'code_verifier': code_verifier,
    }
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    response = requests.post(token_url, data=data, headers=headers)
    
    print(f"Response status code: {response.status_code}")
    print(f"Response text: {response.text}")

    if response.status_code == 200:
        tokens = response.json()
        return tokens['access_token']
    else:
        raise Exception(f"Failed to get access token: {response.status_code} {response.text}")




# 使用 API v2 发布推文

@app.route('/post_tweet', methods=['POST'])
def post_tweet():
    tweet_text = request.form['tweet_text']
    tweet_images = request.files.getlist('tweet_images')  # 获取上传的图片文件
    tweet_videos = request.files.getlist('tweet_videos')  # 获取上传的视频文件

    media_ids = []

    access_token = session.get('access_token')
    if not access_token:
        return jsonify({'error': 'No access token available. Please log in.'}), 403

    try:
        # 上传图片
        for image in tweet_images:
            media_id = upload_media_to_twitter(image, 'image')  # 传递文件对象和类型
            media_ids.append(media_id)
        
        # 上传视频
        for video in tweet_videos:
            media_id = upload_media_to_twitter(video, 'video')  # 传递文件对象和类型
            media_ids.append(media_id)

        json_data = {'text': tweet_text}
        if media_ids:
            json_data['media'] = {'media_ids': media_ids}

        url = "https://api.twitter.com/2/tweets"
        headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }

        response = requests.post(url, headers=headers, json=json_data)
        response.raise_for_status()

        return jsonify({'message': 'Tweet posted successfully!'})
    except requests.exceptions.RequestException as e:
        return jsonify({'error': str(e)}), 500


# Flask at 8000 Port
if __name__ == '__main__':
    app.secret_key = 'your_secret_key'
    app.run(port=8000, debug=True)

代码解释:

mport hashlib

import base64

import os

import requests

import configparser

from requests_oauthlib import OAuth1

from flask import Flask, request, render_template, jsonify, redirect, url_for, session

from bs4 import BeautifulSoup

#创建一个 Flask 应用实例: 我加了注释,粘贴后再检查一遍。

app = Flask(__name__)

#上传媒体的函数,此函数负责将媒体文件(图片或视频)上传到 x 。它使用 OAuth 1.0a 认证。上传成功后,它会返回媒体 ID,供后续发布推文使用。

def upload_media_to_twitter(media_file, media_type):
    url = "https://upload.twitter.com/1.1/media/upload.json"
    files = {'media': media_file}
    data = {
        'media_category': 'tweet_image' if media_type == 'image' else 'tweet_video'
    }

    access_token = session.get('access_token')
    headers = {
        'Authorization': f'Bearer {access_token}'
    }

    try:
        response = requests.post(url, headers=headers, files=files, data=data)
        response.raise_for_status()
        media_id = response.json().get('media_id_string')
        return media_id
    except requests.exceptions.RequestException as e:
        print(f"Error uploading media: {e}")
        print(f"Response content: {response.content}")
        raise e


# 读取 config.ini 配置文件 API 凭证和应用的其他配置信息

def load_config():
    config = configparser.RaWConfigParser() # 这里用 rawconfigparser 因为token里可能有%这类的字符,最初在%前加一个%  ... 
    try:
        config.read('config.ini')
        return config
    except Exception as e:
        print(f"Failed to load config.ini: {str(e)}")
        return None

config = load_config()

#从配置文件中获取关键数据 

# config.ini 文件中读取 Flask 的 secret_key 以及 Twitter API 的凭证(Consumer Key、Consumer Secret、Access Token、Access Token Secret 等)  有用没用,都先赋值。

app.secret_key = config.get('flask', 'secret_key')

consumer_key = config.get('twitter', 'consumer_key')
consumer_secret = config.get('twitter', 'consumer_secret')
client_id = config.get('twitter', 'client_id')
client_secret = config.get('twitter', 'client_secret')
access_token = config.get('twitter', 'access_token')
access_token_secret = config.get('twitter', 'access_token_secret')
redirect_url = config.get('twitter', 'redirect_url')  
scopes = "tweet.read tweet.write users.read offline.access"
 

#创建 OAuth 1.0a 认证对象  这用于媒体上传(v1.1 API)还没搞通,不知道是什么原因,图与视频不行。还在耐心试试

auth = OAuth1(consumer_key, consumer_secret, access_token, access_token_secret)


#为 OAuth 2.0 的 PKCE(Proof Key for Code Exchange)流程生成 code_verifiercode_challenge,用于增强安全性。这段是哪儿炒的?想不起来。

def generate_code_verifier_and_challenge():
    code_verifier = base64.urlsafe_b64encode(os.urandom(32)).rstrip(b'=').decode('utf-8')
    code_challenge = hashlib.sha256(code_verifier.encode('utf-8')).digest()
    code_challenge = base64.urlsafe_b64encode(code_challenge).rstrip(b'=').decode('utf-8')
    return code_verifier, code_challenge

code_verifier, code_challenge = generate_code_verifier_and_challenge()


#生成OAuth 2.0 的授权 URL,用户点击后将重定向到 Twitter 进行授权。

def build_auth_url(client_id, redirect_url, scopes, code_challenge):
    auth_url = (
        f"https://twitter.com/i/oauth2/authorize?response_type=code&client_id={client_id}&redirect_uri={redirect_url}&"
        f"scope={scopes}&state=state&code_challenge={code_challenge}&code_challenge_method=S256"
    )
    return auth_url

auth_url = build_auth_url(client_id, redirect_url, scopes, code_challenge)
 

#Flask 路由:/(主页) 如果没有 access_token,则重定向用户到 Twitter 进行授权。

@app.route('/')
def index():
    access_token = session.get('access_token')
    if not access_token:
        return redirect(auth_url)
    return render_template('index.html', auth_url=auth_url, access_token=access_token)
 

# Flask 路由:/callback(授权回调)用户授权后,Twitter 将返回授权码,该路由将处理该授权码并获取

@app.route('/callback')
def callback():
    code = request.args.get('code')
    if not code:
        return "Error: No authorization code provided", 400
    
    try:
        access_token = get_access_token(code)
        session['access_token'] = access_token
        return redirect(url_for('index'))
    except Exception as ex:
        return f"An error occurred: {str(ex)}", 500
 

# 函数发送 POST 请求到 x 以获取 OAuth 2.0 access_token

def get_access_token(code):
    token_url = 'https://api.twitter.com/2/oauth2/token'
    data = {
        'client_id': client_id,
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': redirect_url,
        'code_verifier': code_verifier,
    }
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    response = requests.post(token_url, data=data, headers=headers)
    
    if response.status_code == 200:
        tokens = response.json()
        return tokens['access_token']
    else:
        raise Exception(f"Failed to get access token: {response.status_code} {response.text}")
 

# Flask 路由:/post_tweet  处理提交的推文文本和媒体(图片或视频)。上传媒体后,将发布推文  现在是文字没问题, 图与视频不成功,今天主要在找这儿的问题,还没解决。

@app.route('/post_tweet', methods=['POST'])
def post_tweet():
    tweet_text = request.form['tweet_text']
    tweet_images = request.files.getlist('tweet_images')
    tweet_videos = request.files.getlist('tweet_videos')

    media_ids = []

    access_token = session.get('access_token')
    if not access_token:
        return jsonify({'error': 'No access token available. Please log in.'}), 403

    try:
        for image in tweet_images:
            media_id = upload_media_to_twitter(image, 'image')
            media_ids.append(media_id)
        
        for video in tweet_videos:
            media_id = upload_media_to_twitter(video, 'video')
            media_ids.append(media_id)

        json_data = {'text': tweet_text}
        if media_ids:
            json_data['media'] = {'media_ids': media_ids}

        url = "https://api.twitter.com/2/tweets"
        headers = {
            'Authorization': f'Bearer {access_token}',
            'Content-Type': 'application/json'
        }

        response = requests.post(url, headers=headers, json=json_data)
        response.raise_for_status()

        return jsonify({'message': 'Tweet posted successfully!'})
    except requests.exceptions.RequestException as e:
        return jsonify({'error': str(e)}), 500

#启动 Flask 应用,监听 8000 端口 

if __name__ == '__main__':
    app.secret_key = 'your_secret_key'
    app.run(port=8000, debug=True)
 

config.ini 文件内容

        作用:存放密匙 令牌

[twitter]
client_id = 看图
client_secret = 看图
bearer_token = %%看图
consumer_key = 看图
consumer_secret = 看图
access_token = 看图
access_token_secret = 看图
redirect_url = http://127.0.0.1:8000/callback
[flask]
secret_key = 

获取方法:

上面令牌等信息用在你的开发者平台  左边: Projects Apps -> Default project

https://developer.x.com/en/portal


 

开发主页还有两处设置,关于账号权力与返回数据如图:红框需要相同,(因为是练手,用的本机127.0.0.1 ),蓝线是必须填写的 比如用 自己的域名或仇家的域名。

上面的设置,是要Projects Apps -> Default project 点击 User authentication settings 才有。

index.html 代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Twitter Post Tool</title>
    <style>
        /* 基本样式 */
        body {
            font-family: Arial, sans-serif;
            padding: 20px;
            max-width: 800px;
            margin: 0 auto;
        }

        h1 {
            text-align: center;
        }

        label {
            display: block;
            margin-bottom: 10px;
        }

        input[type="text"],
        textarea {
            width: 100%;
            padding: 10px;
            margin-bottom: 10px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }

        input[type="file"] {
            display: block;
            margin-bottom: 10px;
        }

        button {
            padding: 10px 20px;
            background-color: #1da1f2;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }

        button:hover {
            background-color: #0d8ae5;
        }

        #images-container img {
            border: 2px solid #ccc;
            margin-bottom: 10px;
        }

        #images-container img.selected {
            border: 2px solid #1da1f2;
        }

        #char-counter {
            display: block;
            margin-top: -10px;
            margin-bottom: 10px;
            text-align: right;
            font-size: 14px;
        }

        #char-counter.red {
            color: red;
        }
    </style>
</head>
<body>
    <h1>Twitter Post Tool</h1>

    <label for="url-input">Enter a news URL:</label>
    <input type="text" id="url-input" placeholder="Paste the news URL here">

    <button id="fetch-button">Fetch Content</button>

    <label for="tweet-text">Tweet Content:</label>
    <textarea id="tweet-text" rows="4" maxlength="140"></textarea>
    <span id="char-counter">0/140</span>

    <!-- 图片上传 -->
    <label for="image-upload">Upload Images:</label>
    <input type="file" id="image-upload" name="tweet_images" accept="image/*" multiple>

    <!-- 视频上传 -->
    <label for="video-upload">Upload Videos:</label>
    <input type="file" id="video-upload" name="tweet_videos" accept="video/*" multiple>

    <div id="images-container"></div>

    <button id="submit-button">Submit to Twitter</button>

    <!-- Hidden field to store the access token -->
    <input type="hidden" id="access-token" value="{{ session.get('access_token') }}">

    <script>
        // 实时字符计数器
        const tweetText = document.getElementById('tweet-text');
        const charCounter = document.getElementById('char-counter');
        
        tweetText.addEventListener('input', function() {
            const length = tweetText.value.length;
            charCounter.textContent = `${length}/140`;
            if (length > 140) {
                charCounter.classList.add('red');
            } else {
                charCounter.classList.remove('red');
            }
        });

        // Fetch URL 内容
        document.getElementById('fetch-button').addEventListener('click', function() {
            const url = document.getElementById('url-input').value;
            
            fetch('/fetch_content', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ url: url })
            })
            .then(response => response.json())
            .then(data => {
                if (data.error) {
                    alert('Error fetching content: ' + data.error);
                } else {
                    document.getElementById('tweet-text').value = data.title;
                    const imagesContainer = document.getElementById('images-container');
                    imagesContainer.innerHTML = ''; // 清空之前的图片

                    data.images.forEach((imageSrc, index) => {
                        const imageWrapper = document.createElement('div');
                        imageWrapper.style.display = 'inline-block';
                        imageWrapper.style.margin = '10px';

                        const img = document.createElement('img');
                        img.src = imageSrc;
                        img.style.width = '100px'; // 固定大小
                        img.style.height = '100px'; // 固定大小
                        img.alt = 'Image ' + (index + 1);

                        const checkbox = document.createElement('input');
                        checkbox.type = 'checkbox';
                        checkbox.name = 'selected_images';
                        checkbox.value = imageSrc;
                        
                        imageWrapper.appendChild(img);
                        imageWrapper.appendChild(checkbox);
                        imagesContainer.appendChild(imageWrapper);
                    });
                }
            })
            .catch(error => {
                console.error('Error fetching content:', error);
            });
        });

        // 提交推文
        document.getElementById('submit-button').addEventListener('click', function () {
            const tweetText = document.getElementById('tweet-text').value;
            const accessToken = document.getElementById('access-token').value;
            const selectedImages = document.querySelectorAll('input[name="selected_images"]:checked');
            const tweetImages = document.getElementById('image-upload').files;
            const tweetVideos = document.getElementById('video-upload').files;
            
            const formData = new FormData();
            formData.append('tweet_text', tweetText);
            formData.append('access_token', accessToken);

            for (let i = 0; i < tweetImages.length; i++) {
                formData.append('tweet_images', tweetImages[i]);
            }

            for (let i = 0; i < tweetVideos.length; i++) {
                formData.append('tweet_videos', tweetVideos[i]);
            }

            fetch('/post_tweet', {
                method: 'POST',
                body: formData
            })
            .then(response => response.json())
            .then(data => {
                if (data.error) {
                    alert('Failed to post tweet: ' + data.error);
                } else {
                    alert('Tweet posted successfully!');
                }
            })
            .catch(error => {
                console.error('Error posting tweet:', error);
            });
        });
    </script>
</body>
</html>

这个没什么花哨的,就是标准模板上改的。加个fetch 功能,还需要完善 js app 。

script.js 代码

// 实时字符计数器
const tweetText = document.getElementById('tweet-text');
const charCounter = document.getElementById('char-counter');

tweetText.addEventListener('input', function() {
    const length = tweetText.value.length;
    charCounter.textContent = `${length}/140`;
    if (length > 140) {
        charCounter.style.color = 'red';
    } else {
        charCounter.style.color = 'black';
    }
});

// Fetch URL 内容
document.getElementById('fetch-button').addEventListener('click', function() {
    const url = document.getElementById('url-input').value;
    
    fetch('/fetch_content', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ url: url })
    })
    .then(response => response.json())
    .then(data => {
        if (data.error) {
            alert('Error fetching content: ' + data.error);
        } else {
            // 设置抓取的标题到 tweet-text 中
            document.getElementById('tweet-text').value = data.title;

            // 更新字符计数器
            const length = data.title.length;
            charCounter.textContent = `${length}/140`;
            if (length > 140) {
                charCounter.style.color = 'red';
            } else {
                charCounter.style.color = 'black';
            }

            // 显示图片
            const imagesContainer = document.getElementById('images-container');
            imagesContainer.innerHTML = ''; // 清空之前的图片

            data.images.forEach((imageSrc, index) => {
                const imageWrapper = document.createElement('div');
                imageWrapper.style.display = 'inline-block';
                imageWrapper.style.margin = '10px';
                imageWrapper.style.textAlign = 'center';

                const img = document.createElement('img');
                img.src = imageSrc;
                img.style.width = '100px'; // 固定大小
                img.style.height = '100px'; // 固定大小
                img.alt = 'Image ' + (index + 1);
                img.onclick = function() {
                    window.open(imageSrc, '_blank');
                };

                const checkbox = document.createElement('input');
                checkbox.type = 'checkbox';
                checkbox.name = 'selected_images';
                checkbox.value = imageSrc;
                
                imageWrapper.appendChild(img);
                imageWrapper.appendChild(document.createElement('br')); // 换行符
                imageWrapper.appendChild(checkbox);
                imagesContainer.appendChild(imageWrapper);
            });
        }
    })
    .catch(error => {
        console.error('Error fetching content:', error);
        alert('Error fetching content: ' + error.message);
    });
});

// 提交推文
document.getElementById('submit-button').addEventListener('click', function () {
    const tweetText = document.getElementById('tweet-text').value;
    const accessToken = document.getElementById('access-token').value;
    const selectedImages = document.querySelectorAll('input[name="selected_images"]:checked');
    
    const formData = new FormData();
    formData.append('tweet_text', tweetText);
    formData.append('access_token', accessToken);

    if (selectedImages.length > 0) {  // Only append images if there are selected images
        selectedImages.forEach(image => {
            formData.append('tweet_images', image.value);
        });
    }

    fetch('/post_tweet', {
        method: 'POST',
        body: formData
    })
    .then(response => response.json())
    .then(data => {
        if (data.error) {
            alert('Failed to post tweet: ' + data.error);
        } else {
            alert('Tweet posted successfully!');
        }
    })
    .catch(error => {
        console.error('Error posting tweet:', error);
    });
});

style.css 代码

body {
    font-family: Arial, sans-serif;
    background-color: #f4f4f4;
    margin: 0;
    padding: 20px;
}

.container {
    max-width: 600px;
    margin: 0 auto;
    background: #fff;
    padding: 20px;
    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
    border-radius: 8px;
}

h1 {
    color: #333;
    text-align: center;
}

.form-group {
    margin-bottom: 15px;
}

.input-field {
    width: 100%;
    padding: 10px;
    margin: 5px 0;
    box-sizing: border-box;
    border: 1px solid #ccc;
    border-radius: 4px;
}

.btn {
    display: inline-block;
    padding: 10px 20px;
    color: #fff;
    background-color: #007bff;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    text-align: center;
}

.btn:hover {
    background-color: #0056b3;
}

.content-section {
    margin-top: 20px;
}

.content-title {
    font-size: 1.2em;
    margin-bottom: 10px;
}

.content-images img {
    margin-right: 10px;
    margin-bottom: 10px;
    border: 2px solid #ddd;
    border-radius: 4px;
    cursor: pointer;
}

.result-message {
    margin-top: 20px;
}

.result-message .success {
    color: green;
}

.result-message .error {
    color: red;
}

结束语:

网上很多是 OAuth 1.0a 的代码, 最开始我也是搬它们的,但总有错误 要不就是 403 401的,都开始用 curl 命令了。 去改修 fetch 只能获得sina 

后来不得不去读 官方参考例子,转用  OAuth 2.0 Authorization code with PKCE.  这个更简单。

其它功能在没放弃前,还有可能完善。 

last modified on 9/19/2024 调整排版

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值