Pārlūkot izejas kodu

Merge pull request #504 from slingamn/playback.4

support znc.in/playback
tags/v1.1.0-rc1
Daniel Oaks 5 gadus atpakaļ
vecāks
revīzija
678c8606b6
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam
9 mainītis faili ar 186 papildinājumiem un 21 dzēšanām
  1. 6
    0
      gencapdefs.py
  2. 6
    1
      irc/caps/defs.go
  3. 32
    16
      irc/channel.go
  4. 2
    0
      irc/client.go
  5. 4
    0
      irc/commands.go
  6. 14
    4
      irc/handlers.go
  7. 7
    0
      irc/help.go
  8. 17
    0
      irc/misc_test.go
  9. 98
    0
      irc/znc.go

+ 6
- 0
gencapdefs.py Parādīt failu

165
         url="https://github.com/ircv3/ircv3-specifications/pull/362",
165
         url="https://github.com/ircv3/ircv3-specifications/pull/362",
166
         standard="Proposed IRCv3",
166
         standard="Proposed IRCv3",
167
     ),
167
     ),
168
+    CapDef(
169
+        identifier="ZNCPlayback",
170
+        name="znc.in/playback",
171
+        url="https://wiki.znc.in/Playback",
172
+        standard="ZNC vendor",
173
+    ),
168
 ]
174
 ]
169
 
175
 
170
 def validate_defs():
176
 def validate_defs():

+ 6
- 1
irc/caps/defs.go Parādīt failu

7
 
7
 
8
 const (
8
 const (
9
 	// number of recognized capabilities:
9
 	// number of recognized capabilities:
10
-	numCapabs = 25
10
+	numCapabs = 26
11
 	// length of the uint64 array that represents the bitset:
11
 	// length of the uint64 array that represents the bitset:
12
 	bitsetLen = 1
12
 	bitsetLen = 1
13
 )
13
 )
112
 	// EventPlayback is the Proposed IRCv3 capability named "draft/event-playback":
112
 	// EventPlayback is the Proposed IRCv3 capability named "draft/event-playback":
113
 	// https://github.com/ircv3/ircv3-specifications/pull/362
113
 	// https://github.com/ircv3/ircv3-specifications/pull/362
114
 	EventPlayback Capability = iota
114
 	EventPlayback Capability = iota
115
+
116
+	// ZNCPlayback is the ZNC vendor capability named "znc.in/playback":
117
+	// https://wiki.znc.in/Playback
118
+	ZNCPlayback Capability = iota
115
 )
119
 )
116
 
120
 
117
 // `capabilityNames[capab]` is the string name of the capability `capab`
121
 // `capabilityNames[capab]` is the string name of the capability `capab`
142
 		"oragono.io/bnc",
146
 		"oragono.io/bnc",
143
 		"znc.in/self-message",
147
 		"znc.in/self-message",
144
 		"draft/event-playback",
148
 		"draft/event-playback",
149
+		"znc.in/playback",
145
 	}
150
 	}
146
 )
151
 )

+ 32
- 16
irc/channel.go Parādīt failu

620
 	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
620
 	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
621
 	rb.Flush(true)
621
 	rb.Flush(true)
622
 
622
 
623
-	var replayLimit int
624
-	customReplayLimit := client.AccountSettings().AutoreplayLines
625
-	if customReplayLimit != nil {
626
-		replayLimit = *customReplayLimit
627
-		maxLimit := channel.server.Config().History.ChathistoryMax
628
-		if maxLimit < replayLimit {
629
-			replayLimit = maxLimit
630
-		}
623
+	// autoreplay any messages as necessary
624
+	config := channel.server.Config()
625
+	var items []history.Item
626
+	if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets[chcfname]) {
627
+		items, _ = channel.history.Between(rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before, false, config.History.ChathistoryMax)
631
 	} else {
628
 	} else {
632
-		replayLimit = channel.server.Config().History.AutoreplayOnJoin
633
-	}
634
-	if 0 < replayLimit {
635
-		// TODO don't replay the client's own JOIN line?
636
-		items := channel.history.Latest(replayLimit)
637
-		if 0 < len(items) {
638
-			channel.replayHistoryItems(rb, items, true)
639
-			rb.Flush(true)
629
+		var replayLimit int
630
+		customReplayLimit := client.AccountSettings().AutoreplayLines
631
+		if customReplayLimit != nil {
632
+			replayLimit = *customReplayLimit
633
+			maxLimit := channel.server.Config().History.ChathistoryMax
634
+			if maxLimit < replayLimit {
635
+				replayLimit = maxLimit
636
+			}
637
+		} else {
638
+			replayLimit = channel.server.Config().History.AutoreplayOnJoin
639
+		}
640
+		if 0 < replayLimit {
641
+			items = channel.history.Latest(replayLimit)
640
 		}
642
 		}
641
 	}
643
 	}
644
+	// remove the client's own JOIN line from the replay
645
+	numItems := len(items)
646
+	for i := len(items) - 1; 0 <= i; i-- {
647
+		if items[i].Message.Msgid == message.Msgid {
648
+			// zero'ed items will not be replayed because their `Type` field is not recognized
649
+			items[i] = history.Item{}
650
+			numItems--
651
+			break
652
+		}
653
+	}
654
+	if 0 < numItems {
655
+		channel.replayHistoryItems(rb, items, true)
656
+		rb.Flush(true)
657
+	}
642
 }
658
 }
643
 
659
 
644
 // plays channel join messages (the JOIN line, topic, and names) to a session.
660
 // plays channel join messages (the JOIN line, topic, and names) to a session.

+ 2
- 0
irc/client.go Parādīt failu

113
 	maxlenRest   uint32
113
 	maxlenRest   uint32
114
 	capState     caps.State
114
 	capState     caps.State
115
 	capVersion   caps.Version
115
 	capVersion   caps.Version
116
+
117
+	zncPlaybackTimes *zncPlaybackTimes
116
 }
118
 }
117
 
119
 
118
 // sets the session quit message, if there isn't one already
120
 // sets the session quit message, if there isn't one already

+ 4
- 0
irc/commands.go Parādīt failu

321
 			handler:   whowasHandler,
321
 			handler:   whowasHandler,
322
 			minParams: 1,
322
 			minParams: 1,
323
 		},
323
 		},
324
+		"ZNC": {
325
+			handler:   zncHandler,
326
+			minParams: 1,
327
+		},
324
 	}
328
 	}
325
 
329
 
326
 	initializeServices()
330
 	initializeServices()

+ 14
- 4
irc/handlers.go Parādīt failu

2001
 			}
2001
 			}
2002
 			channel.SendSplitMessage(msg.Command, lowestPrefix, clientOnlyTags, client, splitMsg, rb)
2002
 			channel.SendSplitMessage(msg.Command, lowestPrefix, clientOnlyTags, client, splitMsg, rb)
2003
 		} else {
2003
 		} else {
2004
-			if service, isService := OragonoServices[strings.ToLower(targetString)]; isService {
2005
-				// NOTICE and TAGMSG to services are ignored
2006
-				if histType == history.Privmsg {
2004
+			// NOTICE and TAGMSG to services are ignored
2005
+			if histType == history.Privmsg {
2006
+				lowercaseTarget := strings.ToLower(targetString)
2007
+				if service, isService := OragonoServices[lowercaseTarget]; isService {
2007
 					servicePrivmsgHandler(service, server, client, message, rb)
2008
 					servicePrivmsgHandler(service, server, client, message, rb)
2009
+					continue
2010
+				} else if _, isZNC := zncHandlers[lowercaseTarget]; isZNC {
2011
+					zncPrivmsgHandler(client, lowercaseTarget, message, rb)
2012
+					continue
2008
 				}
2013
 				}
2009
-				continue
2010
 			}
2014
 			}
2011
 
2015
 
2012
 			user := server.clients.Get(targetString)
2016
 			user := server.clients.Get(targetString)
2746
 	}
2750
 	}
2747
 	return false
2751
 	return false
2748
 }
2752
 }
2753
+
2754
+// ZNC <module> [params]
2755
+func zncHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
2756
+	zncModuleHandler(client, msg.Params[0], msg.Params[1:], rb)
2757
+	return false
2758
+}

+ 7
- 0
irc/help.go Parādīt failu

540
 
540
 
541
 Returns historical information on the last user with the given nickname.`,
541
 Returns historical information on the last user with the given nickname.`,
542
 	},
542
 	},
543
+	"znc": {
544
+		text: `ZNC <module> [params]
545
+
546
+Used to emulate features of the ZNC bouncer. This command is not intended
547
+for direct use by end users.`,
548
+		duplicate: true,
549
+	},
543
 
550
 
544
 	// Informational
551
 	// Informational
545
 	"modes": {
552
 	"modes": {

+ 17
- 0
irc/misc_test.go Parādīt failu

1
+// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"testing"
8
+	"time"
9
+)
10
+
11
+func TestZncTimestampParser(t *testing.T) {
12
+	assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000), t)
13
+	assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000), t)
14
+	assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0), t)
15
+	assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000), t)
16
+	assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0), t)
17
+}

+ 98
- 0
irc/znc.go Parādīt failu

1
+// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"fmt"
8
+	"strconv"
9
+	"strings"
10
+	"time"
11
+)
12
+
13
+type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer)
14
+
15
+var zncHandlers = map[string]zncCommandHandler{
16
+	"*playback": zncPlaybackHandler,
17
+}
18
+
19
+func zncPrivmsgHandler(client *Client, command string, privmsg string, rb *ResponseBuffer) {
20
+	zncModuleHandler(client, command, strings.Fields(privmsg), rb)
21
+}
22
+
23
+func zncModuleHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
24
+	command = strings.ToLower(command)
25
+	if subHandler, ok := zncHandlers[command]; ok {
26
+		subHandler(client, command, params, rb)
27
+	} else {
28
+		nick := rb.target.Nick()
29
+		rb.Add(nil, client.server.name, "NOTICE", nick, fmt.Sprintf(client.t("Oragono does not emulate the ZNC module %s"), command))
30
+		rb.Add(nil, "*status!znc@znc.in", "NOTICE", nick, fmt.Sprintf(client.t("No such module [%s]"), command))
31
+	}
32
+}
33
+
34
+// "number of seconds (floating point for millisecond precision) elapsed since January 1, 1970"
35
+func zncWireTimeToTime(str string) (result time.Time) {
36
+	var secondsPortion, fracPortion string
37
+	dot := strings.IndexByte(str, '.')
38
+	if dot == -1 {
39
+		secondsPortion = str
40
+	} else {
41
+		secondsPortion = str[:dot]
42
+		fracPortion = str[dot:]
43
+	}
44
+	seconds, _ := strconv.ParseInt(secondsPortion, 10, 64)
45
+	fraction, _ := strconv.ParseFloat(fracPortion, 64)
46
+	return time.Unix(seconds, int64(fraction*1000000000))
47
+}
48
+
49
+type zncPlaybackTimes struct {
50
+	after   time.Time
51
+	before  time.Time
52
+	targets map[string]bool // nil for "*" (everything), otherwise the channel names
53
+}
54
+
55
+// https://wiki.znc.in/Playback
56
+// PRIVMSG *playback :play <target> [lower_bound] [upper_bound]
57
+// e.g., PRIVMSG *playback :play * 1558374442
58
+func zncPlaybackHandler(client *Client, command string, params []string, rb *ResponseBuffer) {
59
+	if len(params) < 2 {
60
+		return
61
+	} else if strings.ToLower(params[0]) != "play" {
62
+		return
63
+	}
64
+	targetString := params[1]
65
+
66
+	var after, before time.Time
67
+	if 2 < len(params) {
68
+		after = zncWireTimeToTime(params[2])
69
+	}
70
+	if 3 < len(params) {
71
+		before = zncWireTimeToTime(params[3])
72
+	}
73
+
74
+	var targets map[string]bool
75
+
76
+	// OK: the user's PMs get played back immediately on receiving this,
77
+	// then we save the timestamps in the session to handle replay on future channel joins
78
+	config := client.server.Config()
79
+	if params[1] == "*" {
80
+		items, _ := client.history.Between(after, before, false, config.History.ChathistoryMax)
81
+		client.replayPrivmsgHistory(rb, items, true)
82
+	} else {
83
+		for _, targetName := range strings.Split(targetString, ",") {
84
+			if cfTarget, err := CasefoldChannel(targetName); err == nil {
85
+				if targets == nil {
86
+					targets = make(map[string]bool)
87
+				}
88
+				targets[cfTarget] = true
89
+			}
90
+		}
91
+	}
92
+
93
+	rb.session.zncPlaybackTimes = &zncPlaybackTimes{
94
+		after:   after,
95
+		before:  before,
96
+		targets: targets,
97
+	}
98
+}

Notiek ielāde…
Atcelt
Saglabāt