diff --git a/logbot/README.md b/logbot/README.md index 33b575a..2d1df6f 100644 --- a/logbot/README.md +++ b/logbot/README.md @@ -1,15 +1,100 @@ -# logbot +# RECbot -A small XMPP bot written in Python (using the slixmpp library) that logs all images and messages with the mentioning of *@bot* to an HTML page, to allow collaborative log writing over time. +A small XMPP bot written in Python that logs XMPP conversations into a HTML page, allowing collaborative log writing over time. -To run it: +The bot is used in group chats, where it includes all images that are send to the group and all messages that include `@bot`. - $ python3 logbot.py +*work-in-progress* + +## Situated tails + +* Archive bot, Relearn 2017, +* Streambot, Varia website extension 2017-2018, +* Logbot, Varia XMPP extension 2017-2020, + +## Use RECbot + +* check if `RECbot` is one of the participants in the groupchat! +* send an image to the groupchat **OR** use one of the `__ACTION WORDS__` below +* the bot confirms your contribution and writes the message to a file +* check the output of RECbot (locally or online, for example: ) + +RECbot works with `__ACTION WORDS__` and unique `` codes. + +* `__ADD__` RECbot entries with `__ADD__ `, for example: `__ADD__ Logging as a form of stretching time.` or `__ADD__ https://nicelink.org` +* `__DELETE__` RECbot entries with `__DELETE__ `, for example: `__DELETE__ ~+*/+-` (\*spark) +* `__BOOK__` (\*sparks) + +## Install RECbot + +RECbot uses the `slixmpp` library to connect to XMPP and `beautifulsoup` to parse the HTML pages. + +`$ sudo pip3 install slixmpp beautifulsoup4` + +## Run RECbot! + +`$ python3 RECbot.py` + +The bot will ask you to provide the following details: + +* XMPP address of a (bot)account +* password +* groupchat address +* nickname for the bot +* output folder path + +You can also run it as a oneliner, for example by writing: + +`$ python3 RECbot.py -u bot@vvvvvvaria.org -p CHANGEME -g roomname@muc.vvvvvvaria.org -n RECbot -o /var/www/logs/` + +* `-u` / `--use` = user / use this XMPP address +* `-p` / `--password` = password +* `-g` / `--groupchat` = groupchat +* `-n` / `--nickname` = nickname +* `-o` / `--output` = output + +## \*sparks + +----------- + +It would be so nice to have different RECbot *modes*: `--log`, `--stream`, `--distribusi` + +* `--log`: RECbot writes a growing HTML page with images and text, that can be marked up and styled in HTML/CSS. +* `--stream`: RECbot stores all images that are send to the group, and displays them as an image stream. +* `--distribusi`: RECbot saves files (images, messages as markdown, files, links as HTML pages) and generates a distribusi page of all collected material. + +Under the hood the process can be cut up into two procedures: + +* saving text/image/audio/video based messages as files (.txt, .png/.jpg, .ogg, .og4/.mp4) + * recbot.py +* generating different outputs, depending on the selected *mode* + * distribusi.py[\*] + * log.py[\*] + * stream.py[\*] + +These modes can be changed at any moment. + +[\*] These are standalone scripts. They can be used on any set of files in a folder and generate HTML pages with customizable styling. + +``` + RECbot + + │ --distribusi > distribusi.py + │ [output folder] --log > log.py [output_folder/index.html] + │ (saved as files) --stream > stream.py (saved as index.html + stylesheet.css) + │ --xxx > xxx.py + │ + └── stores text/media + files in output folder + (local/server) +``` + +------------ + +How can `__ACTION WORDS__` become magical `__MAGIC WORDS__` ??? + +------------ -Dependencies: - $ sudo pip3 install slixmpp beautifulsoup4 ---- -That's all for now! diff --git a/logbot/__pycache__/templates.cpython-37.pyc b/logbot/__pycache__/templates.cpython-37.pyc new file mode 100644 index 0000000..15f6e38 Binary files /dev/null and b/logbot/__pycache__/templates.cpython-37.pyc differ diff --git a/logbot/avatar.png b/logbot/avatar.png new file mode 100644 index 0000000..893c2f1 Binary files /dev/null and b/logbot/avatar.png differ diff --git a/logbot/example/.handles.txt b/logbot/example/.handles.txt new file mode 100644 index 0000000..72b9099 --- /dev/null +++ b/logbot/example/.handles.txt @@ -0,0 +1 @@ +ieiauieiauiiuioiiuioooooeooooeieoooieooouuaiauuaia \ No newline at end of file diff --git a/logbot/example/Screenshot from 2021-01-11 22-58-22.png b/logbot/example/Screenshot from 2021-01-11 22-58-22.png new file mode 100644 index 0000000..bbff26b Binary files /dev/null and b/logbot/example/Screenshot from 2021-01-11 22-58-22.png differ diff --git a/logbot/example/entries/ieooo.txt b/logbot/example/entries/ieooo.txt new file mode 100644 index 0000000..0c2327e --- /dev/null +++ b/logbot/example/entries/ieooo.txt @@ -0,0 +1 @@ +text msg, image, video, audio diff --git a/logbot/example/entries/soviet-80s-computers-EFcZsx0VUAAZZoC.jpeg b/logbot/example/entries/soviet-80s-computers-EFcZsx0VUAAZZoC.jpeg new file mode 100644 index 0000000..0638195 Binary files /dev/null and b/logbot/example/entries/soviet-80s-computers-EFcZsx0VUAAZZoC.jpeg differ diff --git a/logbot/example/index.html b/logbot/example/index.html new file mode 100644 index 0000000..ea1eec1 --- /dev/null +++ b/logbot/example/index.html @@ -0,0 +1,65 @@ + + + + + Log + + + +
+

Welcome to this Log!

+

This Log file is based on chat messages exchanged in a XMPP groupchat and is written by RECbot.

+
+

For the writers of this log, you can: +
+
+ send an image, +
+
+ __ADD__ a message, +
+
+ __DELETE__ it by using the HANDLE on the left (*spark), +
+
+ + + + + + + Request information about a __BOOK__ by sending a TITLE (*spark, almost there), +
+
+ ... (*spark) +

+ +
+ +

ibugev@muc.vvvvvvaria.org

+
+
+ ieooo + In general terms, “transhackfeminism” refers to hacking_with_care, using hacking with a meaning of (active) resistance and transformation to generate transversal knowledge through transdisciplinary artistic, aesthetic or cultural practices/ proposals. To work on producing knowledge collectively: without differentiating between theory and practice; as well as to embrace, protect and advance in free culture. To create communities where people meet, exchange, experience and share knowledge. To work on human and non-human alliances and solidarity through DIY/DIWO/DIT biotechnology, artistic and cultural practices. +

+ To stay in touch with the material-affective dimensions of doing and engaging (bio)practices. +

+ https://syllabus.pirate.care/topic/transhackfeminism/ + Added on 11 January 2021 +
+
+ auiio + + Added on 12 January 2021 +
+ +
+ \ No newline at end of file diff --git a/logbot/example/stylesheet.css b/logbot/example/stylesheet.css new file mode 100644 index 0000000..455f568 --- /dev/null +++ b/logbot/example/stylesheet.css @@ -0,0 +1,69 @@ +body{ + background-color: lightgrey; + min-width: 1080px; + margin: 40px; + font-size: 20px; + line-height: 24px; +} +div#welcome{ + float: right; + top:40px; + right:40px; + width: 200px; + font-size: 16px; +} + div#welcome p{ + margin:0 0 1em 0; + padding:0; + } + div#welcome hr{ + border:0; + border-bottom:1px dotted blue; + margin:2em 0; + } +div#echo{ + position: fixed; + bottom: 0; + left: 0; + width: 100%; + padding: 0.5em; + background-color: pink; +} +div.post{ + margin: 2em 5em 2em 9em; + width: 800px; +} +div.post span.tagcontainer span{ + padding-left: 0.5em; + color: blue; +} + +p{ + margin: 1em 0; +} +code{ + color: blue; +} +small{ + font-size: 12px; + line-height:1.2; +} +small.postid{ + float: left; + font-family: monospace; + margin: 0 0 0 -180px; + padding: 1em 1.5em; + /*border-radius: 50px;*/ + /*border: 1px dotted blue;*/ + color: blue; + background-color: white; + font-size: 20px; +} +small.date{ + display:block; + color:magenta; + margin:1em 0; +} +img{ + max-width: 100%; +} \ No newline at end of file diff --git a/logbot/index.html b/logbot/index.html new file mode 100644 index 0000000..1b06573 --- /dev/null +++ b/logbot/index.html @@ -0,0 +1,143 @@ + + + + + *docbot* + + + +

OMG

+ + + +
+

(0)

+

@docbot hello?

+
+ + + +
+

(1)

+

@docbot hello???

+
+ + + +
+

(5)

+

@docbot hello???

+
+ + + +
+

(6)

+

@docbot INFO No additional plugins loaded +INFO No Redis storage discovered +INFO Serving on http://0.0.0.0:8080 +INFO JID set to: bot@vvvvvvaria.org/m0Upw4m2 +INFO Joining testtesttest@muc.vvvvvvaria.org automatically +INFO Joining ibugev@muc.vvvvvvaria.org automatically +@docbot hello??? +^CERROR Fatal write error on socket transport +protocol: +transport: <_SelectorSocketTransport fd=6> +Traceback (most recent call last): + File "/usr/lib/python3.7/asyncio/selector_events.py", line 857, in write + n = self._sock.send(data) +OSError: [Errno 9] Bad file descriptor +ERROR Fatal error on SSL transport +protocol: +transport: <_SelectorSocketTransport closing fd=6> +Traceback (most recent call last): + File "/usr/lib/python3.7/asyncio/selector_events.py", line 857, in write + n = self._sock.send(data) +OSError: [Errno 9] Bad file descriptor + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "/usr/lib/python3.7/asyncio/sslproto.py", line 676, in _process_write_backlog + self._transport.write(chunk) + File "/usr/lib/python3.7/asyncio/selector_events.py", line 861, in write + self._fatal_error(exc, 'Fatal write error on socket transport') + File "/usr/lib/python3.7/asyncio/selector_events.py", line 670, in _fatal_error + self._force_close(exc) + File "/usr/lib/python3.7/asyncio/selector_events.py", line 682, in _force_close + self._loop.call_soon(self._call_connection_lost, exc) + File "/usr/lib/python3.7/asyncio/base_events.py", line 688, in call_soon + self._check_closed() + File "/usr/lib/python3.7/asyncio/base_events.py", line 480, in _check_closed + raise RuntimeError('Event loop is closed') +RuntimeError: Event loop is closed +

+
+ + + +
+

(7)

+

@docbot hello???

+
+ + + +
+

(8)

+

@docbot test

+
+ + + +
+

(10)

+

@docbot hello?

+
+ + + +
+

(11)

+

@docbot hello?

+
+ + + +
+

(12)

+

@docbot NICCCCCCCCEEEE

+
+ + + +
+

(13)

+

@docbot help

+
+ + + +
+

(14)

+

https://xmpp.vvvvvvaria.org:5281/upload/UTVaTk5FsUNXbUOx/avatar.png

+
+ + + +
+

(16)

+

+
+ + + +
+

(17)

+

+
+ + + + + \ No newline at end of file diff --git a/logbot/index.html.j2 b/logbot/index.html.j2 new file mode 100644 index 0000000..3c849e9 --- /dev/null +++ b/logbot/index.html.j2 @@ -0,0 +1,21 @@ + + + + + *logbot* + + + +

OMG

+ +{% for num, msg in messages.items() %} + +
+

({{ num }})

+

{{ msg }}

+
+ +{% endfor %} + + + \ No newline at end of file diff --git a/logbot/logbot.conf b/logbot/logbot.conf new file mode 100644 index 0000000..de02217 --- /dev/null +++ b/logbot/logbot.conf @@ -0,0 +1,5 @@ +[logbot] +account = bot@vvvvvvaria.org +password = streaming +nick = logbot +rooms = testtesttest@muc.vvvvvvaria.org, ibugev@muc.vvvvvvaria.org \ No newline at end of file diff --git a/logbot/logbot.py b/logbot/logbot.py index edf3d8b..282c63b 100644 --- a/logbot/logbot.py +++ b/logbot/logbot.py @@ -1,219 +1,95 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -# To run this bot: -# $ python3 logbot.py -# The output folder of this bot currently is: /var/www/logs/digital-autonomy - -import logging -from getpass import getpass -from argparse import ArgumentParser - -import slixmpp -import ssl, os, requests, urllib - -class MUCBot(slixmpp.ClientXMPP): - """ - A simple Slixmpp bot that will save images - and messages that are marked with @bot to a folder. - """ - - def __init__(self, jid, password, room, nick, output): - slixmpp.ClientXMPP.__init__(self, jid, password) - - self.room = room - self.nick = nick - self.output = output - - # The session_start event will be triggered when - # the bot establishes its connection with the server - # and the XML logs are ready for use. We want to - # listen for this event so that we we can initialize - # our roster. - self.add_event_handler("session_start", self.start) - - # The groupchat_message event is triggered whenever a message - # stanza is received from any chat room. If you also also - # register a handler for the 'message' event, MUC messages - # will be processed by both handlers. - self.add_event_handler("groupchat_message", self.muc_message) - - - def start(self, event): - self.get_roster() - self.send_presence() - - # https://xmpp.org/extensions/xep-0045.html - self.plugin['xep_0045'].join_muc(self.room, - self.nick, - # If a room password is needed, use: - # password=the_room_password, - wait=True) - - # NOTE(luke): disabled for now. We'll make it possible to speak to logbot privately later - # Send a message to the room - # self.send_message(mto=self.room, mbody='Hello! Logbot here. I\'m here to log all the images that are send to this group. You can also log text messages, by including @bot in your message. Happy logging! PS. you can access the logs at https://vvvvvvaria.org/logs/', mtype='groupchat') - - def muc_message(self, msg): - # Some inspection commands - #print('Message: {}'.format(msg)) - - # Always check that a message is not the bot itself, otherwise you will create an infinite loop responding to your own messages. - if msg['mucnick'] != self.nick: - - # Check if output folder exists - if not os.path.exists(self.output): - os.mkdir(self.output) - - # Check if an OOB URL is included in the stanza (which is how an image is sent) - # (OOB object - https://xmpp.org/extensions/xep-0066.html#x-oob) - if len(msg['oob']['url']) > 0: - - # Send a reply - self.send_message(mto=self.room, - mbody="Super, our log is growing. Your image is added!", - mtype='groupchat') - - # Save the image to the output folder - url = msg['oob']['url'] # grep the url in the message - filename = os.path.basename(url) # grep the filename in the url - output_path = os.path.join(self.output, filename) - u = urllib.request.urlopen(url) # read the image data - f = open(output_path, 'wb') # open the output file - f.write(u.read()) # write image to file - f.close() # close the output file - - # Add the image to the log - img = ''.format(filename) - log = 'log.html' - log_path = os.path.join(self.output, log) - f = open(log_path, 'a+') - f.write(img+'\n') - f.close() - - - # Include messages in the log (only when '@bot' is used in the message) - if '@bot' in msg['body']: - - # reply from the bot - self.send_message(mto=self.room, - mbody="Noted! And added to the log. Thanks {}!".format(msg['mucnick']), - mtype='groupchat') - - # Add the message to the log! - message = '

{}

'.format(msg['body'].replace('@bot','')) - log = 'log.html' - log_path = os.path.join(self.output, log) - f = open(log_path, 'a+') - f.write(message+'\n') - f.close() - - if '/book' in msg['body']: # Check if this is a book ... - - self.send_message(mto=self.room, - mbody="Oh a book, that's cool! Thanks {}!".format(msg['mucnick']), - mtype='groupchat') - - # Start of book feature - from bs4 import BeautifulSoup - import re - - book = msg['body'].replace('@bot', '').replace('/book', '') - book = re.sub(' +', ' ', book) # remove double spaces - book = book.lstrip().rstrip() # remove spaces at the beginning and at the end - book = book.replace(' ', '+').lower() # turn space into + and lowercase - - page_link = 'https://www.worldcat.org/search?q={}&qt=results_page'.format(book) - - page_response = requests.get(page_link, timeout=5) - - page_content = BeautifulSoup(page_response.content, "html.parser") - - try: - book_title = page_content.findAll("div", {"class": "name"})[0].text - book_author = page_content.findAll("div", {"class": "author"})[0].text - book_publisher = page_content.findAll("div", {"class": "publisher"})[0].text - - response = 'BOOK: ' + book_title + ' ' + book_author + ' ' + book_publisher - - book_found = True - - except IndexError: - - book_found = False - - if book_found: - - # Add message to log - message = 'BOOK: ' + book_title + ' ' + book_author + ' ' + book_publisher - log = 'log.html' - log_path = os.path.join(self.output, log) - f = open(log_path, 'a+') - f.write(message+'\n') - f.close() - - self.send_message(mto=self.room, mbody='Hope this was the book you were looking for: ' + book_title + ' ' + book_author + ' ' + book_publisher, mtype='groupchat') - - else: - - self.send_message(mto=self.room, mbody='Sorry, no book found!', mtype='groupchat') - - - - -if __name__ == '__main__': - # Setup the command line arguments. - parser = ArgumentParser() - - # output verbosity options. - parser.add_argument("-q", "--quiet", help="set logging to ERROR", - action="store_const", dest="loglevel", - const=logging.ERROR, default=logging.INFO) - parser.add_argument("-d", "--debug", help="set logging to DEBUG", - action="store_const", dest="loglevel", - const=logging.DEBUG, default=logging.INFO) - - # JID and password options. - parser.add_argument("-j", "--jid", dest="jid", - help="JID to use") - parser.add_argument("-p", "--password", dest="password", - help="password to use") - parser.add_argument("-r", "--room", dest="room", - help="MUC room to join") - parser.add_argument("-n", "--nick", dest="nick", - help="MUC nickname") - - # output folder for images - parser.add_argument("-o", "--output", dest="output", - help="output folder, this is where the files are stored", - type=str) - - args = parser.parse_args() - - # Setup logging. - logging.basicConfig(level=args.loglevel, - format='%(levelname)-8s %(message)s') - - if args.jid is None: - args.jid = input("XMPP address: ") - if args.password is None: - args.password = getpass("Password: ") - if args.room is None: - args.room = input("MUC room: ") - if args.nick is None: - args.nick = input("MUC nickname: ") - if args.output is None: - args.output = input("Output folder: ") - - # Setup the MUCBot and register plugins. Note that while plugins may - # have interdependencies, the order in which you register them does - # not matter. - xmpp = MUCBot(args.jid, args.password, args.room, args.nick, args.output) - xmpp.register_plugin('xep_0030') # Service Discovery - xmpp.register_plugin('xep_0045') # Multi-User Chat - xmpp.register_plugin('xep_0199') # XMPP Ping - xmpp.register_plugin('xep_0066') # Process URI's (files, images) - - # Connect to the XMPP server and start processing XMPP stanzas. - xmpp.connect() - xmpp.process() +from xbotlib import Bot +import json +import jinja2 +import re + +db = 'storage.json' + +def readdb(): + storage = open(db, 'r').read() + messages = json.loads(storage) + lastid = sorted(list(messages.keys())) + nextid = str(int(lastid[-1])+1) + return messages + +def writedb(message): + try: + with open(db, 'r') as storage: + messages = json.loads(storage.read()) + if messages.keys(): + keys = [int(x) for x in messages.keys()] + keys.sort() + lastid = keys[-1] + nextid = lastid + 1 + else: + nextid = 0 + messages[f'{ nextid }'] = message + storage = open(db, 'w') + storage.write(json.dumps(messages, indent=4)) + except IOError: + with open(db, 'w') as storage: + storage.write(json.dumps('{}')) + writedb(message) + return messages + +def deletefromdb(id): + with open(db, 'r') as storage: + messages = json.loads(storage.read()) + del messages[id] + storage = open(db, 'w') + storage.write(json.dumps(messages, indent=4)) + return messages + +def writelog(messages): + template = jinja2.Template(open('index.html.j2').read()) + with open('index.html','w') as out: + html = template.render(messages=messages) + out.write(html) + +class logbot(Bot): + + help = '''Oh dear, logbot is here! + +@delete +Delete posts from the log. +For example: @logbot @delete 5 + +@bots +To see who is around :) + +@uptime +To check how long @logbot has been around + +@help +Print this message +''' + + def group(self, message): + print(message.content) + messages = readdb() + + if message.url: + messages = writedb(f'') + reply = 'Thanks for that image!' + + elif '@delete' in message.text: + match = re.findall("@delete \d*", message.content)[0] + id = match.replace('@delete ','') + if id in messages: + print('To be deleted:', messages[str(id)]) + reply = f'This message is deleted: { messages[str(id)] }' + messages = deletefromdb(id) + else: + reply = 'This message is already gone!' + + elif '@help' in message.text: + print('HELP') + + else: + messages = writedb(message.text) + reply = 'Added, thanks!' + + writelog(messages) + return self.reply(reply, room=message.room) + +logbot() \ No newline at end of file diff --git a/logbot/storage.json b/logbot/storage.json new file mode 100644 index 0000000..e69de29 diff --git a/logbot/stylesheet.css b/logbot/stylesheet.css new file mode 100644 index 0000000..95427bd --- /dev/null +++ b/logbot/stylesheet.css @@ -0,0 +1,14 @@ +body{ + background-color: pink; + margin: 1em; +} +.post{ + margin: 1em 0; + clear: both; +} +.post p.id{ + float: left; + margin: 0 1em 1em; +} +.post p.message{ +} \ No newline at end of file