Compare commits

..

18 commits
v1.0.1 ... main

Author SHA1 Message Date
Hugh Rundle 08c32b8647 Merge pull request 'update' (#9) from update into main
Reviewed-on: #9
2024-08-10 12:19:11 +10:00
Hugh Rundle a85e056b4b
Merge branch 'main' into update 2024-08-10 12:18:25 +10:00
Hugh Rundle e2ece557c2
add some more options
- add extra reply options
- fix mentions
2024-08-10 12:09:36 +10:00
Hugh Rundle 2a6cf879b6
update readme and deps 2024-08-10 11:44:48 +10:00
Hugh Rundle fa47c7a0fb
Update README.md 2024-07-29 20:16:28 +10:00
Hugh Rundle e10a3ef7ce
Merge pull request #7 from hughrun/dependabot/npm_and_yarn/ws-7.5.10
Bump ws from 7.5.2 to 7.5.10
2024-06-29 12:32:46 +10:00
dependabot[bot] 85d6589dce
Bump ws from 7.5.2 to 7.5.10
Bumps [ws](https://github.com/websockets/ws) from 7.5.2 to 7.5.10.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.2...7.5.10)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-29 02:12:09 +00:00
Hugh Rundle a99bd4bf92
Merge pull request #5 from hughrun/dependabot/npm_and_yarn/follow-redirects-1.15.6
Bump follow-redirects from 1.14.7 to 1.15.6
2024-04-15 15:45:30 +10:00
dependabot[bot] b86560f033
Bump follow-redirects from 1.14.7 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-15 05:40:05 +00:00
Hugh Rundle 391e7524ca
Update README.md 2022-10-23 21:22:16 +11:00
Hugh Rundle 7b9a90030a
Update README.md 2022-10-23 15:22:39 +11:00
Hugh Rundle 2bfa31907f
Merge pull request #4 from hughrun/dependabot/npm_and_yarn/follow-redirects-1.14.7
Bump follow-redirects from 1.14.1 to 1.14.7
2022-01-17 16:54:30 +11:00
dependabot[bot] d62209587e
Bump follow-redirects from 1.14.1 to 1.14.7
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.1 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.1...v1.14.7)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-17 05:32:17 +00:00
Hugh Rundle d51bd3014e
Merge pull request #3 from hughrun/deps
bump axios and version
2021-11-06 10:16:18 +11:00
Hugh Rundle c337614fe6
bump axios and version 2021-11-06 10:15:33 +11:00
Hugh Rundle e5728006bc improve error-checking and add username to some responses
Previously clippy was left in an unresponsive state if an authentication error occured, or in some cases if the remote server dropped the connection without sending a close() message. Clippy now terminates the socket connection on every connection error or authentication error, waits 5 seconds, and tries again.

Also added username to the response function, and added a couple of extra responses.
2021-07-09 18:48:15 +10:00
Hugh Rundle 0ba65f833c bump ws module 2021-07-09 18:47:50 +10:00
Hugh Rundle 852d96f635 bugfix
- clippy now ignores toots that mention clippy
- remove all mentions before checking toot text for topic
- normalise casing of toots for broader matching (probably only works for ASCII)
2021-05-28 20:52:24 +10:00
4 changed files with 127 additions and 69 deletions

View file

@ -45,6 +45,14 @@ Then run `npm install .` to install npm modules `axios` and `ws`.
Start the bot with the traditional `node index.js`. Start the bot with the traditional `node index.js`.
However, you probably want this to run automatically on a web server. You can do this with `systemd` using the example unit file: `mastodon-clippy.service.example`. Adjust this for your user and paths, and then activate it:
```bash
sudo cp mastodon-clippy.service.example /etc/systemd/system/mastodon-clippy.service
sudo systemctl enable mastodon-clippy.service
sudo systemctl start mastodon-clippy service
```
## interacting with the bot ## interacting with the bot
To "sign up" for notification from your bot, users have two options: To "sign up" for notification from your bot, users have two options:
@ -52,4 +60,4 @@ To "sign up" for notification from your bot, users have two options:
1. follow the bot account 1. follow the bot account
2. send a toot to the bot with the word `START` in capital letters. e.g. `@auspol_clippy START` 2. send a toot to the bot with the word `START` in capital letters. e.g. `@auspol_clippy START`
To "unsubscribe" from the bot, users can send a toot with the word `STOP` in capital letters. e.g. `@auspol_clippy STOP` To "unsubscribe" from the bot, users can send a toot with the word `STOP` in capital letters. e.g. `@auspol_clippy STOP`

156
index.js
View file

@ -1,4 +1,4 @@
/* /*
Clippy bot for mastodon. Clippy bot for mastodon.
@ -17,7 +17,7 @@ const WebSocket = require('ws')
// clippy settings // clippy settings
const access_token = process.env.CLIPPY_ACCESS_TOKEN const access_token = process.env.CLIPPY_ACCESS_TOKEN
const topic = process.env.CLIPPY_TOPIC const topic = process.env.CLIPPY_TOPIC.toLowerCase()
const clippy = process.env.CLIPPY_USER const clippy = process.env.CLIPPY_USER
const domain = process.env.CLIPPY_DOMAIN const domain = process.env.CLIPPY_DOMAIN
@ -26,8 +26,14 @@ const headers = {
'Authorization' : `Bearer ${access_token}` 'Authorization' : `Bearer ${access_token}`
} }
function resetConnection(socket) {
terminate(socket)
console.log(`waiting after error`)
setTimeout( function() { listen() }, 5000)
}
// set up bot account with correct settings // set up bot account with correct settings
function initiateSettings() { function initiateSettings(socket) {
let account = { let account = {
locked: false, locked: false,
@ -40,31 +46,50 @@ function initiateSettings() {
return axios.patch(`https://${domain}/api/v1/accounts/update_credentials`, account, { headers: headers }) return axios.patch(`https://${domain}/api/v1/accounts/update_credentials`, account, { headers: headers })
.catch( err => { .catch( err => {
console.error('ERROR applying bot user settings: ', err.message) console.error('ERROR applying bot user settings: ', err.message)
terminate(socket)
}) })
} }
function suggestion() { // return random suggestion string
function suggestion(username) {
const n = crypto.randomInt(4) const n = crypto.randomInt(12)
switch(n) { switch(n) {
case 0: case 0:
return 'How about logging off instead?'; return 'How about logging off instead?';
case 1: case 1:
return 'Would you like to delete your toot?'; return `Would you like to delete your toot, @${username}?`;
case 2: case 2:
return 'Can I help you take a walk outside?'; return 'Can I help you take a walk outside? 🚶‍➡️';
case 3: case 3:
return 'You may like to reconsider your life choices.'; return 'You may like to reconsider your life choices.';
case 4:
return 'Why not try looking at #CatsOfMastodon instead?';
case 5:
return `Come on @${username}, we've talked about this. 🤷‍♂️`;
case 6:
return `You should go look at some trees. Trees are calming 🌳`;
case 7:
return `I'm not angry. I'm just very disappointed. 😔`;
case 8:
return `You said you were going to stop doing that ...and yet here we are.`;
case 9:
return `Time to touch some grass 🌱`;
case 10:
return `Why not have a nice cup of tea instead? 🫖`;
case 11:
return `And yet you still haven't read all of those books in your TBR pile. 🤔`;
} }
} }
// send a message when someone toots about the topic // send a message when someone toots about the topic
function sendResponse(rip, user) { function sendResponse(rid, user, username) {
let payload = { let payload = {
'status' : `@${user} It looks like you're posting about '${topic}'. ${suggestion()}`, 'status' : `@${user} It looks like you're posting about '${topic}'. ${suggestion(username)}`,
'in_reply_to_id' : rip, 'spoiler_text' : topic,
'in_reply_to_id' : rid,
} }
axios.post(`https://${domain}/api/v1/statuses`, payload, { headers: headers }) axios.post(`https://${domain}/api/v1/statuses`, payload, { headers: headers })
@ -74,6 +99,7 @@ function sendResponse(rip, user) {
} }
// follow users who subscribe
function followAction(id, action) { function followAction(id, action) {
let url = `https://${domain}/api/v1/accounts/${id}/${action}` let url = `https://${domain}/api/v1/accounts/${id}/${action}`
@ -90,67 +116,91 @@ function followAction(id, action) {
} }
function filterMentions(text, mentions) {
// filter toot text to remove mentions before checking for the trigger word
// this means if your trigger word is in a user name, you don't get a tsunami of clippy advice
let rawArray = text.replace(/(<([^>]+)>)/gi, "").split(' ')
return rawArray.map( stub => mentions.some( name => `@${name}` === stub) ? "" : stub).toString()
}
// *********************** // ***********************
// STREAMING USER TIMELINE // STREAMING USER TIMELINE
// This is where the action is! // This is where the action is!
// *********************** // ***********************
const ws = new WebSocket(`wss://${domain}/api/v1/streaming?access_token=${access_token}&stream=user`)
// make sure bot is set up correctly each time it starts function terminate(socket) {
initiateSettings() console.error(`Terminating connection...`)
socket.terminate()
console.log(`Terminated`)
}
// errors function listen() {
ws.on('error', err => {
console.error(`WebSocket error: ${err.message}`)
})
// check updates and notifications in the stream console.log(`Listening...`)
ws.on('message', msg => { const ws = new WebSocket(`wss://${domain}/api/v1/streaming?access_token=${access_token}&stream=user`)
let packet = JSON.parse(msg)
let data = JSON.parse(packet.payload)
// notifications // make sure bot is set up correctly each time it starts
if (packet.event == 'notification') { initiateSettings(ws)
// always follow back // errors
if (data.type == 'follow') { ws.on('error', err => {
followAction(data.account.id, 'follow') console.error(`WebSocket error: ${err.message}`)
} resetConnection(ws)
})
if (data.type == 'mention') { // check updates and notifications in the stream
ws.on('message', msg => {
let packet = JSON.parse(msg)
let data = JSON.parse(packet.payload)
let post = data.status.content // notifications
if (packet.event == 'notification') {
// check start requests // always follow back
if (post.match(/\bSTART\b/)) { if (data.type == 'follow') {
followAction(data.account.id, 'follow') followAction(data.account.id, 'follow')
} }
// check stop requests if (data.type == 'mention') {
if (post.match(/\STOP\b/)) {
followAction(data.account.id, 'unfollow') let post = data.status.content
// check start requests
if (post.match(/\bSTART\b/)) {
followAction(data.account.id, 'follow')
}
// check stop requests
if (post.match(/\STOP\b/)) {
followAction(data.account.id, 'unfollow')
}
} }
} }
}
// updates (posts) // updates (posts)
if (packet.event == 'update') { if (packet.event == 'update') {
let rip = data.id let rid = data.id
let user = data.account.username let user = data.account.acct
let username = data.account.username
// exclude own toots to avoid an infinite loop // get just the account names (@name@domain.tld)
if (data.account.username !== clippy) { let mentions = data.mentions.map( mention => mention.acct)
if ( data.content.includes(topic) ) { // exclude own toots and @mentions to avoid an infinite loops
sendResponse(rip, user) if (username !== clippy && !mentions.includes(clippy)) {
} // get rid of mentions in case topic is within a username
else if (data.spoiler_text.includes(topic)) { let text = filterMentions(data.content, mentions)
sendResponse(rip, user) if ( text.toLowerCase().includes(topic) ) {
} sendResponse(rid, user, username)
else if (data.tags.includes(topic)) { }
sendResponse(rip, user) else if (data.spoiler_text.toLowerCase().includes(topic)) {
sendResponse(rid, user, username)
}
else if (data.tags.map(tag => tag.name.toLowerCase()).includes(topic)) {
sendResponse(rid, user, username)
}
} }
} }
} })
}) }
// let's go
listen()

22
package-lock.json generated
View file

@ -1,26 +1,26 @@
{ {
"name": "mastodon-clippy", "name": "mastodon-clippy",
"version": "1.0.1", "version": "1.1.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"axios": { "axios": {
"version": "0.21.1", "version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": { "requires": {
"follow-redirects": "^1.10.0" "follow-redirects": "^1.14.0"
} }
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.14.1", "version": "1.15.6",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
"integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
}, },
"ws": { "ws": {
"version": "7.4.5", "version": "7.5.10",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
"integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==" "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="
} }
} }
} }

View file

@ -1,8 +1,8 @@
{ {
"name": "mastodon-clippy", "name": "mastodon-clippy",
"version": "1.0.1", "version": "1.1.2",
"description": "Mastodon clippy bot", "description": "Mastodon clippy bot",
"repository": "https://github.com/hughrun/mastodon-clippy.git", "repository": "https://git.suboptimal.solutions/hugh/mastodon-clippy",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -10,7 +10,7 @@
"author": "Hugh Rundle <hugh@hughrundle.net> (https://www.hughrundle.net)", "author": "Hugh Rundle <hugh@hughrundle.net> (https://www.hughrundle.net)",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^1.7.3",
"ws": "^7.4.5" "ws": "^8.18"
} }
} }