2020-05-12 18:05:40 +02:00
// Copyright (c) 2020 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"bufio"
"fmt"
"os"
"runtime/debug"
"strconv"
"time"
2021-05-25 06:34:38 +02:00
"github.com/ergochat/ergo/irc/history"
"github.com/ergochat/ergo/irc/modes"
"github.com/ergochat/ergo/irc/utils"
2020-05-12 18:05:40 +02:00
)
const (
histservHelp = ` HistServ provides commands related to history. `
)
func histservEnabled ( config * Config ) bool {
return config . History . Enabled
}
func historyComplianceEnabled ( config * Config ) bool {
return config . History . Enabled && config . History . Persistent . Enabled && config . History . Retention . EnableAccountIndexing
}
var (
histservCommands = map [ string ] * serviceCommand {
"forget" : {
handler : histservForgetHandler ,
help : ` Syntax : $ bFORGET < account > $ b
FORGET deletes all history messages sent by an account . ` ,
helpShort : ` $bFORGET$b deletes all history messages sent by an account. ` ,
capabs : [ ] string { "history" } ,
enabled : histservEnabled ,
minParams : 1 ,
maxParams : 1 ,
} ,
"delete" : {
handler : histservDeleteHandler ,
help : ` Syntax : $ bDELETE [ target ] < msgid > $ b
DELETE deletes an individual message by its msgid . The target is a channel
name or nickname ; depending on the history implementation , this may or may not
be necessary to locate the message . ` ,
helpShort : ` $bDELETE$b deletes an individual message by its msgid. ` ,
enabled : histservEnabled ,
minParams : 1 ,
maxParams : 2 ,
} ,
"export" : {
handler : histservExportHandler ,
help : ` Syntax : $ bEXPORT < account > $ b
2020-05-13 18:12:31 +02:00
EXPORT exports all messages sent by an account as JSON . This can be used at
the request of the account holder . ` ,
2020-05-12 18:05:40 +02:00
helpShort : ` $bEXPORT$b exports all messages sent by an account as JSON. ` ,
enabled : historyComplianceEnabled ,
capabs : [ ] string { "history" } ,
minParams : 1 ,
maxParams : 1 ,
} ,
"play" : {
handler : histservPlayHandler ,
help : ` Syntax : $ bPLAY < target > [ limit ] $ b
PLAY plays back history messages , rendering them into direct messages from
2021-04-19 14:54:40 +02:00
HistServ . ' target ' is a channel name or nickname to query , and ' limit '
2020-05-12 18:05:40 +02:00
is a message count or a time duration . Note that message playback may be
incomplete or degraded , relative to direct playback from / HISTORY or
CHATHISTORY . ` ,
helpShort : ` $bPLAY$b plays back history messages. ` ,
enabled : histservEnabled ,
minParams : 1 ,
maxParams : 2 ,
} ,
}
)
2020-11-29 05:27:11 +01:00
func histservForgetHandler ( service * ircService , server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
2020-05-12 18:05:40 +02:00
accountName := server . accounts . AccountToAccountName ( params [ 0 ] )
if accountName == "" {
2020-11-29 05:27:11 +01:00
service . Notice ( rb , client . t ( "Could not look up account name, proceeding anyway" ) )
2020-05-12 18:05:40 +02:00
accountName = params [ 0 ]
}
server . ForgetHistory ( accountName )
2020-11-29 05:27:11 +01:00
service . Notice ( rb , fmt . Sprintf ( client . t ( "Enqueued account %s for message deletion" ) , accountName ) )
2020-05-12 18:05:40 +02:00
}
2020-11-29 05:27:11 +01:00
func histservDeleteHandler ( service * ircService , server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
2020-05-12 18:05:40 +02:00
var target , msgid string
if len ( params ) == 1 {
msgid = params [ 0 ]
} else {
target , msgid = params [ 0 ] , params [ 1 ]
}
2021-03-11 06:34:53 +01:00
// operators can delete; if individual delete is allowed, a chanop or
// the message author can delete
2020-05-12 18:05:40 +02:00
accountName := "*"
2021-03-11 06:34:53 +01:00
isChanop := false
isOper := client . HasRoleCapabs ( "history" )
if ! isOper {
if server . Config ( ) . History . Retention . AllowIndividualDelete {
channel := server . channels . Get ( target )
if channel != nil && channel . ClientIsAtLeast ( client , modes . Operator ) {
isChanop = true
} else {
accountName = client . AccountName ( )
}
2020-05-12 18:05:40 +02:00
}
}
2021-03-11 06:34:53 +01:00
if ! isOper && ! isChanop && accountName == "*" {
service . Notice ( rb , client . t ( "Insufficient privileges" ) )
return
}
2020-05-12 18:05:40 +02:00
err := server . DeleteMessage ( target , msgid , accountName )
if err == nil {
2020-11-29 05:27:11 +01:00
service . Notice ( rb , client . t ( "Successfully deleted message" ) )
2020-05-12 18:05:40 +02:00
} else {
2021-03-11 06:34:53 +01:00
if isOper {
2020-11-29 05:27:11 +01:00
service . Notice ( rb , fmt . Sprintf ( client . t ( "Error deleting message: %v" ) , err ) )
2020-05-12 18:05:40 +02:00
} else {
2020-11-29 05:27:11 +01:00
service . Notice ( rb , client . t ( "Could not delete message" ) )
2020-05-12 18:05:40 +02:00
}
}
}
2020-11-29 05:27:11 +01:00
func histservExportHandler ( service * ircService , server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
2020-05-12 18:05:40 +02:00
cfAccount , err := CasefoldName ( params [ 0 ] )
if err != nil {
2020-11-29 05:27:11 +01:00
service . Notice ( rb , client . t ( "Invalid account name" ) )
2020-05-12 18:05:40 +02:00
return
}
config := server . Config ( )
2020-05-13 17:56:17 +02:00
// don't include the account name in the filename because of escaping concerns
filename := fmt . Sprintf ( "%s-%s.json" , utils . GenerateSecretToken ( ) , time . Now ( ) . UTC ( ) . Format ( IRCv3TimestampFormat ) )
2020-05-12 18:05:40 +02:00
pathname := config . getOutputPath ( filename )
outfile , err := os . Create ( pathname )
if err != nil {
2020-11-29 05:27:11 +01:00
service . Notice ( rb , fmt . Sprintf ( client . t ( "Error opening export file: %v" ) , err ) )
2020-05-12 18:05:40 +02:00
} else {
2020-11-29 05:27:11 +01:00
service . Notice ( rb , fmt . Sprintf ( client . t ( "Started exporting data for account %[1]s to file %[2]s" ) , cfAccount , filename ) )
2020-05-12 18:05:40 +02:00
}
2020-11-29 05:27:11 +01:00
go histservExportAndNotify ( service , server , cfAccount , outfile , filename , client . Nick ( ) )
2020-05-12 18:05:40 +02:00
}
2020-11-29 05:27:11 +01:00
func histservExportAndNotify ( service * ircService , server * Server , cfAccount string , outfile * os . File , filename , alertNick string ) {
2020-05-12 18:05:40 +02:00
defer func ( ) {
if r := recover ( ) ; r != nil {
server . logger . Error ( "history" ,
fmt . Sprintf ( "Panic in history export routine: %v\n%s" , r , debug . Stack ( ) ) )
}
} ( )
defer outfile . Close ( )
writer := bufio . NewWriter ( outfile )
defer writer . Flush ( )
server . historyDB . Export ( cfAccount , writer )
client := server . clients . Get ( alertNick )
if client != nil && client . HasRoleCapabs ( "history" ) {
2020-11-29 05:27:11 +01:00
client . Send ( nil , service . prefix , "NOTICE" , client . Nick ( ) , fmt . Sprintf ( client . t ( "Data export for %[1]s completed and written to %[2]s" ) , cfAccount , filename ) )
2020-05-12 18:05:40 +02:00
}
}
2020-11-29 05:27:11 +01:00
func histservPlayHandler ( service * ircService , server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
2020-05-12 18:05:40 +02:00
items , _ , err := easySelectHistory ( server , client , params )
if err != nil {
2020-11-29 05:27:11 +01:00
service . Notice ( rb , client . t ( "Could not retrieve history" ) )
2020-05-12 18:05:40 +02:00
return
}
playMessage := func ( timestamp time . Time , nick , message string ) {
2020-12-14 21:23:01 +01:00
service . Notice ( rb , fmt . Sprintf ( "%s <%s> %s" , timestamp . Format ( "15:04:05" ) , NUHToNick ( nick ) , message ) )
2020-05-12 18:05:40 +02:00
}
for _ , item := range items {
// TODO: support a few more of these, maybe JOIN/PART/QUIT
if item . Type != history . Privmsg && item . Type != history . Notice {
continue
}
if len ( item . Message . Split ) == 0 {
playMessage ( item . Message . Time , item . Nick , item . Message . Message )
} else {
for _ , pair := range item . Message . Split {
playMessage ( item . Message . Time , item . Nick , pair . Message )
}
}
}
2020-11-29 05:27:11 +01:00
service . Notice ( rb , client . t ( "End of history playback" ) )
2020-05-12 18:05:40 +02:00
}
// handles parameter parsing and history queries for /HISTORY and /HISTSERV PLAY
func easySelectHistory ( server * Server , client * Client , params [ ] string ) ( items [ ] history . Item , channel * Channel , err error ) {
2021-04-19 14:54:40 +02:00
channel , sequence , err := server . GetHistorySequence ( nil , client , params [ 0 ] )
2020-05-12 18:05:40 +02:00
if sequence == nil || err != nil {
return nil , nil , errNoSuchChannel
}
var duration time . Duration
maxChathistoryLimit := server . Config ( ) . History . ChathistoryMax
limit := 100
if maxChathistoryLimit < limit {
limit = maxChathistoryLimit
}
if len ( params ) > 1 {
providedLimit , err := strconv . Atoi ( params [ 1 ] )
if err == nil && providedLimit != 0 {
limit = providedLimit
if maxChathistoryLimit < limit {
limit = maxChathistoryLimit
}
} else if err != nil {
duration , err = time . ParseDuration ( params [ 1 ] )
if err == nil {
limit = maxChathistoryLimit
}
}
}
if duration == 0 {
2021-04-06 06:46:07 +02:00
items , err = sequence . Between ( history . Selector { } , history . Selector { } , limit )
2020-05-12 18:05:40 +02:00
} else {
now := time . Now ( ) . UTC ( )
start := history . Selector { Time : now }
end := history . Selector { Time : now . Add ( - duration ) }
2021-04-06 06:46:07 +02:00
items , err = sequence . Between ( start , end , limit )
2020-05-12 18:05:40 +02:00
}
return
}