Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
  • 5 commits
  • 15 files changed
  • 0 commit comments
  • 2 contributors
View
0  httpsrv/__init__.py
No changes.
View
235 httpsrv/http_server.py
@@ -1,235 +0,0 @@
-import socket
-import re
-import datetime
-
-import sys
-import threading
-class TimeoutError(Exception): pass
-
-def timelimit(timeout):
- def internal(function):
- def internal2(*args, **kw):
- class Calculator(threading.Thread):
- def __init__(self):
- threading.Thread.__init__(self)
- self.result = None
- self.error = None
-
- def run(self):
- try:
- self.result = function(*args, **kw)
- except:
- self.error = sys.exc_info()[0]
-
- c = Calculator()
- c.start()
- c.join(timeout)
- if c.isAlive():
- raise TimeoutError
- if c.error:
- raise c.error
- return c.result
- return internal2
- return internal
-
-class InvalidRequestException:
- pass
-
-class ClientConnection:
- def __init__(self, socket, address):
- self.socket = socket
- self.address = address
-
- @timelimit(10)
- def wait_for_request(self):
- data = ""
-
- while True:
- recv_data = self.socket.recv(65536)
- data += recv_data
-
- if "\r\n\r\n" in data:
- break
-
- return ClientRequest(self, data)
-
-class ClientRequest:
- def __init__(self, client, data):
- self.client = client
-
- self.headers = {}
-
- self.cookies = {}
-
- lines = data.split("\r\n")
-
- request_line = lines[0]
- lines = lines[1:]
-
- m = re.search("^(\w+) (.+) (HTTP\/...)$", request_line)
- if m:
- method, request_uri, version = m.group(1, 2, 3)
-
- self.method = method
- self.request_path = request_uri
- self.version = version
-
- for line in lines:
- if line:
- m = re.search("^(\S+): (.+)$", line)
- if m:
- name, value = m.group(1, 2)
- self.headers[name] = value
- else:
- raise InvalidRequestException()
-
- if "Cookie" in self.headers:
- cookies = self.headers["Cookie"]
-
- for trash1, key, value, trash2 in re.findall("(^|:)(.*?)=(.*)(;|$)", cookies):
- self.cookies[key] = value
-
- def get_version(self):
- return self.version
-
- def get_method(self):
- return self.method
-
- def get_request_uri(self):
- return self.request_uri
-
- def get_header(self, name):
- if name in self.headers:
- return self.headers[name]
- else:
- return None
-
- def get_cookie(self, name):
- if name in self.cookies:
- return self.cookies[name]
- else:
- return None
-
- def __str__(self):
- return "%s %s %s %s" % (self.method, self.request_path, self.version, self.headers)
-
-class ServerResponse:
- def __init__(self, status_code):
- self.version = "HTTP/1.1"
- self.status_code = status_code
- self.headers = {}
- self.content = None
-
- def add_header(self, key, value):
- self.headers[key] = value
-
- def remove_header(self, key):
- if key in self.headers:
- del self.headers[key]
-
- def set_content(self, content):
- self.content = content
-
- if content:
- self.add_header("Content-Length", str(len(content)))
- else:
- self.remove_header("Content-Length")
-
- def add_cookie(self, name, value, life_time=datetime.timedelta(1)):
- self.cookies[name] = (value, datetime.datetime.now()+life_time)
-
- def compile(self):
- lines = []
-
- status_line = "%s %d %s" % (self.version, self.status_code, "OK")
- lines.append(status_line)
- for key, value in self.headers.items():
- lines.append("%s: %s" % (key, value))
-
- data = "\r\n".join(lines) + "\r\n\r\n"
-
- if self.content:
- data += self.content
-
- return data
-
-class HTTPServer:
- def __init__(self, port, host=""):
- self.handle_request_callback = None
-
- self.socket = socket.socket()
- self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- self.socket.bind((host, port))
- self.socket.listen(10)
-
- self.socket.setblocking(False)
-
- self.request_queue_lock = threading.Lock()
- self.request_queue = []
-
- def register_handle_request_callback(self, callback):
- self.handle_request_callback = callback
-
- def accept_client_connection(self):
- s, address = self.socket.accept()
-
- s.setblocking(True)
-
- return ClientConnection(s, address)
-
- def respond_404(self, request):
- response = ServerResponse(404)
-
- response.add_header("Content-Type", "text/html")
- response.add_header("Connection", "close")
-
- response.set_content("404 etc")
-
- self.respond(request, response)
-
- def respond_200(self, request, data, type = "text/html"):
- response = ServerResponse(200)
-
- response.add_header("Content-Type", type)
- response.add_header("Connection", "close")
- response.set_content(data)
-
- self.respond(request, response)
-
- def respond(self, request, response):
- data = response.compile()
-
- while data:
- sent = request.client.socket.send(data)
- if sent <= 0:
- return
- else:
- data = data[sent:]
-
- request.client.socket.close()
-
- def get_request(self, client):
- try:
- request = client.wait_for_request()
- self.request_queue_lock.acquire()
- self.request_queue.append(request)
- self.request_queue_lock.release()
- except TimeoutError:
- client.socket.close()
- except InvalidRequestException:
- print "invalid request O.o"
- client.socket.close()
-
- def tick(self):
- self.request_queue_lock.acquire()
- while self.request_queue:
- request = self.request_queue.pop()
- self.handle_request_callback(request)
- self.request_queue_lock.release()
-
- try:
- client = self.accept_client_connection()
- thread = threading.Thread(None, self.get_request, None, (client,))
- thread.start()
- except socket.error:
- pass # no incoming connection atm...
View
4 httpsrv/main.py
@@ -1,4 +0,0 @@
-import http_server
-
-server = http_server.HTTPServer(8080)
-server.run()
View
30 ircbot.py
@@ -102,15 +102,16 @@ def execute_plugins(self, network, trigger, *arguments):
datetime.datetime.now().strftime("[%H:%M:%S]"), network,
plugin, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2]))
- try:
- self.tell(self.settings.admin_network, self.settings.admin_channel,
- "%s %s Plugin '%s' threw exception, exinfo: '%s', traceback: '%s'" % (
+ if trigger != "timer_beat":
+ try:
+ self.tell(self.settings.admin_network, self.settings.admin_channel,
+ "%s %s Plugin '%s' threw exception, exinfo: '%s', traceback: '%s'" % (
+ datetime.datetime.now().strftime("[%H:%M:%S]"), network,
+ plugin, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2])[::-1]))
+ except:
+ print "%s %s Unable to send exception to admin channel, exinfo: '%s', traceback: '%s'" % (
datetime.datetime.now().strftime("[%H:%M:%S]"), network,
- plugin, sys.exc_info(), traceback.extract_tb(sys.exc_info()[2])[::-1]))
- except:
- print "%s %s Unable to send exception to admin channel, exinfo: '%s', traceback: '%s'" % (
- datetime.datetime.now().strftime("[%H:%M:%S]"), network,
- sys.exc_info(), traceback.extract_tb(sys.exc_info()[2]))
+ sys.exc_info(), traceback.extract_tb(sys.exc_info()[2]))
def on_connected(self, network):
for channel in self.settings.networks[network]['channels']:
@@ -194,22 +195,17 @@ def tell(self, network, target, message=None):
return self.clients[network].tell(target, message)
def tick(self):
- if self.need_reload.has_key('ircbot') and self.need_reload['ircbot']:
+ # Do reload if necassary
+ if self.need_reload.get('ircbot'):
reload(ircclient)
reload(plugin_handler)
self.callbacks = self.get_callbacks()
for client in self.clients.values():
client.callbacks = copy(self.callbacks)
- #client.on_reload()
- #self.execute_plugins(, "")
-
self.need_reload['ircbot'] = False
- # FIXME timer is broken
-# if not self.timer_heap.empty() and not self.client.connected:
-# print "ATTENTION! We are not connected. Skipping timers!"
-# else:
+ # Call timers
now = datetime.datetime.now()
while not self.timer_heap.empty() and self.timer_heap.top().trigger_time <= now:
timer = self.timer_heap.pop()
@@ -230,5 +226,3 @@ def add_timer(self, delta, recurring, target, *args):
self.timer_heap.push(timer)
- def add_background_job(self, name, callback, target, args):
- pass
View
42 main.py
@@ -13,47 +13,12 @@
sys.exit(0)
import ircbot
-from httpsrv import http_server
bot = ircbot.IRCBot(settings.Settings())
-
-#web_server = http_server.HTTPServer(host="127.0.0.1", port=8000)
-botnik_picture_data = None
-
-def handle_request(request):
- if request.request_path == "/botnik.png":
- global botnik_picture_data
-
- if not botnik_picture_data:
- try:
- file = open("botnik.png")
- botnik_picture_data = file.read()
- file.close()
- except:
- web_server.respond_200(request, "Couldn't send image...")
- web_server.respond_200(request, botnik_picture_data, "image/png")
- return
-
- c = None
-# if bot.is_connected(): #FIXME
-# c = "connected"
-# else:
-# c = "disconnected"
-
- data = "I think that I am %s.<p><img src=\"botnik.png\"><p>" % c
-
- data += "Conversation:<p><pre>"
- for line in bot.client.lines:
- data += line.replace("<", "&lt;").replace(">", "&gt;") + "\n"
- data += "</pre>"
-
- web_server.respond_200(request, data)
-
-#web_server.register_handle_request_callback(handle_request)
-
bot.add_timer(datetime.timedelta(0, 600), True, bot.send_all_networks, "PING :iamabanana")
-sys.path += [os.path.join(sys.path[0], "httpsrv"), os.path.join(sys.path[0], "ircclient"),
- os.path.join(sys.path[0], "plugins")]
+
+# Add paths for debugger
+sys.path += [os.path.join(sys.path[0], "ircclient"), os.path.join(sys.path[0], "plugins")]
def Tick():
while True:
@@ -67,7 +32,6 @@ def Tick():
bot.on_reload()
bot.tick()
- #web_server.tick()
time.sleep(0.1)
except KeyboardInterrupt:
View
20 plugins/compliment.py
@@ -4,9 +4,8 @@
from commands import Command
import string
-import os
-import pickle
import random
+import utility
class ComplimentCommand(Command):
def __init__(self):
@@ -33,22 +32,11 @@ def trig_addcompliment(self, bot, source, target, trigger, argument):
self.save()
return "Added compliment: %s" % argument.replace('%s', source)
- def save(self):
- f = open(os.path.join("data", "compliments.txt"), "w")
- p = pickle.Pickler(f)
- p.dump(self.compliments)
- f.close()
+ def save(self):
+ utility.save_data("compliments", self.compliments)
def on_load(self):
- self.compliments = []
-
- try:
- f = open(os.path.join("data", "compliments.txt"), "r")
- unpickler = pickle.Unpickler(f)
- self.compliments = unpickler.load()
- f.close()
- except:
- pass
+ self.compliments = utility.load_data("compliments", [])
def on_unload(self):
self.compliments = None
View
17 plugins/favorites.py
@@ -1,7 +1,5 @@
# coding: utf-8
-from __future__ import with_statement
-import pickle
from commands import Command
import re
import utility
@@ -68,24 +66,13 @@ def trig_fav(self, bot, source, target, trigger, argument):
return "No such favorite '%s'." % fav_trig
def save(self):
- with open('data/favorites.txt', 'w') as file:
- p = pickle.Pickler(file)
-
- p.dump(self.favorites)
+ utility.save_data("favorites", self.favorites)
def on_modified_options(self):
self.save()
def on_load(self):
- self.favorites = {}
-
- try:
- with open('data/favorites.txt') as file:
- unpickler = pickle.Unpickler(file)
-
- self.favorites = unpickler.load()
- except:
- pass
+ self.favorites = utility.load_data("favorites", {})
def on_unload(self):
self.favorites.clear()
View
174 plugins/game_plugin.py
@@ -1,174 +0,0 @@
-# coding: utf-8
-
-from __future__ import with_statement
-from commands import Command
-import random
-import datetime
-import pickle
-import utility
-import standard
-import re
-
-class Game:
- def __init__(self, name):
- self.name = name
- self.players = {}
- self.timeout = None
- self.time = None
- self.current_question = None
- self.timeout_streak = 0
- self.running = False
- self.words = ["fur", "nigeria", "chewing gum", "cigar", "gamecube", "flower", "mp3", "bottle", "film", "radio", "knob", "fuck", "temperature", "milk", "mouse", "man", "wax", "pillow", "bicycle", "pub", "telephone", "stalk", "dog", "cat", "blacksmith", "glass", "door", "house", "metal", "lighter", "window", "mechanic", "camera", "stapler", "pencil", "tape", "scissors"]
-
- def set_dictionary(self, dictionary):
- self.dictionary = dictionary
- self.words = ["fur", "nigeria", "disco", "chewing gum", "cigar", "gamecube", "flower", "mp3", "bottle", "film", "radio", "knob", "fuck", "temperature", "milk", "mouse", "man", "wax", "pillow", "bicycle", "pub", "telephone", "stalk", "dog", "cat", "blacksmith", "glass", "door", "house", "metal", "lighter", "window", "mechanic", "camera", "stapler", "pencil", "tape", "scissors"]
-
- def on_tick(self, bot, time):
- self.time = time
-
- if self.running and (not self.timeout or time > self.timeout):
- if self.timeout and time - self.timeout > datetime.timedelta(0, 0, 0, 0, 10): #if we're 10 minutes it's more than just lag...
- self.running = False
- return
-
- self.timeout_streak += 1
- if self.timeout_streak > 3:
- self.timeout_streak = 0
- self.send_timeout(bot)
- self.send_timeout_quit(bot)
- self.stop(bot)
- else:
- if self.current_question:
- self.send_timeout(bot)
- self.new_question()
- self.send_question(bot)
-
- def on_privmsg(self, bot, source, target, message):
- self.on_tick(bot, self.time)
-
- if self.running:
- if self.current_question[1] == message:
- self.timeout_streak = 0
-
- if source in self.players:
- self.players[source] += 1
- else:
- self.players[source] = 1
-
- bot.tell(self.name, "Yay! %s got it!" % utility.extract_nick(source))
-
- self.new_question()
- self.send_question(bot)
-
- def start(self, bot):
- if not self.running:
- self.running = True
- self.current_question = None
- self.timeout = None
- bot.tell(self.name, "Game started.")
-
- def new_question(self):
- if len(self.words):
- word = self.words[0]
- self.words = self.words[1:]
-
- question = standard.WikipediaCommand.instance.wp_get(word)
-
- if question:
- question = re.sub("(?i)" + word, "*" * len(word), question)
- self.current_question = (question, word)
-
- if not self.current_question:
- self.current_question = random.choice(self.dictionary.items())
-
- self.timeout = self.time + datetime.timedelta(0, 30)
-
- def send_question(self, bot):
- bot.tell(self.name, "Question: %s" % self.current_question[0])
-
- def stop(self, bot):
- if self.running:
- self.running = False
- bot.tell(self.name, "Game stopped.")
-
- def format_hiscore(self, tuple):
- return "%s: %d" % (utility.extract_nick(tuple[0]), tuple[1])
-
- def send_hiscore(self, bot):
- l = self.players.items()
- l.sort(key=lambda x: (x[1], x[0]))
- str = ", ".join(map(self.format_hiscore, reversed(l)))
- bot.tell(self.name, "Hi-score: %s." % str)
-
- def send_timeout(self, bot):
- bot.tell(self.name, "Timed out. Answer: %s." % self.current_question[1])
-
- def send_timeout_quit(self, bot):
- bot.tell(self.name, "Stopping inactive game.")
-
-class GamePlugin(Command):
- hooks = ['on_privmsg']
-
- def __init__(self):
- self.dictionary = { "*round time machine*": "clock", "*fourlegged reliever*": "chair", "*round rubber carrier*": "wheel", "*code machine*": "matricks", "*italian plumber*": "mario", "*squishy ball with gun*": "tee", "*round house kick master*": "chuck norris", "*best encoding*": "utf-8" }
- self.games = {}
-
- def on_load(self):
- self.load_games()
-
- for game in self.games.values():
- game.set_dictionary(self.dictionary)
-
- def on_unload(self):
- self.save_games()
-
- def on_save(self):
- self.save_games()
-
- def save_games(self):
- file = open('data/games.txt', 'w')
- p = pickle.Pickler(file)
- p.dump(self.games)
- file.close()
-
- def load_games(self):
- try:
- with open('data/games.txt', 'r') as file:
- self.games = pickle.Unpickler(file).load()
- except:
- pass
-
-
- def trig_gamestart(self, bot, source, target, trigger, argument):
- if not target in self.games.keys():
- self.games[target] = Game(target)
- self.games[target].set_dictionary(self.dictionary)
-
- game = self.games[target]
- game.start(bot)
-
- def trig_gamestop(self, bot, source, target, trigger, argument):
- if target in self.games.keys():
- game = self.games[target]
- game.stop(bot)
-
- self.on_save()
-
- def trig_gamehiscore(self, bot, source, target, trigger, argument):
- if target in self.games.keys():
- game = self.games[target]
- game.send_hiscore(bot)
- else:
- return "I have no hiscore for this game."
-
- def on_privmsg(self, bot, source, target, message):
- if target in self.games.keys():
- game = self.games[target]
- game.on_privmsg(bot, source, target, message)
-
- return None
-
- def timer_beat(self, bot, time):
- for game in self.games.values():
- game.on_tick(bot, time)
View
19 plugins/postnr.py
@@ -3,8 +3,6 @@
import re
import utility
import string
-import os
-import pickle
from commands import Command
class PostNr(Command):
@@ -63,21 +61,10 @@ def trig_postnr(self, bot, source, target, trigger, argument):
return self.posten_postnr_query(self.utf82iso(args[0]), self.utf82iso(args[1]))
def save(self):
- f = open(os.path.join("data", "postnr_addresses.txt"), "w")
- p = pickle.Pickler(f)
- p.dump(self.places)
- f.close()
+ utility.save_data("postnr_addresses", self.places)
- def on_load(self):
- self.places = {}
-
- try:
- f = open(os.path.join("data", "postnr_addresses.txt"), "r")
- unpickler = pickle.Unpickler(f)
- self.places = unpickler.load()
- f.close()
- except:
- pass
+ def on_load(self):
+ self.places = utility.load_data("postnr_addresses", {})
def on_unload(self):
self.places = {}
View
25 plugins/pylisp.py
@@ -1,7 +1,6 @@
-from __future__ import with_statement
-import pickle
from plugins import Plugin
from commands import Command
+import utility
import command_catcher
import random
@@ -742,23 +741,19 @@ def trig_lisp(self, bot, source, target, trigger, argument):
return str(e)
def save(self):
- with open('data/lisp_state.txt', 'w') as file:
- p = pickle.Pickler(file)
-
- self.savable_environment.parent = None
- p.dump(self.savable_environment)
- self.savable_environment.parent = self.globals
+ self.savable_environment.parent = None
+ utility.save_data("lisp_state", self.savable_environment)
+ self.savable_environment.parent = self.globals
def on_load(self):
- try:
- with open('data/lisp_state.txt') as file:
- unp = pickle.Unpickler(file)
-
- self.savable_environment = unp.load()
- except:
+ self.savable_environment = utility.load_data("lisp_state")
+
+ if not self.savable_environment:
self.savable_environment = Environment(self.globals)
+ else:
+ self.savable_environment.parent = self.globals
- self.savable_environment.parent = self.globals
+
def on_unload(self):
self.savable_environment = Environment(self.globals)
View
16 plugins/reminder.py
@@ -1,7 +1,5 @@
# coding: utf-8
-from __future__ import with_statement
-import pickle
import os
import re
import datetime
@@ -77,20 +75,10 @@ def timer_beat(self, bot, now, network):
self.save()
def save(self):
- with open('data/reminders.txt', 'w') as file:
- p = pickle.Pickler(file)
-
- p.dump(self.reminders)
+ utility.save_data("reminders", self.reminders)
def on_load(self):
- self.reminders = []
-
- try:
- with open('data/reminders.txt') as file:
- unp = pickle.Unpickler(file)
- self.reminders = unp.load()
- except:
- pass
+ self.reminders = utility.load_data("reminders", [])
def on_unload(self):
self.reminders = []
View
17 plugins/rss.py
@@ -1,7 +1,5 @@
# coding: utf-8
-from __future__ import with_statement
-import pickle
import os
import re
import datetime
@@ -162,21 +160,10 @@ def timer_beat(self, bot, now, network):
self.save()
def save(self):
- with open('data/rss_watch_list.txt', 'w') as file:
- p = pickle.Pickler(file)
-
- p.dump(self.watch_list)
+ utility.save_data("rss_watch_list", self.watch_list)
def on_load(self):
- self.watch_list = []
-
- try:
- with open('data/rss_watch_list.txt') as file:
- unp = pickle.Unpickler(file)
-
- self.watch_list = unp.load()
- except:
- pass
+ self.watch_list = utility.load_data("rss_watch_list", [])
def on_unload(self):
self.watch_list = []
View
34 plugins/standard.py
@@ -5,8 +5,6 @@
import string
import re
import utility
-import os
-import pickle
import random
class EchoCommand(Command):
@@ -69,21 +67,10 @@ def trig_addinsult(self, bot, source, target, trigger, argument):
return "Added insult: %s" % argument.replace('%s', source)
def save(self):
- f = open(os.path.join("data", "insults.txt"), "w")
- p = pickle.Pickler(f)
- p.dump(self.insults)
- f.close()
+ utility.save_data("insults", self.insults)
def on_load(self):
- self.insults = []
-
- try:
- f = open(os.path.join("data", "insults.txt"), "r")
- unpickler = pickle.Unpickler(f)
- self.insults = unpickler.load()
- f.close()
- except:
- pass
+ self.insults = utility.load_data("insults", [])
def on_unload(self):
self.insults = None
@@ -194,21 +181,10 @@ def trig_temp(self, bot, source, target, trigger, argument):
return "Temperature in %s: invalid place, try using .yr instead." % (argument_text)
def save(self):
- f = open(os.path.join("data", "places.txt"), "w")
- p = pickle.Pickler(f)
- p.dump(self.places)
- f.close()
+ utility.save_data("places", self.places)
- def on_load(self):
- self.places = {}
-
- try:
- f = open(os.path.join("data", "places.txt"), "r")
- unpickler = pickle.Unpickler(f)
- self.places = unpickler.load()
- f.close()
- except:
- pass
+ def on_load(self):
+ self.places = utility.load_data("places", {})
def on_unload(self):
self.places = {}
View
24 plugins/title_reader.py
@@ -1,7 +1,5 @@
# coding: utf-8
-from __future__ import with_statement
-import pickle
import sys
import re
import utility
@@ -129,18 +127,11 @@ def trig_title(self, bot, source, target, trigger, argument):
def save_urls(self):
- file = open('data/urls.txt', 'w')
- p = pickle.Pickler(file)
- p.dump(self.url_list)
- file.close()
+ utility.save_data("urls", self.url_list)
def load_urls(self):
- try:
- with open('data/urls.txt', 'r') as file:
- self.url_list = pickle.Unpickler(file).load()
- except IOError:
- pass
+ self.url_list = utility.load_data("urls", [])
def on_load(self):
@@ -182,18 +173,11 @@ def clean(self, url, title):
def mask_load(self):
- try:
- with open('data/urlmasks.txt', 'r') as file:
- self.url_masks = pickle.Unpickler(file).load()
- except IOError:
- pass
+ self.url_masks = utility.load_data("urlmasks", {})
def mask_save(self):
- file = open('data/urlmasks.txt', 'w')
- p = pickle.Pickler(file)
- p.dump(self.url_masks)
- file.close()
+ utility.save_data("urlmasks", self.url_masks)
def trig_titlemask(self, bot, source, target, trigger, argument):
View
5 plugins/utility.py
@@ -6,6 +6,7 @@
from plugins import Plugin
import htmlentitydefs
import re
+import os
import signal
import string
import settings
@@ -121,14 +122,14 @@ def read_url(url):
return data
def save_data(name, data):
- handle = open('data/' + name + '.txt', 'w')
+ handle = open(os.path.join('data', name + '.txt'), 'w')
p = pickle.Pickler(handle)
p.dump(data)
handle.close()
def load_data(name, default_value=None):
try:
- with open('data/' + name + '.txt', 'r') as handle:
+ with open(os.path.join('data', name + '.txt'), 'r') as handle:
return pickle.Unpickler(handle).load()
except:
print "Could not load data from file 'data/" + str(name) + ".txt' :("

No commit comments for this range

Something went wrong with that request. Please try again.