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"
"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
)
2023-05-31 07:16:14 +02:00
type CanDelete uint
const (
canDeleteAny CanDelete = iota // User is allowed to delete any message (for a given channel/PM)
canDeleteSelf // User is allowed to delete their own messages (ditto)
canDeleteNone // User is not allowed to delete any message (ditto)
)
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 ,
2022-04-02 02:52:09 +02:00
help : ` Syntax : $ bDELETE < target > < msgid > $ b
2020-05-12 18:05:40 +02:00
2022-04-02 02:52:09 +02:00
DELETE deletes an individual message by its msgid . The target is the channel
name . The msgid is the ID as can be found in the tags of that message . ` ,
helpShort : ` $bDELETE$b deletes an individual message by its target and msgid. ` ,
2020-05-12 18:05:40 +02:00
enabled : histservEnabled ,
2022-04-02 02:52:09 +02:00
minParams : 2 ,
2020-05-12 18:05:40 +02:00
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
}
2023-05-31 07:16:14 +02:00
// Returns:
//
// 1. `canDeleteAny` if the client allowed to delete other users' messages from the target, ie.:
// - the client is a channel operator, or
// - the client is an operator with "history" capability
//
// 2. `canDeleteSelf` if the client is allowed to delete their own messages from the target
// 3. `canDeleteNone` otherwise
func deletionPolicy ( server * Server , client * Client , target string ) CanDelete {
2021-03-11 06:34:53 +01:00
isOper := client . HasRoleCapabs ( "history" )
2023-05-31 07:16:14 +02:00
if isOper {
return canDeleteAny
} else {
2021-03-11 06:34:53 +01:00
if server . Config ( ) . History . Retention . AllowIndividualDelete {
channel := server . channels . Get ( target )
if channel != nil && channel . ClientIsAtLeast ( client , modes . Operator ) {
2023-05-31 07:16:14 +02:00
return canDeleteAny
2021-03-11 06:34:53 +01:00
} else {
2023-05-31 07:16:14 +02:00
return canDeleteSelf
2021-03-11 06:34:53 +01:00
}
2023-05-31 07:16:14 +02:00
} else {
return canDeleteNone
2020-05-12 18:05:40 +02:00
}
}
2023-05-31 07:16:14 +02:00
}
func histservDeleteHandler ( service * ircService , server * Server , client * Client , command string , params [ ] string , rb * ResponseBuffer ) {
target , msgid := params [ 0 ] , params [ 1 ] // Fix #1881 2 params are required
canDelete := deletionPolicy ( server , client , target )
accountName := "*"
if canDelete == canDeleteNone {
2021-03-11 06:34:53 +01:00
service . Notice ( rb , client . t ( "Insufficient privileges" ) )
return
2023-05-31 07:16:14 +02:00
} else if canDelete == canDeleteSelf {
accountName = client . AccountName ( )
if accountName == "*" {
service . Notice ( rb , client . t ( "Insufficient privileges" ) )
return
}
2021-03-11 06:34:53 +01:00
}
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 {
2023-05-31 07:16:14 +02:00
isOper := client . HasRoleCapabs ( "history" )
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 ) {
2021-09-19 03:28:16 +02:00
defer server . HandlePanic ( )
2020-05-12 18:05:40 +02:00
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-11-01 06:23:07 +01: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
}