Skip to content

Commit

Permalink
Merge pull request #4 from ShiranAbir/feature/settings-support
Browse files Browse the repository at this point in the history
Feature/settings support
  • Loading branch information
ShiranAbir authored Dec 31, 2022
2 parents 50aa2e8 + b46c265 commit 9ade05b
Show file tree
Hide file tree
Showing 15 changed files with 303 additions and 67 deletions.
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,11 @@ It uses the [chatgpt](https://github.com/transitive-bullshit/chatgpt-api) Node.j
# Usage

## Setup
1. Register your account on [ChatGPT](https://chat.openai.com/auth/login) (make sure to register a normal account and don't use Google/Microsoft).
2. Then set your credentials so the app can use it:
2.1 Open an elevated command line and run the following (change `examplemail` and `examplepass` to yours):
2.2 `setx OPENAI_EMAIL examplemail /m`
2.3 `setx OPENAI_PASSWORD examplepass /m`
Register your account on [ChatGPT](https://chat.openai.com/auth/login) (make sure to register a normal account and don't use Google/Microsoft).

## Running
1. Open the `chaty.exe` file.
2. When opened, it'll use your credentials to login, but will fail the reCaptcha.
2. When opened, it'll ask your credentials to login, but will fail the reCaptcha.
3. Solve the reCaptcha manually and click `Continue` and it'll finish the rest.
4. Then you can use the the `Electron` window and ask `ChatGPT` questions!

Expand Down
17 changes: 9 additions & 8 deletions backend/chatgpt.service.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import * as googleTTS from 'google-tts-api'
var chatApi = null
var conversationId = null
var parentMessageId = null
export async function chatgpt_init() {
if (!process.env.OPENAI_EMAIL || !process.env.OPENAI_PASSWORD){
dialog.showMessageBoxSync({message:"Please set Email and Password first!"})
export async function chatgpt_init(settings) {
let isGoogleLogin = false
let isMicrosoftLogin = false
if (settings.accountType == 'Google') isGoogleLogin = true
if (settings.accountType == 'Microsoft') isMicrosoftLogin = true

app.quit()
return
}
const api = new ChatGPTAPIBrowser({
email: process.env.OPENAI_EMAIL,
password: process.env.OPENAI_PASSWORD,
email: settings.username,
password: settings.password,
isGoogleLogin: isGoogleLogin,
isMicrosoftLogin: isMicrosoftLogin
})
await api.initSession()

Expand Down
30 changes: 28 additions & 2 deletions backend/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
// Modules to control application life and create native browser window
const { app, protocol, BrowserWindow } = require('electron')
const { app, protocol, BrowserWindow, ipcMain, safeStorage } = require('electron')
const path = require('path')

require('./server')
const Store = require('electron-store');

//defined the store
let store = new Store();

const createWindow = () => {
// Create the browser window.
Expand Down Expand Up @@ -30,11 +34,33 @@ app.whenReady().then(() => {
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})


ipcMain.on('electron-store-set', (event, key, value) => {
// Encrypt password using safeStorage
if (value.password && safeStorage.isEncryptionAvailable()) {
value.password = safeStorage.encryptString(value.password).toString('base64')
}

store.set(key, value)

event.sender.send('asynchronous-result', true)
})

ipcMain.on('electron-store-get', (event, key) => {
const value = store.get(key, {})
// Decrypt password using safeStorage
if (value.password && safeStorage.isEncryptionAvailable()) {
value.password = safeStorage.decryptString(Buffer.from(value.password, 'base64'))
}

event.sender.send('asynchronous-result', value)
})
})

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit()
})
})
20 changes: 19 additions & 1 deletion backend/preload.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const { contextBridge, ipcRenderer } = require('electron')

// All the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
window.addEventListener('DOMContentLoaded', () => {
Expand All @@ -9,4 +11,20 @@ window.addEventListener('DOMContentLoaded', () => {
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency])
}
})
})

// Expose ipcRenderer to the client
contextBridge.exposeInMainWorld('ipcRenderer', {
send: (channel, key, value) => {
let validChannels = ['electron-store-set', 'electron-store-get']
if (validChannels.includes(channel)) {
if (typeof(value) !== 'undefined') ipcRenderer.send(channel, key, value)
else ipcRenderer.send(channel, key)
}

return new Promise((resolve) => {
ipcRenderer.once('asynchronous-result', (event, data) => resolve(data))
})
}
})

23 changes: 12 additions & 11 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,27 @@ const app = express();
const port = 3000;

var chatgptFunc = null;
var chatgptInitFunc = null;
var speechFunc = null;
// need to import es module dynamically
import('./chatgpt.service.mjs').then(chatgpt =>
{
chatgptFunc = chatgpt.chatgpt
speechFunc = chatgpt.getSpeechUrl
chatgpt.chatgpt_init()
}
);

app.use(compression())
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.post("/", async function (req, res) {
app.post("/init", async function (req, res) {
if (!chatgptFunc){
res.send("Backend isn't ready yet!")
return
const chatgpt = await import('./chatgpt.service.mjs')
chatgptFunc = chatgpt.chatgpt
chatgptInitFunc = chatgpt.chatgpt_init
speechFunc = chatgpt.getSpeechUrl

console.log("Backend isn't ready yet!")
}

res.send(await chatgptInitFunc(req.body));
});

app.post("/ask", async function (req, res) {
const question = req.body.question;
res.send(await chatgptFunc(question));
});
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "chaty",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"description": "Chaty is an Electron bot that you can use to interact with ChatGPT",
"author": "Shiran Abir",
Expand All @@ -23,11 +23,12 @@
"axios": "^1.2.1",
"bootstrap": "^5.2.3",
"bootstrap-vue-3": "^0.4.15",
"chatgpt": "^3.3.1",
"chatgpt": "^3.3.8",
"clone-deep": "^4.0.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
"electron-squirrel-startup": "^1.0.0",
"electron-store": "^8.1.0",
"express": "^4.18.2",
"google-tts-api": "^2.0.2",
"highlight.js": "^11.7.0",
Expand Down
26 changes: 21 additions & 5 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
<template>
<mainView :callConnecting="callConnecting" />
</template>

<script setup lang="ts">
import mainView from '@/views/mainView.vue'
</script>

<template>
<mainView/>
</template>


<script lang="ts">
export default {
data(){
return{
callConnecting:false
}
},
async created() {
await this.$store.dispatch({ type: 'loadSettings' })
const isConnected = await this.$store.dispatch({ type: 'initBackend' })
if (!isConnected){
this.callConnecting = true
}
}
}
</script>
9 changes: 0 additions & 9 deletions src/assets/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,6 @@

.button-container{
display: flex;

.non-active{
background-color: #121212;
}

.active{
background-color: #dce9ed;
color: #121212;
}

button{
background-color: #121212;
Expand Down
15 changes: 1 addition & 14 deletions src/components/header.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,21 @@
<p>Chaty</p>
</div>
<div class="button-container">
<button :class="getSoundClass" @click="toggleSound">Sound</button>
<button @click="$emit('showSettings', true)">Settings</button>
</div>
</div>
</div>
</template>

<script lang="ts">
export default {
props: {
person: Object,
},
data(){
return{
isSoundOn: false
}
},
methods:{
toggleSound(){
this.isSoundOn = !this.isSoundOn
this.$store.dispatch({ type: 'toggleSound'})
}
},
computed:{
getSoundClass():string{
const soundClass = this.isSoundOn ? 'active' : 'non-active'
return soundClass
}
}
}
Expand Down
113 changes: 113 additions & 0 deletions src/components/settings.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<template>
<div>
<b-modal v-model="showModal" id="settingsMenu" title="Settings" ok-title="Save" @hide="$emit('settingsClosed')" @ok="handleOk">
<b-form-group
id="fieldset-horizontal"
label-cols="2"
content-cols="9"
label="Username:"
label-for="input-horizontal"
>
<b-form-input type="email" v-model="username" placeholder="Enter your email"/>
</b-form-group>
<b-form-group
id="fieldset-horizontal"
label-cols="2"
content-cols="9"
label="Password:"
label-for="input-horizontal"
>
<b-form-input type="password" v-model="password" placeholder="Enter your password"/>
</b-form-group>
<b-form-group
label="Account type:"
label-cols="3"
v-slot="{ ariaDescribedby }"
>
<b-form-radio-group
class="pt-2"
v-model="accountType"
:options="['Normal', 'Google', 'Microsoft']"
:aria-describedby="ariaDescribedby"
/>
</b-form-group>
<b-form-group
label="Read answers:"
label-cols="4"
label-for="input-horizontal"
>
<b-form-checkbox
id="checkbox-1"
v-model="shouldRead"
name="checkbox-1"
switch
size="lg"
/>
</b-form-group>
</b-modal>
</div>
</template>

<script lang="ts">
export default {
props: {
showSettings: Boolean
},
data(){
return{
showModal: false,
username: '',
password: '',
accountType: 'Normal',
shouldRead: false,
settingsChanged: false
}
},
async created() {
// Load settings from electron store
const settings = await window.ipcRenderer.send('electron-store-get', 'settings')
if (!settings.username) return
this.username = settings.username
this.password = settings.password
this.accountType = settings.accountType
this.shouldRead = settings.shouldRead
this.$watch('username', () => { this.settingsChanged = true })
this.$watch('password', () => { this.settingsChanged = true})
this.$watch('accountType', () => { this.settingsChanged = true})
},
methods:{
async handleOk() {
const settings = {
username: this.username,
password: this.password,
accountType: this.accountType,
shouldRead: this.shouldRead
}
await window.ipcRenderer.send('electron-store-set', 'settings', settings)
await this.$store.dispatch({ type: 'loadSettings' })
if (this.settingsChanged){
this.settingsChanged = false
await this.$store.dispatch({ type: 'initBackend' })
}
},
},
watch: {
showSettings: {
handler(newValue, oldValue) {
this.showModal = newValue
},
immediate: true
}
},
computed:{
}
}
</script>

<style>
</style>
Loading

0 comments on commit 9ade05b

Please sign in to comment.