this repo has no description
1const std = @import("std");
2const uuid = @import("uuid");
3const xev = @import("xev");
4const zeit = @import("zeit");
5
6const db = @import("db.zig");
7const http = @import("http.zig");
8const log = @import("log.zig");
9const sanitize = @import("sanitize.zig");
10
11const Allocator = std.mem.Allocator;
12const Connection = Server.Connection;
13const HeapArena = @import("HeapArena.zig");
14const Queue = @import("queue.zig").Queue;
15const Server = @import("Server.zig");
16const Http = @import("http.zig");
17
18const assert = std.debug.assert;
19
20// Global user modes
21const UserMode = packed struct {
22 operator: bool = false, // +o, global mod
23
24 const none: UserMode = .{};
25};
26
27// Channel modes. We can add "private" channels for example
28const ChannelMode = packed struct {};
29
30pub const ChannelPrivileges = packed struct {
31 operator: bool = false, // +o, channel mod
32
33 const none: ChannelPrivileges = .{};
34};
35
36pub const SaslMechanism = enum {
37 plain,
38};
39
40const Sasl = union(SaslMechanism) {
41 plain: struct {
42 username: []const u8,
43 password: []const u8,
44 },
45};
46
47pub const Capability = enum {
48 @"away-notify",
49 batch,
50 @"draft/chathistory",
51 @"draft/no-implicit-names",
52 @"draft/read-marker",
53 @"echo-message",
54 @"message-tags",
55 sasl,
56 @"server-time",
57};
58
59pub const ChatHistory = struct {
60 pub const TargetsRequest = struct {
61 from: Timestamp,
62 to: Timestamp,
63 limit: u16,
64 };
65
66 pub const AfterRequest = struct {
67 target: []const u8,
68 after_ms: Timestamp,
69 limit: u16,
70 };
71
72 pub const BeforeRequest = struct {
73 conn: *Connection,
74 target: []const u8,
75 before_ms: Timestamp,
76 limit: u16,
77 };
78
79 pub const LatestRequest = struct {
80 conn: *Connection,
81 target: []const u8,
82 limit: u16,
83 };
84
85 pub const HistoryMessage = struct {
86 uuid: []const u8,
87 timestamp: Timestamp,
88 sender: []const u8,
89 message: []const u8,
90
91 pub fn lessThan(_: void, lhs: HistoryMessage, rhs: HistoryMessage) bool {
92 return lhs.timestamp.milliseconds < rhs.timestamp.milliseconds;
93 }
94 };
95
96 pub const HistoryBatch = struct {
97 arena: HeapArena,
98 fd: xev.TCP,
99 items: []HistoryMessage,
100 target: []const u8,
101 };
102
103 pub const Target = struct {
104 nick_or_channel: []const u8,
105 latest_timestamp: Timestamp,
106 };
107
108 pub const TargetBatch = struct {
109 arena: HeapArena,
110 fd: xev.TCP,
111 items: []Target,
112 };
113};
114
115pub const Message = struct {
116 bytes: []const u8,
117 timestamp: Timestamp,
118 uuid: uuid.Uuid,
119
120 pub fn init(bytes: []const u8) Message {
121 return .{
122 .bytes = bytes,
123 .timestamp = .init(),
124 .uuid = uuid.v4.new(),
125 };
126 }
127
128 pub fn copy(self: Message, alloc: Allocator) Allocator.Error!Message {
129 return .{
130 .bytes = try alloc.dupe(u8, self.bytes),
131 .timestamp = self.timestamp,
132 .uuid = self.uuid,
133 };
134 }
135
136 pub const ParamIterator = struct {
137 params: ?[]const u8,
138 index: usize = 0,
139
140 pub fn next(self: *ParamIterator) ?[]const u8 {
141 const params = self.params orelse return null;
142 if (self.index >= params.len) return null;
143
144 // consume leading whitespace
145 while (self.index < params.len) {
146 if (params[self.index] != ' ') break;
147 self.index += 1;
148 }
149
150 const start = self.index;
151 if (start >= params.len) return null;
152
153 // If our first byte is a ':', we return the rest of the string as a
154 // single param (or the empty string)
155 if (params[start] == ':') {
156 self.index = params.len;
157 if (start == params.len - 1) {
158 return "";
159 }
160 return params[start + 1 ..];
161 }
162
163 // Find the first index of space. If we don't have any, the reset of
164 // the line is the last param
165 self.index = std.mem.indexOfScalarPos(u8, params, self.index, ' ') orelse {
166 defer self.index = params.len;
167 return params[start..];
168 };
169
170 return params[start..self.index];
171 }
172 };
173
174 pub const Tag = struct {
175 key: []const u8,
176 value: []const u8,
177 };
178
179 pub const TagIterator = struct {
180 tags: []const u8,
181 index: usize = 0,
182
183 // tags are a list of key=value pairs delimited by semicolons.
184 // key[=value] [; key[=value]]
185 pub fn next(self: *TagIterator) ?Tag {
186 if (self.index >= self.tags.len) return null;
187
188 // find next delimiter
189 const end = std.mem.indexOfScalarPos(u8, self.tags, self.index, ';') orelse self.tags.len;
190 var kv_delim = std.mem.indexOfScalarPos(u8, self.tags, self.index, '=') orelse end;
191 // it's possible to have tags like this:
192 // @bot;account=botaccount;+typing=active
193 // where the first tag doesn't have a value. Guard against the
194 // kv_delim being past the end position
195 if (kv_delim > end) kv_delim = end;
196
197 defer self.index = end + 1;
198
199 return .{
200 .key = self.tags[self.index..kv_delim],
201 .value = if (end == kv_delim) "" else self.tags[kv_delim + 1 .. end],
202 };
203 }
204 };
205
206 pub fn tagIterator(msg: Message) TagIterator {
207 const src = msg.bytes;
208 if (src[0] != '@') return .{ .tags = "" };
209
210 assert(src.len > 1);
211 const n = std.mem.indexOfScalarPos(u8, src, 1, ' ') orelse src.len;
212 return .{ .tags = src[1..n] };
213 }
214
215 pub fn source(msg: Message) ?[]const u8 {
216 const src = msg.bytes;
217 var i: usize = 0;
218
219 // get past tags
220 if (src[0] == '@') {
221 assert(src.len > 1);
222 i = std.mem.indexOfScalarPos(u8, src, 1, ' ') orelse return null;
223 }
224
225 // consume whitespace
226 while (i < src.len) : (i += 1) {
227 if (src[i] != ' ') break;
228 }
229
230 // Start of source
231 if (src[i] == ':') {
232 assert(src.len > i);
233 i += 1;
234 const end = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse src.len;
235 return src[i..end];
236 }
237
238 return null;
239 }
240
241 pub fn command(msg: Message) []const u8 {
242 if (msg.bytes.len == 0) return "";
243 const src = msg.bytes;
244 var i: usize = 0;
245
246 // get past tags
247 if (src[0] == '@') {
248 assert(src.len > 1);
249 i = std.mem.indexOfScalarPos(u8, src, 1, ' ') orelse return "";
250 }
251 // consume whitespace
252 while (i < src.len) : (i += 1) {
253 if (src[i] != ' ') break;
254 }
255
256 // get past source
257 if (src[i] == ':') {
258 assert(src.len > i);
259 i += 1;
260 i = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse return "";
261 }
262 // consume whitespace
263 while (i < src.len) : (i += 1) {
264 if (src[i] != ' ') break;
265 }
266
267 assert(src.len > i);
268 // Find next space
269 const end = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse src.len;
270
271 return src[i..end];
272 }
273
274 pub fn paramIterator(msg: Message) ParamIterator {
275 return .{ .params = msg.rawParameters() };
276 }
277
278 pub fn rawParameters(msg: Message) []const u8 {
279 const src = msg.bytes;
280 var i: usize = 0;
281
282 // get past tags
283 if (src[0] == '@') {
284 i = std.mem.indexOfScalarPos(u8, src, 0, ' ') orelse return "";
285 }
286 // consume whitespace
287 while (i < src.len) : (i += 1) {
288 if (src[i] != ' ') break;
289 }
290
291 // get past source
292 if (src[i] == ':') {
293 assert(src.len > i);
294 i += 1;
295 i = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse return "";
296 }
297 // consume whitespace
298 while (i < src.len) : (i += 1) {
299 if (src[i] != ' ') break;
300 }
301
302 // get past command
303 i = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse return "";
304
305 assert(src.len > i);
306 return src[i + 1 ..];
307 }
308
309 /// Returns the value of the tag 'key', if present
310 pub fn getTag(self: Message, key: []const u8) ?[]const u8 {
311 var tag_iter = self.tagIterator();
312 while (tag_iter.next()) |tag| {
313 if (!std.mem.eql(u8, tag.key, key)) continue;
314 return tag.value;
315 }
316 return null;
317 }
318
319 pub fn compareTime(_: void, lhs: Message, rhs: Message) bool {
320 return lhs.timestamp_ms < rhs.timestamp_ms;
321 }
322};
323
324pub const MessageIterator = struct {
325 bytes: []const u8,
326 index: usize = 0,
327
328 /// Returns the next message. Trailing \r\n is is removed
329 pub fn next(self: *MessageIterator) ?[]const u8 {
330 if (self.index >= self.bytes.len) return null;
331 const n = std.mem.indexOfScalarPos(u8, self.bytes, self.index, '\n') orelse return null;
332 defer self.index = n + 1;
333 return std.mem.trimRight(u8, self.bytes[self.index..n], "\r\n");
334 }
335
336 pub fn bytesRead(self: MessageIterator) usize {
337 return self.index;
338 }
339};
340
341pub const ClientMessage = enum {
342 // Connection Messages
343 CAP,
344 AUTHENTICATE,
345 PASS,
346 NICK,
347 USER,
348 PING,
349 PONG,
350 OPER,
351 QUIT,
352 ERROR,
353
354 // Channel Ops
355 JOIN,
356 PART,
357 TOPIC,
358 NAMES,
359 LIST,
360 INVITE,
361 KICK,
362
363 // Server queries and commands
364 MOTD,
365 VERSION,
366 ADMIN,
367 CONNECT,
368 LUSERS,
369 TIME,
370 STATS,
371 HELP,
372 INFO,
373 MODE,
374
375 // Sending messages
376 PRIVMSG,
377 NOTICE,
378 TAGMSG,
379
380 // User-based queries
381 WHO,
382 WHOIS,
383 WHOWAS,
384
385 // Operator messages
386 KILL,
387 REHASH,
388 RESTART,
389 SQUIT,
390
391 // Optional messages
392 AWAY,
393 LINKS,
394 USERHOST,
395 WALLOPS,
396
397 // Extensions
398 CHATHISTORY,
399 MARKREAD,
400
401 pub fn fromString(str: []const u8) ?ClientMessage {
402 inline for (@typeInfo(ClientMessage).@"enum".fields) |enumField| {
403 if (std.ascii.eqlIgnoreCase(str, enumField.name)) {
404 return @field(ClientMessage, enumField.name);
405 }
406 }
407 return null;
408 }
409};
410
411pub const User = struct {
412 nick: []const u8,
413 username: []const u8,
414 real: []const u8,
415 avatar_url: []const u8,
416 modes: UserMode,
417
418 away: bool,
419
420 connections: std.ArrayListUnmanaged(*Connection),
421 channels: std.ArrayListUnmanaged(*Channel),
422
423 pub fn init() User {
424 return .{
425 .nick = "",
426 .username = "",
427 .real = "",
428 .avatar_url = "",
429 .connections = .empty,
430 .channels = .empty,
431 .away = false,
432 .modes = .none,
433 };
434 }
435
436 pub fn deinit(self: *User, gpa: Allocator) void {
437 gpa.free(self.nick);
438 gpa.free(self.username);
439 gpa.free(self.real);
440 gpa.free(self.avatar_url);
441 self.connections.deinit(gpa);
442 self.channels.deinit(gpa);
443 }
444
445 pub fn isAway(self: *User) bool {
446 return self.away or self.connections.items.len == 0;
447 }
448};
449
450pub const Channel = struct {
451 name: []const u8,
452 topic: []const u8,
453 members: std.ArrayListUnmanaged(Member),
454 streams: std.ArrayListUnmanaged(*http.EventStream),
455
456 state: State = .{},
457
458 // We store some state so we can format messages to event streams better. The event stream
459 // clients are stateless, we just send them messages and they render it
460 const State = struct {
461 last_sender: ?*User = null,
462 last_timestamp: Timestamp = .{ .milliseconds = 0 },
463 };
464
465 const Member = struct {
466 user: *User,
467 privileges: ChannelPrivileges,
468 };
469
470 pub fn init(name: []const u8, topic: []const u8) Channel {
471 return .{
472 .name = name,
473 .topic = topic,
474 .members = .empty,
475 .streams = .empty,
476 };
477 }
478
479 pub fn deinit(self: *Channel, gpa: Allocator) void {
480 gpa.free(self.name);
481 gpa.free(self.topic);
482 self.members.deinit(gpa);
483 self.web_event_queues.deinit(gpa);
484 self.streams.deinit(gpa);
485 }
486
487 pub fn addUser(self: *Channel, server: *Server, user: *User, new_conn: *Connection) Allocator.Error!void {
488 log.debug("user={s} joining {s}", .{ user.nick, self.name });
489 // First, we see if the User is already in this channel
490 for (self.members.items) |u| {
491 if (u.user == user) {
492 // The user is already here. We just need to send the new connection a JOIN and NAMES
493 try new_conn.print(server.gpa, ":{s} JOIN {s}\r\n", .{ user.nick, self.name });
494
495 if (self.topic.len > 0) {
496 // Send the topic
497 try new_conn.print(
498 server.gpa,
499 ":{s} 332 {s} {s} :{s}\r\n",
500 .{ server.hostname, user.nick, self.name, self.topic },
501 );
502 }
503
504 // Next we see if this user needs to have an implicit names sent
505 if (new_conn.caps.@"draft/no-implicit-names") return;
506
507 // Send implicit NAMES
508 return self.names(server, new_conn);
509 }
510 }
511
512 // Next we add them
513 try self.members.append(server.gpa, .{ .user = user, .privileges = .none });
514 // Add the channel to the users list of channels
515 try user.channels.append(server.gpa, self);
516
517 // Next we tell everyone about this user joining
518 for (self.members.items) |u| {
519 for (u.user.connections.items) |conn| {
520 try conn.print(server.gpa, ":{s} JOIN {s}\r\n", .{ user.nick, self.name });
521 try server.queueWrite(conn.client, conn);
522 }
523 }
524
525 // This user just joined the channel, so we need to handle implicit names for each
526 // connection so all of the users connections receive the same information
527 for (user.connections.items) |conn| {
528 // Send the topic
529 if (self.topic.len > 0) {
530 // Send the topic
531 try new_conn.print(
532 server.gpa,
533 ":{s} 332 {s} {s} :{s}\r\n",
534 .{ server.hostname, user.nick, self.name, self.topic },
535 );
536 }
537 // See if this connection needs to have an implicit names sent
538 if (conn.caps.@"draft/no-implicit-names") continue;
539
540 // Send implicit NAMES
541 try self.names(server, conn);
542 }
543
544 try server.thread_pool.spawn(db.createChannelMembership, .{ server.db_pool, self.name, user.nick });
545 }
546
547 /// Notifies anyone in the channel with away-notify that the user is away
548 pub fn notifyAway(self: *Channel, server: *Server, user: *User) Allocator.Error!void {
549 for (self.members.items) |u| {
550 for (u.user.connections.items) |c| {
551 if (!c.caps.@"away-notify") continue;
552 try c.print(
553 server.gpa,
554 ":{s} AWAY :{s} is away\r\n",
555 .{ user.nick, user.nick },
556 );
557 try server.queueWrite(c.client, c);
558 }
559 }
560 }
561
562 /// Notifies anyone in the channel with away-notify that the user is back
563 pub fn notifyBack(self: *Channel, server: *Server, user: *User) Allocator.Error!void {
564 for (self.members.items) |m| {
565 const u = m.user;
566 for (u.connections.items) |c| {
567 if (!c.caps.@"away-notify") continue;
568 try c.print(
569 server.gpa,
570 ":{s} AWAY\r\n",
571 .{user.nick},
572 );
573 try server.queueWrite(c.client, c);
574 }
575 }
576 }
577
578 pub fn setTopic(self: *Channel, server: *Server, conn: *Connection, topic: []const u8) Allocator.Error!void {
579 const user = conn.user orelse return;
580 allowed: {
581 // Network operators are always allowed to change the topic
582 if (user.modes.operator) break :allowed;
583
584 // First check if this user has permissions
585 for (self.members.items) |m| {
586 if (m.user == user and m.privileges.operator) {
587 break :allowed;
588 }
589 }
590 return server.errChanOpPrivsNeeded(conn, self.name);
591 }
592
593 // We have permissions to set the topic
594 const old = self.topic;
595 self.topic = try server.gpa.dupe(u8, topic);
596 server.gpa.free(old);
597
598 try server.thread_pool.spawn(db.updateTopic, .{ server, self.name, topic });
599
600 // Tell all the users
601 for (self.members.items) |m| {
602 for (m.user.connections.items) |c| {
603 try c.print(
604 server.gpa,
605 ":{s} TOPIC {s} :{s}\r\n",
606 .{ server.hostname, self.name, self.topic },
607 );
608 try server.queueWrite(c.client, c);
609 }
610 }
611 }
612
613 // Removes the user from the channel. Sends a PART to all members
614 pub fn removeUser(self: *Channel, server: *Server, user: *User) Allocator.Error!void {
615 for (self.members.items, 0..) |m, i| {
616 const u = m.user;
617 if (u == user) {
618 _ = self.members.swapRemove(i);
619 break;
620 }
621 } else {
622 for (user.connections.items) |conn| {
623 try conn.print(
624 server.gpa,
625 ":{s} 442 {s} {s} :You're not in that channel\r\n",
626 .{ server.hostname, conn.nickname(), self.name },
627 );
628 }
629 return;
630 }
631
632 // Spawn a thread to remove the membership from the db
633 try server.thread_pool.spawn(db.removeChannelMembership, .{ server.db_pool, self.name, user.nick });
634
635 // Remove the channel from the user struct
636 for (user.channels.items, 0..) |uc, i| {
637 if (uc == self) {
638 _ = user.channels.swapRemove(i);
639 }
640 }
641
642 // Send a PART message to all members
643 for (self.members.items) |m| {
644 const u = m.user;
645 for (u.connections.items) |c| {
646 try c.print(
647 server.gpa,
648 ":{s} PART {s} :User left\r\n",
649 .{ user.nick, self.name },
650 );
651 try server.queueWrite(c.client, c);
652 }
653 }
654
655 // Send a PART to the user who left too
656 for (user.connections.items) |c| {
657 try c.print(
658 server.gpa,
659 ":{s} PART {s} :User left\r\n",
660 .{ user.nick, self.name },
661 );
662 }
663 }
664
665 pub fn names(self: *Channel, server: *Server, conn: *Connection) Allocator.Error!void {
666 for (self.members.items) |us| {
667 try conn.print(
668 server.gpa,
669 ":{s} 353 {s} = {s} :{s}\r\n",
670 .{ server.hostname, conn.nickname(), self.name, us.user.nick },
671 );
672 }
673 try conn.print(
674 server.gpa,
675 ":{s} 366 {s} {s} :End of names list\r\n",
676 .{ server.hostname, conn.nickname(), self.name },
677 );
678 try server.queueWrite(conn.client, conn);
679 }
680
681 pub fn who(self: *Channel, server: *Server, conn: *Connection, msg: Message) Allocator.Error!void {
682 const client: []const u8 = if (conn.user) |user| user.nick else "*";
683 var iter = msg.paramIterator();
684 _ = iter.next(); // We already have the first param (the target)
685
686 // Get the WHOX args, if there aren't any we can use an empty string for the same logic
687 const args = iter.next() orelse "";
688 const token = iter.next();
689
690 if (args.len == 0) {
691 for (self.members.items) |member| {
692 const user = member.user;
693 var flag_buf: [3]u8 = undefined;
694 var flag_len: usize = 1;
695 flag_buf[0] = if (user.isAway()) 'G' else 'H';
696 if (user.modes.operator) {
697 flag_buf[flag_len] = '*';
698 flag_len += 1;
699 }
700 if (member.privileges.operator) {
701 flag_buf[flag_len] = '@';
702 flag_len += 1;
703 }
704
705 const flags = flag_buf[0..flag_len];
706 try conn.print(
707 server.gpa,
708 ":{s} 352 {s} {s} {s} {s} {s} {s} {s} :0 {s}\r\n",
709 .{
710 server.hostname,
711 client,
712 self.name,
713 user.username,
714 server.hostname,
715 server.hostname,
716 user.nick,
717 flags,
718 user.real,
719 },
720 );
721 }
722 } else {
723 for (self.members.items) |member| {
724 const user = member.user;
725 try conn.print(
726 server.gpa,
727 ":{s} 354 {s}",
728 .{ server.hostname, client },
729 );
730
731 // Find the index of the standard field indicator
732 const std_idx = std.mem.indexOfScalar(u8, args, '%') orelse args.len;
733 // TODO: any nonstandard fields
734
735 // Handle standard fields, in order. The order is tcuihsnfdlaor
736 if (std.mem.indexOfScalarPos(u8, args, std_idx, 't')) |_| {
737 if (token) |t| try conn.print(server.gpa, " {s}", .{t});
738 }
739 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'c')) |_| {
740 try conn.print(server.gpa, " {s}", .{self.name});
741 }
742 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'u')) |_| {
743 try conn.print(server.gpa, " {s}", .{user.username});
744 }
745 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'i')) |_| {
746 try conn.print(server.gpa, " {s}", .{"127.0.0.1"});
747 }
748 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'h')) |_| {
749 try conn.print(server.gpa, " {s}", .{server.hostname});
750 }
751 if (std.mem.indexOfScalarPos(u8, args, std_idx, 's')) |_| {
752 try conn.print(server.gpa, " {s}", .{server.hostname});
753 }
754 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'n')) |_| {
755 try conn.print(server.gpa, " {s}", .{user.nick});
756 }
757 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'f')) |_| {
758 const flag = if (user.isAway()) "G" else "H";
759 try conn.print(server.gpa, " {s}", .{flag});
760 if (user.modes.operator) {
761 try conn.print(server.gpa, "{s}", .{"*"});
762 }
763 if (member.privileges.operator) {
764 try conn.print(server.gpa, "{s}", .{"@"});
765 }
766 }
767 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'd')) |_| {
768 try conn.write(server.gpa, " 0");
769 }
770 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'l')) |_| {
771 try conn.write(server.gpa, " 0");
772 }
773 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'a')) |_| {
774 try conn.print(server.gpa, " {s}", .{user.username});
775 }
776 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'o')) |_| {
777 // TODO: chan op level
778 try conn.print(server.gpa, " {s}", .{user.username});
779 }
780 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'r')) |_| {
781 try conn.print(server.gpa, " :{s}", .{user.real});
782 }
783 try conn.write(server.gpa, "\r\n");
784 }
785 }
786 try conn.print(
787 server.gpa,
788 ":{s} 315 {s} {s} :End of WHO list\r\n",
789 .{ server.hostname, client, self.name },
790 );
791 try server.queueWrite(conn.client, conn);
792 }
793
794 pub fn getPrivileges(self: *Channel, user: *User) ChannelPrivileges {
795 for (self.members.items) |m| {
796 if (m.user == user) {
797 return m.privileges;
798 }
799 }
800 return .none;
801 }
802
803 /// Updates the privileges of user. Saves to the db
804 pub fn storePrivileges(self: *Channel, server: *Server, user: *User, privs: ChannelPrivileges) !void {
805 for (self.members.items) |*m| {
806 if (m.user == user) {
807 m.privileges = privs;
808 // Save to the db
809 try server.thread_pool.spawn(
810 db.updatePrivileges,
811 .{ server.db_pool, user, privs, self.name },
812 );
813 return;
814 }
815 }
816 }
817
818 pub fn sendPrivMsgToStreams(
819 self: *Channel,
820 server: *Server,
821 sender: *User,
822 msg: Message,
823 ) Allocator.Error!void {
824 // We'll write the format once into buf. Then copy this to each stream for writing to the
825 // stream
826 var buf: std.ArrayListUnmanaged(u8) = .empty;
827 defer buf.deinit(server.gpa);
828 var writer = buf.writer(server.gpa);
829
830 const sender_sanitized: sanitize.Html = .{ .bytes = sender.nick };
831
832 defer {
833 // save the state
834 self.state.last_sender = sender;
835 self.state.last_timestamp = msg.timestamp;
836 }
837
838 // Parse the message
839 var iter = msg.paramIterator();
840 _ = iter.next(); // we can ignore the target
841 const content = iter.next() orelse return;
842 const content_sanitized: sanitize.Html = .{ .bytes = content };
843
844 // We don't reprint the sender if the last message this message are from the same
845 // person. Unless enough time has elapsed (5 minutes)
846 if (self.state.last_sender == sender and
847 (self.state.last_timestamp.milliseconds + 5 * std.time.ms_per_min) >= msg.timestamp.milliseconds)
848 {
849 const fmt =
850 \\event: message
851 \\data: <div class="message"><p class="body">{s}</p></div>
852 \\
853 \\
854 ;
855 try writer.print(fmt, .{content_sanitized});
856 } else {
857 const fmt =
858 \\event: message
859 \\data: <div class="message"><p class="nick"><b>{s}</b></p><p class="body">{s}</p></div>
860 \\
861 \\
862 ;
863 try writer.print(fmt, .{ sender_sanitized, content_sanitized });
864 }
865
866 // Now the buf has the text we want to send to each stream.
867 for (self.streams.items) |stream| {
868 try stream.writeAll(server.gpa, buf.items);
869 try server.queueWriteEventStream(stream);
870 }
871 }
872};
873
874pub const Timestamp = struct {
875 milliseconds: i64,
876
877 pub fn init() Timestamp {
878 return .{ .milliseconds = std.time.milliTimestamp() };
879 }
880
881 pub fn format(
882 self: Timestamp,
883 comptime fmt: []const u8,
884 options: std.fmt.FormatOptions,
885 writer: anytype,
886 ) !void {
887 _ = options;
888 _ = fmt;
889 const instant = zeit.instant(
890 .{ .source = .{ .unix_nano = self.milliseconds * std.time.ns_per_ms } },
891 ) catch unreachable;
892 const time = instant.time();
893 try writer.print(
894 "{d}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}Z",
895 .{
896 time.year,
897 @intFromEnum(time.month),
898 time.day,
899 time.hour,
900 time.minute,
901 time.second,
902 time.millisecond,
903 },
904 );
905 }
906};