nodebb-plugin-openai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +3 -0
- package/.gitattributes +22 -0
- package/LICENSE +21 -0
- package/README.md +23 -0
- package/commitlint.config.js +26 -0
- package/lib/controllers.js +9 -0
- package/library.js +174 -0
- package/package.json +47 -0
- package/plugin.json +15 -0
- package/public/lib/.eslintrc +3 -0
- package/public/lib/admin.js +17 -0
- package/renovate.json +5 -0
- package/screenshots/screenshot1.png +0 -0
- package/templates/admin/plugins/openai.tpl +49 -0
package/.eslintrc
ADDED
package/.gitattributes
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Auto detect text files and perform LF normalization
|
|
2
|
+
* text=auto
|
|
3
|
+
|
|
4
|
+
# Custom for Visual Studio
|
|
5
|
+
*.cs diff=csharp
|
|
6
|
+
*.sln merge=union
|
|
7
|
+
*.csproj merge=union
|
|
8
|
+
*.vbproj merge=union
|
|
9
|
+
*.fsproj merge=union
|
|
10
|
+
*.dbproj merge=union
|
|
11
|
+
|
|
12
|
+
# Standard to msysgit
|
|
13
|
+
*.doc diff=astextplain
|
|
14
|
+
*.DOC diff=astextplain
|
|
15
|
+
*.docx diff=astextplain
|
|
16
|
+
*.DOCX diff=astextplain
|
|
17
|
+
*.dot diff=astextplain
|
|
18
|
+
*.DOT diff=astextplain
|
|
19
|
+
*.pdf diff=astextplain
|
|
20
|
+
*.PDF diff=astextplain
|
|
21
|
+
*.rtf diff=astextplain
|
|
22
|
+
*.RTF diff=astextplain
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 NodeBB
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# OpenAI Plugin for NodeBB
|
|
2
|
+
|
|
3
|
+
This plugin let's users ask questions to ChatGPT in their forum posts by mentioning the chatgpt user with @<chat_gpt_username>. The user is created by admins and their username is set in the ACP page.
|
|
4
|
+
|
|
5
|
+
You can also allow users to send private messages to the chatgpt user.
|
|
6
|
+
|
|
7
|
+
If the chat gpt user is added to a public room it can answer questions by mentioning like in topics.
|
|
8
|
+
|
|
9
|
+
The model used can be changed in the ACP page.
|
|
10
|
+
|
|
11
|
+
Get an API key from openai at https://platform.openai.com/api-keys and save it in the ACP page of the plugin and restart your forum.
|
|
12
|
+
|
|
13
|
+
## Dependencies
|
|
14
|
+
|
|
15
|
+
This plugin requires nodebb-plugin-mentions to be installed and active.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
npm install nodebb-plugin-openai
|
|
20
|
+
|
|
21
|
+
## Screenshots
|
|
22
|
+
|
|
23
|
+

|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
extends: ['@commitlint/config-angular'],
|
|
5
|
+
rules: {
|
|
6
|
+
'header-max-length': [1, 'always', 72],
|
|
7
|
+
'type-enum': [
|
|
8
|
+
2,
|
|
9
|
+
'always',
|
|
10
|
+
[
|
|
11
|
+
'breaking',
|
|
12
|
+
'build',
|
|
13
|
+
'chore',
|
|
14
|
+
'ci',
|
|
15
|
+
'docs',
|
|
16
|
+
'feat',
|
|
17
|
+
'fix',
|
|
18
|
+
'perf',
|
|
19
|
+
'refactor',
|
|
20
|
+
'revert',
|
|
21
|
+
'style',
|
|
22
|
+
'test',
|
|
23
|
+
],
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
};
|
package/library.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { OpenAI } = require('openai');
|
|
4
|
+
|
|
5
|
+
let openai;
|
|
6
|
+
|
|
7
|
+
const db = require.main.require('./src/database');
|
|
8
|
+
const meta = require.main.require('./src/meta');
|
|
9
|
+
const controllers = require('./lib/controllers');
|
|
10
|
+
const routeHelpers = require.main.require('./src/routes/helpers');
|
|
11
|
+
const socketHelpers = require.main.require('./src/socket.io/helpers');
|
|
12
|
+
const topics = require.main.require('./src/topics');
|
|
13
|
+
const user = require.main.require('./src/user');
|
|
14
|
+
const messaging = require.main.require('./src/messaging');
|
|
15
|
+
const api = require.main.require('./src/api');
|
|
16
|
+
|
|
17
|
+
const plugin = module.exports;
|
|
18
|
+
|
|
19
|
+
plugin.init = async (params) => {
|
|
20
|
+
const { router /* , middleware , controllers */ } = params;
|
|
21
|
+
const settings = await meta.settings.get('openai')
|
|
22
|
+
if (settings && settings.apikey) {
|
|
23
|
+
openai = new OpenAI({
|
|
24
|
+
apiKey: settings.apikey,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
routeHelpers.setupAdminPageRoute(router, '/admin/plugins/openai', controllers.renderAdminPage);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
plugin.actionMentionsNotify = async function (hookData) {
|
|
32
|
+
try {
|
|
33
|
+
const { notification } = hookData;
|
|
34
|
+
if (!notification) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const settings = (await meta.settings.get('openai')) || {};
|
|
39
|
+
const chatgptusername = settings['chatgpt-username'];
|
|
40
|
+
const chatgptUid = await user.getUidByUsername(chatgptusername);
|
|
41
|
+
if (!chatgptUid) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (notification.tid && notification.bodyLong.startsWith(`@${chatgptusername}`)) {
|
|
45
|
+
const message = notification.bodyLong.replace(new RegExp(`^@${chatgptusername}`), '');
|
|
46
|
+
if (message.length) {
|
|
47
|
+
const response = await chatComplete(message);
|
|
48
|
+
|
|
49
|
+
if (response) {
|
|
50
|
+
const postData = await topics.reply({
|
|
51
|
+
uid: chatgptUid,
|
|
52
|
+
content: response,
|
|
53
|
+
tid: notification.tid,
|
|
54
|
+
toPid: notification.pid,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await user.updateOnlineUsers(chatgptUid);
|
|
58
|
+
await socketHelpers.notifyNew(chatgptUid, 'newPost', {
|
|
59
|
+
posts: [postData],
|
|
60
|
+
'reputation:disabled': meta.config['reputation:disabled'] === 1,
|
|
61
|
+
'downvote:disabled': meta.config['downvote:disabled'] === 1,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(err.stack);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
plugin.actionMessagingSave = async function (hookData) {
|
|
72
|
+
try {
|
|
73
|
+
const { message } = hookData;
|
|
74
|
+
if (message.system) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const settings = (await meta.settings.get('openai')) || {};
|
|
78
|
+
const chatgptusername = settings['chatgpt-username'];
|
|
79
|
+
const chatgptUid = await user.getUidByUsername(chatgptusername);
|
|
80
|
+
const { roomId, content, fromuid } = message;
|
|
81
|
+
// don't reply to self
|
|
82
|
+
if (parseInt(fromuid, 10) === parseInt(chatgptUid, 10)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const [roomData, inRoom] = await Promise.all([
|
|
87
|
+
messaging.getRoomData(roomId),
|
|
88
|
+
messaging.isUserInRoom(chatgptUid, roomId),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
if (!roomData || !inRoom) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const isPrivate = !roomData.public && !roomData.groupChat;
|
|
95
|
+
if (isPrivate && settings.enablePrivateMessages === 'off') {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const shouldReply = isPrivate || content.startsWith(`@${chatgptusername}`);
|
|
99
|
+
|
|
100
|
+
// only reply in 1v1 chats or in public group chats when @<chat-gpt-username> is used
|
|
101
|
+
if (!shouldReply) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let conversation = [{ role: 'user', content: message.content }];
|
|
105
|
+
if (isPrivate) {
|
|
106
|
+
const mids = await getMessageIds(roomId, chatgptUid, 0, 20);
|
|
107
|
+
|
|
108
|
+
// dont allow chats to get too long
|
|
109
|
+
if (mids.length > 20) {
|
|
110
|
+
await api.chats.post({ uid: chatgptUid, session: {} }, {
|
|
111
|
+
roomId,
|
|
112
|
+
message: 'Conversation too long, please start a new chat',
|
|
113
|
+
toMid: message.mid,
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let messages = await messaging.getMessagesFields(mids, ['fromuid', 'content', 'system']);
|
|
118
|
+
messages = messages.filter(m => m && !m.system);
|
|
119
|
+
conversation = messages.map(
|
|
120
|
+
(msg) => ({ role: msg.fromuid === chatgptUid ? 'assistant' : 'user', content: msg.content })
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const response = await chatComplete(conversation);
|
|
125
|
+
|
|
126
|
+
if (response) {
|
|
127
|
+
await api.chats.post({ uid: chatgptUid, session: {} }, {
|
|
128
|
+
roomId,
|
|
129
|
+
message: response,
|
|
130
|
+
toMid: isPrivate ? undefined : message.mid,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error(err.stack);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
async function getMessageIds(roomId, uid, start, stop) {
|
|
139
|
+
const isPublic = await db.getObjectField(`chat:room:${roomId}`, 'public');
|
|
140
|
+
if (parseInt(isPublic, 10) === 1) {
|
|
141
|
+
return await db.getSortedSetRange(
|
|
142
|
+
`chat:room:${roomId}:mids`, start, stop
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const userjoinTimestamp = await db.sortedSetScore(`chat:room:${roomId}:uids`, uid);
|
|
146
|
+
return await db.getSortedSetRangeByScore(
|
|
147
|
+
`chat:room:${roomId}:mids`, start, stop === -1 ? -1 : stop - start + 1, userjoinTimestamp, '+inf'
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function chatComplete(message) {
|
|
152
|
+
if (!openai) {
|
|
153
|
+
throw new Error('API not created!');
|
|
154
|
+
}
|
|
155
|
+
const isConversation = Array.isArray(message);
|
|
156
|
+
const { model } = await meta.settings.get('openai');
|
|
157
|
+
const chatCompletion = await openai.chat.completions.create({
|
|
158
|
+
model: model || 'gpt-3.5-turbo',
|
|
159
|
+
messages: isConversation ? message : [{ role: 'user', content: message }],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return chatCompletion.choices[0]?.message?.content;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
plugin.addAdminNavigation = (header) => {
|
|
166
|
+
header.plugins.push({
|
|
167
|
+
route: '/plugins/openai',
|
|
168
|
+
icon: 'fa-robot',
|
|
169
|
+
name: 'OpenAI',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return header;
|
|
173
|
+
};
|
|
174
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodebb-plugin-openai",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A NodeBB OpenAI plugin",
|
|
5
|
+
"main": "library.js",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/nodebb/nodebb-plugin-openai"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"nodebb",
|
|
12
|
+
"plugin",
|
|
13
|
+
"openai"
|
|
14
|
+
],
|
|
15
|
+
"husky": {
|
|
16
|
+
"hooks": {
|
|
17
|
+
"pre-commit": "lint-staged",
|
|
18
|
+
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"lint-staged": {
|
|
22
|
+
"*.js": [
|
|
23
|
+
"eslint --fix",
|
|
24
|
+
"git add"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/nodebb/nodebb-plugin-openai/issues"
|
|
30
|
+
},
|
|
31
|
+
"readmeFilename": "README.md",
|
|
32
|
+
"nbbpm": {
|
|
33
|
+
"compatibility": "^3.2.0"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"openai": "4.53.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@commitlint/cli": "19.3.0",
|
|
40
|
+
"@commitlint/config-angular": "19.3.0",
|
|
41
|
+
"eslint": "8.57.0",
|
|
42
|
+
"eslint-config-nodebb": "0.2.1",
|
|
43
|
+
"eslint-plugin-import": "2.29.1",
|
|
44
|
+
"husky": "9.0.11",
|
|
45
|
+
"lint-staged": "15.2.7"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "nodebb-plugin-openai",
|
|
3
|
+
"url": "https://github.com/NodeBB/nodebb-plugin-openai",
|
|
4
|
+
"library": "./library.js",
|
|
5
|
+
"hooks": [
|
|
6
|
+
{ "hook": "static:app.load", "method": "init" },
|
|
7
|
+
{ "hook": "filter:admin.header.build", "method": "addAdminNavigation" },
|
|
8
|
+
{ "hook": "action:mentions.notify", "method": "actionMentionsNotify" },
|
|
9
|
+
{ "hook": "action:messaging.save", "method": "actionMessagingSave" }
|
|
10
|
+
],
|
|
11
|
+
"modules": {
|
|
12
|
+
"../admin/plugins/openai.js": "./public/lib/admin.js"
|
|
13
|
+
},
|
|
14
|
+
"templates": "templates"
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { save, load } from 'settings';
|
|
4
|
+
|
|
5
|
+
export function init() {
|
|
6
|
+
handleSettingsForm();
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function handleSettingsForm() {
|
|
10
|
+
load('openai', $('.openai-settings'), function () {
|
|
11
|
+
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
$('#save').on('click', () => {
|
|
15
|
+
save('openai', $('.openai-settings'));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<div class="acp-page-container">
|
|
2
|
+
<!-- IMPORT admin/partials/settings/header.tpl -->
|
|
3
|
+
|
|
4
|
+
<div class="row m-0">
|
|
5
|
+
<div id="spy-container" class="col-12 col-md-8 px-0 mb-4" tabindex="0">
|
|
6
|
+
<form role="form" class="openai-settings">
|
|
7
|
+
<div class="mb-4">
|
|
8
|
+
<h5 class="fw-bold tracking-tight settings-header">General</h5>
|
|
9
|
+
|
|
10
|
+
<div class="mb-3">
|
|
11
|
+
<label class="form-label" for="apikey">API Key</label>
|
|
12
|
+
<input type="text" id="apikey" name="apikey" title="API Key" class="form-control">
|
|
13
|
+
<p class="form-text">
|
|
14
|
+
Get your <a href="https://platform.openai.com/api-keys">API Key</a> and enter it above. Requires a restart.
|
|
15
|
+
</p>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="mb-3">
|
|
18
|
+
<label class="form-label" for="chatgpt-username">ChatGPT Username</label>
|
|
19
|
+
<input type="text" id="chatgpt-username" name="chatgpt-username" title="ChatGPT Username" class="form-control">
|
|
20
|
+
<p class="form-text">
|
|
21
|
+
<a href="{config.relative_path}/admin/manage/users">Create a user</a> and enter their username. Other users can mention this user to ask questions to ChatGPT or send private messages if enabled below.
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="form-check form-switch">
|
|
26
|
+
<input type="checkbox" class="form-check-input" id="enablePrivateMessages" name="enablePrivateMessages">
|
|
27
|
+
<label for="enablePrivateMessages" class="form-check-label">Enable Private Messages</label>
|
|
28
|
+
<p class="form-text">
|
|
29
|
+
If enabled users can send ChatGPT user private messages.
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="mb-3">
|
|
34
|
+
<label class="form-label" for="model">Model</label>
|
|
35
|
+
<select class="form-select" id="model" name="model" title="Model">
|
|
36
|
+
<option value="gpt-3.5-turbo">gpt-3.5-turbo</option>
|
|
37
|
+
<option value="gpt-4o-mini">gpt-4o-mini</option>
|
|
38
|
+
<option value="gpt-4o">gpt-4o</option>
|
|
39
|
+
<option value="gpt-4-turbo">gpt-4-turbo</option>
|
|
40
|
+
<option value="gpt-4">gpt-4</option>
|
|
41
|
+
</select>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</form>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- IMPORT admin/partials/settings/toc.tpl -->
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|