diff --git a/.gitignore b/.gitignore index 86f7387..277b196 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ build/ test/session_token.dart example/lib/session_token.dart test/clearance_token.dart -example/lib/clearance_token.dart \ No newline at end of file +example/lib/clearance_token.dart +test/user_agent.dart +example/lib/user_agent.dart \ No newline at end of file diff --git a/README.md b/README.md index 96b58ed..0e6fefe 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This version have been updated to use Puppeteer to log in to ChatGPT and extract - [Demo](#demo) - [Installation](#installation) - [Usage](#usage) -- [SessionToken and ClearanceToken](#sessiontoken) +- [SessionToken, ClearanceToken and UserAgent](#sessiontoken) - [License](#license) :warning: Be Careful! @@ -36,6 +36,7 @@ import 'package:flutter_chatgpt_api/flutter_chatgpt_api.dart'; _api = ChatGPTApi( sessionToken: SESSION_TOKEN, clearanceToken: CLEARANCE_TOKEN, + userAgent: USER_AGENT, ); setState(() { @@ -84,6 +85,9 @@ Copy the value for __Secure-next-auth.session-token and save it to your environm Copy the value for cf_clearance and save it to your environment. `example/lib/clearance_token.dart` +Copy the value for user_agent and save it to your environment. +`example/lib/user_agent.dart` + Should look something like this: ```dart const SESSION_TOKEN = '__Secure-next-auth.session-token from https://chat.openai.com/chat'; @@ -92,6 +96,10 @@ const SESSION_TOKEN = '__Secure-next-auth.session-token from https://chat.openai ```dart const CLEARANCE_TOKEN = 'cf_clearance token from https://chat.openai.com/chat'; ``` + +```dart +const USER_AGENT = 'user_agent from https://chat.openai.com/chat'; +``` ## Credit - Huge thanks to Travis Fischer for creating [Node.js ChatGPT API](https://github.com/transitive-bullshit/chatgpt-api) (unofficial) 💪 diff --git a/example/lib/main.dart b/example/lib/main.dart index e4816bf..600f286 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,7 @@ -import 'package:example/clearance_token.dart'; -import 'package:example/session_token.dart'; +import 'session_token.dart'; +import 'clearance_token.dart'; +import 'user_agent.dart'; + import 'package:flutter/material.dart'; import 'package:flutter_chatgpt_api/flutter_chatgpt_api.dart'; @@ -46,6 +48,8 @@ class _ChatPageState extends State { _api = ChatGPTApi( sessionToken: SESSION_TOKEN, clearanceToken: CLEARANCE_TOKEN, + userAgent: USER_AGENT, + debug: true, ); isLoading = false; } @@ -122,6 +126,7 @@ class _ChatPageState extends State { _textController.clear(); Future.delayed(const Duration(milliseconds: 50)) .then((_) => _scrollDown()); + await _api.sendModeration(input); var newMessage = await _api.sendMessage( input, conversationId: _conversationId, diff --git a/example/pubspec.lock b/example/pubspec.lock index b24b7d0..3f70f79 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -5,64 +5,56 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.10.0" + version: "2.9.0" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.0" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.1.1" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.17.0" + version: "1.16.0" crypto: dependency: transitive description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" fake_async: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" flutter: @@ -81,8 +73,7 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" flutter_test: @@ -94,64 +85,49 @@ packages: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "4.0.2" - js: - dependency: transitive - description: - name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" - url: "https://pub.dev" - source: hosted - version: "0.6.5" lints: dependency: transitive description: name: lints - sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "2.0.1" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.12.13" + version: "0.12.12" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.1.5" meta: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.8.0" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" sky_engine: @@ -163,74 +139,65 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.9.1" + version: "1.9.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.11.0" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.1.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.2.1" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "0.4.16" + version: "0.4.12" typed_data: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted version: "3.0.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.2" sdks: - dart: ">=2.19.0-406.0.dev <4.0.0" + dart: ">=2.18.2 <3.0.0" flutter: ">=1.17.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9f8404e..1590857 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: '>=2.19.0-406.0.dev <3.0.0' + sdk: '>=2.18.2' # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions diff --git a/lib/flutter_chatgpt_api.dart b/lib/flutter_chatgpt_api.dart index b47fbda..74eff5d 100644 --- a/lib/flutter_chatgpt_api.dart +++ b/lib/flutter_chatgpt_api.dart @@ -16,6 +16,7 @@ class ChatGPTApi { String? apiBaseUrl; String backendApiBaseUrl; String userAgent; + bool debug; final ExpiryMap _accessTokenCache = ExpiryMap(); @@ -23,25 +24,12 @@ class ChatGPTApi { ChatGPTApi({ required this.sessionToken, required this.clearanceToken, + required this.userAgent, this.apiBaseUrl = 'https://chat.openai.com/api', this.backendApiBaseUrl = 'https://chat.openai.com/backend-api', - this.userAgent = defaultUserAgent, + this.debug = false, }); - Map defaultHeaders = { - 'user-agent': defaultUserAgent, - 'x-openai-assistant-app-id': '', - 'accept-language': 'en-US,en;q=0.9', - HttpHeaders.accessControlAllowOriginHeader: 'https://chat.openai.com', - HttpHeaders.refererHeader: 'https://chat.openai.com/chat', - 'sec-ch-ua': - '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"', - 'sec-ch-ua-platform': '"Windows"', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'same-origin', - }; - Future sendMessage( String message, { String? conversationId, @@ -66,31 +54,47 @@ class ChatGPTApi { final url = '$backendApiBaseUrl/conversation'; + var headersConv = { + 'cookie': 'cf_clearance=$clearanceToken', + 'user-agent': userAgent, + 'x-openai-assistant-app-id': '', + 'accept-language': 'en-US,en;q=0.9', + HttpHeaders.accessControlAllowOriginHeader: 'https://chat.openai.com', + HttpHeaders.refererHeader: 'https://chat.openai.com/chat', + 'sec-ch-ua': + '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"', + 'sec-ch-ua-platform': '"Mac"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'authorization': 'Bearer $accessToken', + 'content-Type': 'application/json', + 'accept': 'text/event-stream', + }; + + if (debug) { + print('== REQUEST =='); + print('POST $url'); + print('Headers : $headersConv'); + print('Body : ${body.toJson()}'); + } + final response = await http.post( Uri.parse(url), - headers: { - 'user-agent': defaultUserAgent, - 'x-openai-assistant-app-id': '', - 'accept-language': 'en-US,en;q=0.9', - HttpHeaders.accessControlAllowOriginHeader: 'https://chat.openai.com', - HttpHeaders.refererHeader: 'https://chat.openai.com/chat', - 'sec-ch-ua': - '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"', - 'sec-ch-ua-platform': '"Windows"', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'cors', - 'sec-fetch-site': 'same-origin', - 'Authorization': 'Bearer $accessToken', - 'Content-Type': 'application/json', - 'Accept': 'text/event-stream', - 'Cookie': 'cf_clearance=$clearanceToken' - }, + headers: headersConv, body: body.toJson(), ); + if (debug) { + print('== RESPONSE =='); + print('HTTP code : ${response.statusCode}'); + print('Headers : ${response.headers}'); + print('Body : ${response.body}'); + } + if (response.statusCode != 200) { if (response.statusCode == 429) { - throw Exception('Rate limited'); + throw Exception(response.body); } else { throw Exception('Failed to send message'); } @@ -118,28 +122,111 @@ class ChatGPTApi { } } + Future isAuthenticated() async { + try { + await _refreshAccessToken(); + return true; + } catch (e) { + return false; + } + } + + Future sendModeration(String input) async { + final accessToken = await _refreshAccessToken(); + + final body = ModerationsBody( + input: input, + model: 'text-moderation-playground', + ); + + final url = '$backendApiBaseUrl/moderations'; + + var headersConv = { + 'user-agent': userAgent, + 'x-openai-assistant-app-id': '', + 'accept-language': 'en-US,en;q=0.9', + HttpHeaders.accessControlAllowOriginHeader: 'https://chat.openai.com', + HttpHeaders.refererHeader: 'https://chat.openai.com/chat', + 'sec-ch-ua': + '"Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"', + 'sec-ch-ua-platform': '"Mac"', + 'sec-fetch-dest': 'empty', + 'sec-fetch-mode': 'cors', + 'sec-fetch-site': 'same-origin', + 'authorization': 'Bearer $accessToken', + 'content-Type': 'application/json', + 'accept': '*/*', + 'cookie': 'cf_clearance=$clearanceToken', + }; + + if (debug) { + print('== REQUEST =='); + print('POST $url'); + print('Headers : $headersConv'); + print('Body : ${body.toJson()}'); + } + + final response = await http.post( + Uri.parse(url), + headers: headersConv, + body: body.toJson(), + ); + + if (debug) { + print('== RESPONSE =='); + print('HTTP code : ${response.statusCode}'); + print('Headers : ${response.headers}'); + print('Body : ${response.body}'); + } + + if (response.statusCode != 200) { + if (response.statusCode == 429) { + throw Exception('Rate limited'); + } else { + throw Exception('Failed to send message'); + } + } else if (_errorMessages.contains(response.body)) { + throw Exception('OpenAI returned an error'); + } + + return response.body; + } + Future _refreshAccessToken() async { final cachedAccessToken = _accessTokenCache['KEY_ACCESS_TOKEN']; if (cachedAccessToken != null) { return cachedAccessToken; } + var headersSession = { + 'cookie': + 'cf_clearance=$clearanceToken;__Secure-next-auth.session-token=$sessionToken', + 'user-agent': userAgent, + 'accept': '*/*', + }; + + if (debug) { + print('POST $apiBaseUrl/auth/session'); + print('Headers : $headersSession'); + } try { - final res = await http.get( + final response = await http.get( Uri.parse('$apiBaseUrl/auth/session'), - headers: { - 'cookie': - 'cf_clearance=$clearanceToken;__Secure-next-auth.session-token=$sessionToken', - 'accept': '*/*', - ...defaultHeaders, - }, + headers: headersSession, ); - if (res.statusCode != 200) { + if (debug) { + print('== RESPONSE =='); + print('HTTP code : ${response.statusCode}'); + print('Headers : ${response.headers}'); + print('Body : ${response.body}'); + } + + if (response.statusCode != 200) { throw Exception('Failed to refresh access token'); } - final accessToken = jsonDecode(res.body)['accessToken']; + final accessToken = jsonDecode(response.body)['accessToken']; if (accessToken == null) { throw Exception( @@ -154,9 +241,6 @@ class ChatGPTApi { } } -const defaultUserAgent = - 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36'; - const _errorMessages = [ "{\"detail\":\"Hmm...something seems to have gone wrong. Maybe try me again in a little bit.\"}", ]; diff --git a/lib/src/models/moderation_body.model.dart b/lib/src/models/moderation_body.model.dart index a83896f..dd51055 100644 --- a/lib/src/models/moderation_body.model.dart +++ b/lib/src/models/moderation_body.model.dart @@ -1,13 +1,20 @@ -enum AvailableModerationModels { - textModerationPlayground, -} +import 'dart:convert'; class ModerationsBody { final String input; - final AvailableModerationModels model; + final String model; ModerationsBody({ required this.input, required this.model, }); + + Map toMap() { + return { + 'input': input, + 'model': model, + }; + } + + String toJson() => json.encode(toMap()); } diff --git a/pubspec.yaml b/pubspec.yaml index 8b76b5d..fd37a46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/coskuncay/flutter_chatgpt_api repository: https://github.com/coskuncay/flutter_chatgpt_api environment: - sdk: '>=2.18.2 <3.0.0' + sdk: '>=2.18.2' flutter: ">=1.17.0" dependencies: diff --git a/test/flutter_chatgpt_test.dart b/test/flutter_chatgpt_test.dart index 9b13c20..0e89903 100644 --- a/test/flutter_chatgpt_test.dart +++ b/test/flutter_chatgpt_test.dart @@ -3,11 +3,15 @@ import 'package:flutter_test/flutter_test.dart'; import 'clearance_token.dart'; import 'session_token.dart'; +import 'user_agent.dart'; void main() { test('do basic prompt', () async { final api = ChatGPTApi( - sessionToken: SESSION_TOKEN, clearanceToken: CLEARANCE_TOKEN); + sessionToken: SESSION_TOKEN, + clearanceToken: CLEARANCE_TOKEN, + userAgent: USER_AGENT, + ); const prompt = 'Write a python version of bubble sort. Do not include example usage.';