Skip to content

Commit 6318bba

Browse files
committed
and ensure proper message ordering
1 parent a16f49b commit 6318bba

File tree

3 files changed

+180
-28
lines changed

3 files changed

+180
-28
lines changed

nextgen_bot.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Copyright (c) 2024 iiPython
2+
3+
# Modules
4+
import sys
5+
import typing
6+
from time import time
7+
from base64 import b64encode
8+
from urllib.parse import quote_plus
9+
from platform import python_version
10+
11+
from requests import Session
12+
13+
from nightwatch import __version__
14+
from nightwatch.bot import Client, Context
15+
from nightwatch.bot.client import AuthorizationFailed
16+
from nightwatch.logging import log
17+
18+
# Create client
19+
class NextgenerationBot(Client):
20+
def __init__(self) -> None:
21+
super().__init__()
22+
23+
# Extra data
24+
self.send_on_join = None
25+
self.session, self.cache = Session(), {"time": 0, "token": None}
26+
27+
# Handle now playing
28+
def get_spotify_access(self) -> None:
29+
with self.session.post(
30+
"https://accounts.spotify.com/api/token",
31+
data = "grant_type=client_credentials",
32+
headers = {
33+
"Authorization": f"Basic {b64encode(b'3f974573800a4ff5b325de9795b8e603:ff188d2860ff44baa57acc79c121a3b9').decode()}",
34+
"Content-Type": "application/x-www-form-urlencoded"
35+
}
36+
) as response:
37+
self.cache["time"] = time()
38+
self.cache["token"] = response.json()["access_token"]
39+
40+
log.info("test_bot", "Spotify access token has been regenerated!")
41+
42+
def get_now_playing(self) -> tuple[dict | None, str | None]:
43+
44+
# Should we regenerate our spotify token?
45+
if (time() - self.cache["time"]) >= 3550: # Not 3600, just for some wiggle room
46+
self.get_spotify_access()
47+
48+
# Fetch actual data
49+
with self.session.get("https://api.listenbrainz.org/1/user/iiPython/playing-now") as response:
50+
result = (response.json())["payload"]["listens"]
51+
52+
if not result:
53+
return None, None
54+
55+
result = result[0]
56+
57+
# Reorganize the result data
58+
tm = result["track_metadata"]
59+
result = {"artist": tm["artist_name"], "track": tm["track_name"], "album": tm["release_name"],}
60+
with self.session.get(
61+
f"https://api.spotify.com/v1/search?q={quote_plus(f'{result['artist']} {result['album']}')}&type=album&limit=1",
62+
headers = {
63+
"Authorization": f"Bearer {self.cache['token']}",
64+
"Content-Type": "application/x-www-form-urlencoded"
65+
}
66+
) as response:
67+
return result, (response.json())["albums"]["items"][0]["images"][-2]["url"]
68+
69+
# Handle rejoining (for changing name or hex)
70+
async def rejoin(self, username: typing.Optional[str] = None, hex: typing.Optional[str] = None) -> None:
71+
await self.close()
72+
await self.event_loop(username or self.user.name, hex or self.user.hex, self.address) # type: ignore
73+
74+
# Handle events
75+
async def on_connect(self, ctx: Context) -> None:
76+
log.info("test_bot", f"Connected to {ctx.rics.name}!")
77+
78+
async def on_message(self, ctx: Context) -> None:
79+
if self.send_on_join is not None:
80+
await ctx.send(self.send_on_join)
81+
self.send_on_join = None
82+
return
83+
84+
command = ctx.message.message
85+
if command[:2] != "p!":
86+
return
87+
88+
log.info("test_bot", f"'{ctx.user.name}' ran '{command[2:]}'!")
89+
match command[2:].split(" "):
90+
case ["help"]:
91+
await ctx.reply("Commands: p!help, p!eval, p!music, p!user, p!people, p!rename, p!set-hex, p!version")
92+
93+
case ["eval", *expression]:
94+
to_evaluate = " ".join(expression).strip("`")
95+
if not (to_evaluate.strip() and to_evaluate != expression):
96+
return await ctx.reply("Nothing was specified to run.")
97+
98+
try:
99+
return await ctx.reply(str(eval(to_evaluate, {"ctx": ctx})))
100+
101+
except Exception as e:
102+
return await ctx.reply(str(e))
103+
104+
case ["music"]:
105+
data, image = self.get_now_playing()
106+
if not (data and image):
107+
return await ctx.reply("iiPython isn't listening to anything right now.")
108+
109+
await ctx.send(f"iiPython is listening to **[{data['track']}](https://www.last.fm/music/{data['artist']}/_/{data['track']})** by **[{data['artist']}](https://www.last.fm/music/{data['artist']})** (on **[{data['album']}](https://www.last.fm/music/{data['artist']}/{data['album']})**).")
110+
await ctx.send(f"![{data['track']} by {data['artist']} cover art]({image})")
111+
112+
case ["user", *username]:
113+
client = next(filter(lambda u: u.name == " ".join(username), ctx.rics.users), None)
114+
if client is None:
115+
return await ctx.reply("Specified user doesn't *fucking* exist.")
116+
117+
await ctx.send(f"**Name:** {'🤖 ' if client.bot else '★ ' if client.admin else ''}{client.name} | **HEX Code:** #{client.hex}")
118+
119+
case ["rename" | "set-hex" as command, *response]:
120+
try:
121+
if not response:
122+
return await ctx.reply("Are you gonna specify a value or what?")
123+
124+
log.info("test_bot", f"Reauthenticated using name '{self.user.name}' and HEX code #{self.user.hex}!") # type: ignore
125+
await self.rejoin(
126+
" ".join(response) if command == "rename" else None,
127+
response[0] if command == "set-hex" else None
128+
)
129+
130+
except AuthorizationFailed as problem:
131+
if problem.json is not None:
132+
message = (problem.json.get("message") or problem.json["detail"][0]["msg"]).rstrip(".").lower()
133+
self.send_on_join = f"Failed to switch {'username' if command == 'rename' else 'hex code'} because '{message}'."
134+
135+
log.error("test_bot", "Failed to switch name or hex code!") # type: ignore
136+
await self.event_loop(self.user.name, self.user.hex, self.address) # type: ignore
137+
138+
case ["people"]:
139+
await ctx.send(f"There are {len(ctx.rics.users)} users: {', '.join(f'{u.name}{f' ({'admin' if u.admin else 'bot'})' if u.admin or u.bot else ''}' for u in ctx.rics.users)}")
140+
141+
case ["version"]:
142+
await ctx.reply(f"Running on Nightwatch v{__version__} using Python {python_version()}.")
143+
144+
case _:
145+
log.warn("test_bot", "Invalid command was ran, see above.")
146+
await ctx.reply("I have **no idea** what the *fuck* you just asked...")
147+
148+
NextgenerationBot().run(
149+
username = "Prism",
150+
hex = "126bf1",
151+
address = sys.argv[1]
152+
)

nightwatch/web/js/flows/connection.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,12 @@ export default class ConnectionManager {
5151
};
5252
switch (type) {
5353
case "message":
54-
this.callbacks.on_message(data);
54+
this.callbacks.on_message(data, false);
5555
break
5656

5757
case "rics-info":
5858
document.getElementById("server-name").innerText = data.name;
59-
for (const message of data["message-log"]) this.callbacks.on_message(message);
59+
for (const message of data["message-log"]) this.callbacks.on_message(message, true);
6060
for (const user of data["user-list"]) this.callbacks.handle_member("join", user);
6161
break;
6262

nightwatch/web/js/nightwatch.js

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const FILE_HANDLER = new FileHandler();
8484
window.location.reload(); // Fight me.
8585
});
8686
},
87-
on_message: async (message) => {
87+
on_message: async (message, historic) => {
8888
const current_time = TIME_FORMATTER.format(new Date(message.time * 1000));
8989

9090
// Check for anything hidden
@@ -119,34 +119,14 @@ const FILE_HANDLER = new FileHandler();
119119
};
120120
};
121121

122-
// Check for files
123122
const file_match = raw_attachment.match(new RegExp(`^https?:\/\/${address}\/file\/([a-zA-Z0-9_-]{21})\/.*$`));
124-
if (file_match) {
125-
function bytes_to_human(size) {
126-
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
127-
return +((size / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["B", "kB", "MB", "GB"][i];
128-
}
129-
const response = await (await fetch(`http${connection.protocol}://${address}/api/file/${file_match[1]}/info`)).json();
130-
if (response.code === 200) {
131-
const mimetype = FILE_HANDLER.mimetype(response.data.name);
132-
if (["avif", "avifs", "png", "apng", "jpg", "jpeg", "jfif", "webp", "ico", "gif", "svg"].includes(mimetype.toLowerCase())) {
133-
attachment = `<a href = "${attachment}" target = "_blank"><img alt = "${response.data.name}" src = "${attachment}"></a>`;
134-
} else {
135-
attachment = `<div class = "file">
136-
<div><span>${response.data.name}</span> <span>${mimetype}</span></div>
137-
<div><span>${bytes_to_human(response.data.size)}</span> <button data-uri="${attachment}">Download</button></div>
138-
</div>`;
139-
}
140-
classlist += " padded";
141-
}
142-
}
143123

144124
// Construct message
145125
const element = document.createElement("div");
146126
element.classList.add("message");
147127
element.innerHTML = `
148128
<span style = "color: #${message.user.hex};${hide_author ? 'color: transparent;' : ''}">${message.user.name}</span>
149-
<span class = "${classlist}">${attachment}</span>
129+
<span class = "${classlist}">${file_match ? "Loading attachment..." : attachment}</span>
150130
<span class = "timestamp"${current_time === last_time ? ' style="color: transparent;"' : ''}>${current_time}</span>
151131
`;
152132

@@ -156,12 +136,32 @@ const FILE_HANDLER = new FileHandler();
156136
chat.scrollTop = chat.scrollHeight;
157137
last_author = message.user.name, last_time = current_time;
158138

159-
// Handle downloading
160-
const button = element.querySelector("[data-uri]");
161-
if (button) button.addEventListener("click", () => { window.open(button.getAttribute("data-uri"), "_blank"); });
162-
163139
// Handle notification sound
164140
if (!document.hasFocus()) NOTIFICATION_SFX.play();
141+
142+
// Check for files
143+
if (file_match) {
144+
function bytes_to_human(size) {
145+
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
146+
return +((size / Math.pow(1024, i)).toFixed(2)) * 1 + " " + ["B", "kB", "MB", "GB"][i];
147+
}
148+
149+
const response = await (await fetch(`http${connection.protocol}://${address}/api/file/${file_match[1]}/info`)).json();
150+
if (response.code === 200) {
151+
const message = element.querySelector(".message-content");
152+
const mimetype = FILE_HANDLER.mimetype(response.data.name);
153+
if (["avif", "avifs", "png", "apng", "jpg", "jpeg", "jfif", "webp", "ico", "gif", "svg"].includes(mimetype.toLowerCase())) {
154+
message.innerHTML = `<a href = "${attachment}" target = "_blank"><img alt = "${response.data.name}" src = "${attachment}"></a>`;
155+
} else {
156+
message.innerHTML = `<div class = "file">
157+
<div><span>${response.data.name}</span> <span>${mimetype}</span></div>
158+
<div><span>${bytes_to_human(response.data.size)}</span> <button>Download</button></div>
159+
</div>`;
160+
message.querySelector("button").addEventListener("click", () => { window.open(attachment, "_blank"); });
161+
}
162+
message.classList.add("padded");
163+
}
164+
}
165165
},
166166
handle_member: (event_type, member) => {
167167
const member_list = document.querySelector(".member-list");

0 commit comments

Comments
 (0)