Trouver qui supprime du contenu sur un framapad

Bonjour,

J’ai créé un Framapad de classe sur lequel mes élèves ont travaillé en autonomie. Malheureusement, certains petits malins se sont amusés à supprimer les réponses de leurs camarades, ce qui évidemment les a rendus furax.

J’ai consulté l’historique du pad, mais si je vois bien qui écrit et quand les textes disparaissent, je ne vois pas qui est responsable des suppressions.
Y a-t-il un moyen de trouver cette information ?

Merci d’avance pour votre aide !

Bonjour,

Je viens de regarder au niveau de l’interface, je n’ai rien vu qui pourrait faire l’affaire. La seule chose qui aurait pu être utile était la possibilité de rejouer l’historique des modifications. Seulement, ce n’affiche pas qui a supprimé du texte (pas de nom en surbrillance; ni même un code couleur)

La seule possibilité est d’exporter le pad dans son format Etherpad:

image

Mais là, cela devient compliqué. Les modifications sont énumérées à la fin du document. Dans un format plutôt incompréhensible. Il existe ici une petite documentation qui explique un peu le format utilisé: https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-notes.txt

Bonjour,

Merci pour votre réponse. J’ai exporté mon fichier au format Etherpad, mais comment suis-je censée le lire ensuite ? Je suis sur Mac et n’ai aucune application qui ouvre ce type d’extension, et quand j’importe mon document sur une plateforme Etherpad je me retrouve avec la même présentation que sur Framapad…
Désolée, je suis un peu perdue.

Bonsoir,

Peut-être que je viens trop tard, peut-être que ce sera inutile. Mais voici un script que j’ai écrit qui permet de visualiser dans un terminal les changement opéré sur un etherpad. C’est écrit en Python, donc il faudra installer cet environnement auparavant d’exécuter le script. Je n’ai pas de Mac, donc je ne l’ai testé que sur mon portable sous Linux et sur une machine du boulot sous Windows 10 (DOS ou Powershell). Je ne garantis pas que cela fonctionne tout le temps. De plus cela dépend également de l’encodage du fichier Etherpad.

Enfin, si cela tente quelqu’un d’essayer. Il faut :

  1. Copier coller le script suivant dans un nouveau fichier que l’on nommera par exemple `EtherpadView.py´
  2. Ouvrir un terminal de commande selon son système d’exploitation (DOS, Powershell, Bash, etc… ), de préférence qui permet la colorisation du texte
  3. Copier le fichier etherpad dans le même dossier que ce script
  4. Exécuter la commande : python EtherpadView.py my.pad (où my.pad est le nom du fichier pad copier au point 3)

Cela affiche quelque chose comme ceci:

C’est en fait un peu comme l’historique de l’interface web le permet sur Framapad à la différence que cela montre également les éléments qui ont été supprimés.

On démarre à la version initiale. Et on navigue dans l’historique de la sorte:

  • « flèche bas » ou « flèche droite » -> changement suivant
  • « flèche haut » ou « flèche gauche » -> changement précédent
  • « page haut » -> changement suivant par pas de 10
  • « page bas » -> changement précédent par pas de 10
  • « home » -> revenir à la version initiale
  • « end » -> aller à la version finale
  • « q » -> pour sortir

Pour l’affichage, on distinguera :

  • en haut en bleu, le nom de la personne qui a fait les changements sur la version visualisée et en bleu-vert la date et l’heure à laquelle la modification a été faite
  • dans le centre de la fenêtre, le texte. avec en vert les ajouts qui ont été faits, et en rouge les éléments qui ont étés supprimés.

Voici le script:

"""
    EtherpadView helps viewing changes done on a Etherpad text.
    
    Copyright (C) 2021  Pali Palo

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
"""

from datetime import datetime
import json
import os
import sys
import shlex
import struct
import platform
import subprocess

# This code build a system dependent alias 
# for clearing entire terminal screen
def get_clear():
    current_os = platform.system()
    if current_os == 'Windows':
        return lambda: os.system('cls')
    elif current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'):
        return lambda: os.system('clear')
    else :
        return lambda: os.system('cls')

clear = get_clear()

# The following code is used to get terminal attributes under Linux,
# Mac and Windows. (Hope this really works)
# This has been found here https://gist.github.com/jtriley/1108174
 
def get_terminal_size():
    """ getTerminalSize()
     - get width and height of console
     - works on linux,os x,windows,cygwin(windows)
     originally retrieved from:
     http://stackoverflow.com/questions/566746/how-to-get-console-window-width-in-python
    """
    current_os = platform.system()
    tuple_xy = None
    if current_os == 'Windows':
        tuple_xy = _get_terminal_size_windows()
        if tuple_xy is None:
            tuple_xy = _get_terminal_size_tput()
            # needed for window's python in cygwin's xterm!
    if current_os in ['Linux', 'Darwin'] or current_os.startswith('CYGWIN'):
        tuple_xy = _get_terminal_size_linux()
    if tuple_xy is None:
        tuple_xy = (80, 25)      # default value
    return tuple_xy
 
def _get_terminal_size_windows():
    try:
        from ctypes import windll, create_string_buffer
        # stdin handle is -10
        # stdout handle is -11
        # stderr handle is -12
        h = windll.kernel32.GetStdHandle(-12)
        csbi = create_string_buffer(22)
        res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
        if res:
            (bufx, bufy, curx, cury, wattr,
             left, top, right, bottom,
             maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
            sizex = right - left + 1
            sizey = bottom - top + 1
            return sizex, sizey
    except:
        pass
 
def _get_terminal_size_tput():
    # get terminal width
    # src: http://stackoverflow.com/questions/263890/how-do-i-find-the-width-height-of-a-terminal-window
    try:
        cols = int(subprocess.check_call(shlex.split('tput cols')))
        rows = int(subprocess.check_call(shlex.split('tput lines')))
        return (cols, rows)
    except:
        pass
 
def _get_terminal_size_linux():
    def ioctl_GWINSZ(fd):
        try:
            import fcntl
            import termios
            cr = struct.unpack('hh',
                               fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
            return cr
        except:
            pass
    cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
    if not cr:
        try:
            fd = os.open(os.ctermid(), os.O_RDONLY)
            cr = ioctl_GWINSZ(fd)
            os.close(fd)
        except:
            pass
    if not cr:
        try:
            cr = (os.environ['LINES'], os.environ['COLUMNS'])
        except:
            return None
    return int(cr[1]), int(cr[0])

console_columns = get_terminal_size() [0]

# This code is used to get user input one char at a time
# without requesting the user to validate it with enter key

def _find_getch():
    try:
        import termios
    except ImportError:
        # Non-POSIX. Return msvcrt's (Windows') getch.
        import msvcrt
        return msvcrt.getch

    # POSIX system. Create and return a getch that manipulates the tty.
    import sys, tty
    def _getch():
        fd = sys.stdin.fileno()
        old_settings = termios.tcgetattr(fd)
        try:
            tty.setraw(fd)
            ch = sys.stdin.read(1)
        finally:
            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
        return ch

    return _getch

getch = _find_getch()

# This code is used to decode base36 integer value found
# in the changeset.
# Base36 is a numeric system such this : 
#   0, 1, 2, ..., 9, a, b, c, ..., z, 10, 11, .., 19, 1a, ...

def base36decode ( number ) :
    return int ( number, 36 )

# This code is used to get the integer value for a command
# in a changeset
def get_changeset_value( changes_string, i ):
    allowed_commands = ':><+-=|*'
    value = ''
    while i < len( changes_string ) and allowed_commands.find( changes_string[i] ) == -1 :
        value += changes_string[i]
        i += 1
    return base36decode( value ), i

# This code is used to apply the changes on the previous
# version of the text, by following the commands found
# in the changset.
# Its output its a displayable text which have certain color;
# and the text modified accordingly to changeset. This allows
# caller to display a visual changes to the text and reroll
# the text for the next changeset.

def apply_pad_revision( revision_ID, text ) :
    global revisions
    tokens = revisions[ revision_ID ][ 'changeset' ].partition( '$' )
    
    changes_string = tokens[0]
    original_text = text
    original_text_pos = 0
    revision_text = tokens[2]
    revision_text_pos = 0
    modified_text = ''
    modified_text_pos = 0
    displayed_text = ''
    displayed_text_pos = 0
    i = 1 # 1 instead 0 to bypass leading Z char which is only a magic byte
    while i < len( changes_string ) :
        command = changes_string[i]
        value, i = get_changeset_value( changes_string, i + 1 )
        # print ( "Command : " + command + ' ' + str( value ) )
        if command == ':' or command == '>' or command == '<' :
            # ':' means previous revision text length
            # '>' means number of char added to previous revision text
            # '<' means number of char removed from previous revision text
            # We do not need this information here, so pass it
            pass
        elif command == '+' :
            # '+' means that X characters are to be added from this actual cursor
            # within previous revision text.
            modified_text += revision_text [ revision_text_pos : revision_text_pos + value ]
            modified_text_pos += value
            revision_text_part = revision_text [ revision_text_pos : revision_text_pos + value ]
            if revision_text_part.endswith( '\n' ) :
                revision_text_part = revision_text_part[ 0: len( revision_text_part ) - 1 ] + ' \n'
            displayed_text += '\33[42;1m' + revision_text_part + '\33[0m'
            displayed_text_pos += len( revision_text_part ) + len ( '\33[42;1m' + '\33[0m' )
            revision_text_pos += value
            # original_text_pos is unchanged since it is a new inserted text
        elif command == '-' :
            # modified_text_pos is unchanged
            original_text_part = original_text [ original_text_pos : original_text_pos + value ]
            if original_text_part.endswith( '\n' ) :
                original_text_part = original_text_part[ 0: len( original_text_part ) - 1 ] + ' \n'
            displayed_text += '\33[41;1m' + original_text_part + '\33[0m'
            displayed_text_pos += len( original_text_part ) + len ( '\33[41;1m' + '\33[0m' )
            original_text_pos += value
        elif command == '=' :
            modified_text += original_text [ original_text_pos : original_text_pos + value ]
            modified_text_pos += value
            displayed_text += original_text [ original_text_pos : original_text_pos + value ]
            displayed_text_pos += value
            original_text_pos += value
        elif command == '|' :
            sub_command = changes_string[i]
            sub_value, i = get_changeset_value( changes_string, i + 1 )
            # print( "Subcommand : " + sub_command + ' ' + str( sub_value ) )
            if sub_command == '+' :
                # '+' means that <sub_value> characters are to be added from this actual cursor
                # within previous revision text and that added text contains <value> newline chars.
                modified_text += revision_text [ revision_text_pos : revision_text_pos + sub_value ]
                modified_text_pos += sub_value
                revision_text_part = revision_text [ revision_text_pos : revision_text_pos + sub_value ]
                if revision_text_part.endswith( '\n' ) :
                    revision_text_part = revision_text_part[ 0: len( revision_text_part ) - 1 ] + ' \n'
                displayed_text += '\33[42;1m' + revision_text_part + '\33[0m'
                displayed_text_pos += len( revision_text_part ) + len ( '\33[42;1m' + '\33[0m' )
                revision_text_pos += sub_value
                # original_text_pos is unchanged
            elif sub_command == '-' :
                # modified_text_pos is unchanged
                original_text_part = original_text [ original_text_pos : original_text_pos + sub_value ]
                if original_text_part.endswith( '\n' ) :
                    original_text_part = original_text_part[ 0: len( original_text_part ) - 1 ] + ' \n'
                displayed_text += '\33[41;1m' + original_text_part + '\33[0m'
                displayed_text_pos += len( original_text_part ) + len ( '\33[41;1m' + '\33[0m' )
                original_text_pos += sub_value
            elif sub_command == '=' :
                modified_text += original_text [ original_text_pos : original_text_pos + sub_value ]
                modified_text_pos += sub_value
                displayed_text += original_text [ original_text_pos : original_text_pos + sub_value ]
                displayed_text_pos += sub_value
                original_text_pos += sub_value
        elif command == '*' :
            # Applying attributes to following changes. Generally this set 
            # the author of the change, but this in fact the same
            # as the changeset author found in meta entry. So this is strictly
            # ignored here. But this may reflect the font face or style change 
            # (bold, italic, color, etc.)
            pass
    displayed_text += original_text[ original_text_pos : ]
    modified_text += original_text[ original_text_pos : ]
    
    return displayed_text, modified_text

# This code is used to get response from the user depending on
# the system on wich this script runs.
def get_response() :
	response = getch()
	if platform.system() == 'Windows' :
		response = hex( ord( response ) )
		if response == '0xe0' :
			sub_char = hex( ord( getch() ) )
			if sub_char == '0x48' or sub_char == '0x4b' : # up or left
				return 'prev'
			elif sub_char == '0x50' or sub_char == '0x4d' : # down or right
				return 'next'
			elif sub_char == '0x48' : # home
				return 'home'
			elif sub_char == '0x46' : # end
				return 'end'
			elif sub_char == '0x49' :
				return 'pgup'
			elif sub_char == '0x51' :
				return 'pgdn'
		elif response == '0x71' : # lower byte of unicode value for q key
			return 'q'
	else :
		if response == '\x1b' :
			sub_char = getch()
			if sub_char == '\x5b' :
				sub_char = getch()
				if sub_char == '\x41' or sub_char == '\x44' : # up or left
					return 'prev'
				elif sub_char == '\x42' or sub_char == '\x43' : # down or right
					return 'next'
				elif sub_char == '\x48' : # home
					return 'home'
				elif sub_char == '\x46' : # end
					return 'end'
				elif sub_char == '\x35' :
					sub_char = getch()
					if sub_char == '\x7e' : # page up
						return 'pgup'
				elif sub_char == '\x36' :
					sub_char = getch()
					if sub_char == '\x7e' : # page down
						return 'pgdn'
	return response

if __name__ == "__main__":

    # handle sript parameter
    if len( sys.argv ) != 2 :
        print( "Please give one etherpad file name as argument for this script" )
        quit()

    # open the etherpad (which is indeed a json export of a variable)
    with open( sys.argv[1] ) as json_file :
        document = json.load(json_file)

    # First, extract only revisions from Etherpad data.
    # (In meantime, build the authors list to ease display later)
    revisions = dict()
    authors = dict()
    for entry_ID in document :
        tokens = entry_ID.split( ':' )
        if entry_ID.startswith( 'pad:' ) :
            # For all pad entries, # tokens[0] = "pad" and tokens[1] = pad ID.
            # For revisionned pad entries, tokens[2] = "revs" and tokens[3] =
            # revision sequential number
            # (there might be several pads by design but not in this case)
            if len( tokens ) == 4 and tokens[2] == 'revs' :
                revisions[ tokens[3] ] = document[ entry_ID ]
        if entry_ID.startswith( 'globalAuthor:' ) :
            authors[ tokens[1] ] = document[ entry_ID ][ 'name' ]

    # Then, parsed the revisions sorted by sequential revision number
    # and change the text one step at a time.s
    history = dict()
    i = 0
    text = '' # by default there is no text yet
    for revision_ID in sorted( revisions, key=int ) :
        title = ''
        if revision_ID == '0' :
            title = 'Pad revision #' + revision_ID + ' (original version)'
        else :
            revision_info = revisions[ revision_ID ][ 'meta' ]
            author = '\33[44;1m' + authors[ revision_info[ 'author' ] ] + '\33[0m'
            timestamp = '\33[46;1m' + datetime.fromtimestamp( revision_info[ 'timestamp' ] / 1000 ).isoformat() + '\33[0m'
            title = 'Pad revision #' + revision_ID + ' done by "' + author + '" at ' + timestamp
        
        displayed_text, text = apply_pad_revision( revision_ID, text )
        
        sequence_information = dict()
        sequence_information[ 'title' ] = title
        sequence_information[ 'text' ] = displayed_text
        
        history[ i ] = sequence_information
        i += 1

    # Finally navigate through change history (until user hit q key)
    response = ''
    history_sequence = 0
    while response.lower() != 'q':
        clear()
        print( history[ history_sequence ][ 'title' ] )
        print( "=" * int( console_columns ) )
        print( history[ history_sequence ][ 'text' ] )
        print( "=" * int( console_columns ) )
        print( "← or ↑ (previous) | ↓ or → (next) | PgUp (-10) | PgDn (+10) | Home (first) | End (last) | Q (stop) ")
        response = get_response()
        if response == 'pgup' :
            history_sequence -= 10
        elif response == 'pgdn' :
            history_sequence += 10
        elif response == 'next' :
            history_sequence += 1
        elif response == 'prev' :
            history_sequence -= 1
        elif response == 'home' :
            history_sequence = 0
        elif response == 'end' :
            history_sequence = len( history ) - 1
            
        if history_sequence < 0 :
            history_sequence = 0
        elif history_sequence > len( history ) - 1 :
            history_sequence = len( history ) - 1
1 « J'aime »