Skip to content

Commit ebdcf8d

Browse files
committed
Start working on stories plugin
1 parent aadd89a commit ebdcf8d

File tree

3 files changed

+201
-1
lines changed

3 files changed

+201
-1
lines changed

nedry/builtin_plugins/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
from nedry.builtin_plugins.mock import Mocking
44
from nedry.builtin_plugins.schedule import Schedule
55
from nedry.builtin_plugins.trivia import Trivia
6+
from nedry.builtin_plugins.stories import Stories
67

7-
builtin_plugin_modules = [KnockKnockJokes, Wikipedia, Mocking, Schedule, Trivia]
8+
builtin_plugin_modules = [KnockKnockJokes, Wikipedia, Mocking, Schedule, Trivia, Stories]

nedry/builtin_plugins/stories.py

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import os
2+
3+
from nedry.plugin import PluginModule
4+
from nedry import utils
5+
6+
PROMPTS_FILE = os.path.join(os.path.dirname(__file__), "writing_prompts.txt")
7+
8+
STORY_HELPTEXT = """
9+
{0} new|add|show|stop [optional story contribution text]
10+
11+
Interact with the story being written on the current discord channel.
12+
13+
The first argument to this command may be one of 4 operations:
14+
15+
new - Start a new story in this channel.
16+
17+
add - Contribute the next part of the story being written on this channel.
18+
[optional story contribution text] should be replaced with your desired
19+
text for the next part of the story.
20+
21+
show - Show the current story as written so far.
22+
23+
stop - Stop the story writing session, and show the story as written so far.
24+
25+
Examples:
26+
27+
@BotName !story new (Start a new story)
28+
@BotName !story add And then he fell down... (Contribute to the current story)
29+
@BotName !story show (Show the story as written so far)
30+
@BotName !story stop (Stop the story)
31+
"""
32+
33+
stories_by_channel = {}
34+
35+
36+
class StoryContribution(object):
37+
"""
38+
Represents a story contribution by a specific discord user in a specific channel
39+
"""
40+
def __init__(self, discord_user, text):
41+
self.discord_user = discord_user
42+
self.text = text
43+
44+
45+
class StorySession(object):
46+
"""
47+
Represents a single story written by multiple discord users in a single channel
48+
"""
49+
def __init__(self, prompt):
50+
self.prompt = prompt
51+
self.contributions = []
52+
53+
def add_contribution(self, contribution):
54+
self.contributions.append(contribution)
55+
56+
def dump_story(self):
57+
ret = ""
58+
lines = [self.prompt] + [x.text for x in self.contributions]
59+
for c in lines:
60+
text = c.strip()
61+
if text[-1] not in ['.', '?', '!', ':', ';', '-']:
62+
text += '.'
63+
64+
ret += text + ' '
65+
66+
return ret
67+
68+
69+
def _handle_new_op(cmd_word, proc, message):
70+
if message.channel.id in stories_by_channel:
71+
return proc.usage_msg(f"{message.author.mention} Story already in progress in this channel, "
72+
f"stop the current story before starting a new one.", cmd_word)
73+
74+
prompt = utils.random_line_from_file(PROMPTS_FILE)
75+
session = StorySession(prompt)
76+
stories_by_channel[message.channel.id] = session
77+
78+
mention = ""
79+
if hasattr(message.channel, 'mention'):
80+
mention = message.channel.mention
81+
else:
82+
mention = message.author.mention
83+
84+
return (f"{mention} Let's write a story! I will start you off with a prompt, "
85+
f"and you can add to the story using the '!story add' command.\n\n"
86+
f"Here is your prompt:\n```{prompt}```")
87+
88+
def _handle_add_op(cmd_word, proc, message, text):
89+
if message.channel.id not in stories_by_channel:
90+
return proc.usage_msg(f"{message.author.mention} No story is in progress on this channel, "
91+
f"you must start a new one before adding to it", cmd_word)
92+
93+
if text == '':
94+
return proc.usage_msg(f"{message.author.mention} Please provide some text for the story.",
95+
cmd_word)
96+
97+
session = stories_by_channel[message.channel.id]
98+
session.add_contribution(StoryContribution(message.author, text))
99+
100+
return f"{message.author.mention} Got it, thank you for your story contribution!"
101+
102+
def _handle_show_op(cmd_word, proc, message):
103+
if message.channel.id not in stories_by_channel:
104+
return proc.usage_msg(f"{message.author.mention} No story is in progress on this channel.",
105+
cmd_word)
106+
107+
session = stories_by_channel[message.channel.id]
108+
109+
return f"{message.author.mention} Here is the story so far:\n```{session.dump_story()}```"
110+
111+
def _handle_stop_op(cmd_word, proc, message):
112+
if message.channel.id not in stories_by_channel:
113+
return proc.usage_msg(f"{message.author.mention} No story is in progress on this channel.",
114+
cmd_word)
115+
116+
session = stories_by_channel[message.channel.id]
117+
story = session.dump_story()
118+
del stories_by_channel[message.channel.id]
119+
120+
return f"{message.author.mention} OK, story stopped. Here is your story:\n```{story}```"
121+
122+
def story_command_handler(cmd_word, args, message, proc, config, twitch_monitor):
123+
args = args.split()
124+
if len(args) == 0:
125+
return proc.usage_msg(f"{message.author.mention} Command requires more information.", cmd_word)
126+
127+
op = args[0].strip().lower()
128+
if op not in ['new', 'add', 'stop', 'show']:
129+
return proc.usage_msg(f"{message.author.mention} Unrecognized operation.", cmd_word)
130+
131+
if 'new' == op:
132+
return _handle_new_op(cmd_word, proc, message)
133+
elif 'add' == op:
134+
return _handle_add_op(cmd_word, proc, message, ' '.join(args[1:]).strip())
135+
elif 'show' == op:
136+
return _handle_show_op(cmd_word, proc, message)
137+
elif 'stop' == op:
138+
return _handle_stop_op(cmd_word, proc, message)
139+
140+
141+
class Stories(PluginModule):
142+
"""
143+
Plugin for writing prompts and collaborative story writing.
144+
"""
145+
plugin_name = "stories"
146+
plugin_version = "1.0.0"
147+
plugin_short_description = "Writing prompts and collaborative story writing"
148+
plugin_long_description = """
149+
Provides a random writing prompt, and collects story fragments from discord users
150+
to collaboritvely write a story.
151+
152+
Commands added:
153+
154+
!story (see !help story)
155+
"""
156+
157+
def open(self):
158+
"""
159+
Enables plugin operation; subscribe to events and/or initialize things here
160+
"""
161+
self.discord_bot.add_command("story", story_command_handler, False, STORY_HELPTEXT)
162+
163+
def close(self):
164+
"""
165+
Disables plugin operation; unsubscribe from events and/or tear down things here
166+
"""
167+
self.discord_bot.remove_command("story")

nedry/utils.py

+32
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import random
12
import datetime
23
import zoneinfo
34

@@ -149,3 +150,34 @@ def list_to_english(words):
149150
return "%s and %s" % (words[0], words[1])
150151
else:
151152
return ", ".join(words[:-1]) + " and " + words[-1]
153+
154+
def random_line_from_file(filename):
155+
ret = b""
156+
with open(filename, "rb") as fh:
157+
fh.seek(0, 2) # Seek to the end of the file
158+
fsize = fh.tell() # Get file size
159+
160+
# Seek to random file offset
161+
offs = int((fsize - 1) * random.random())
162+
fh.seek(offs, 0)
163+
164+
# Keep reading single chars backwards until we see a newline
165+
while True:
166+
if fh.tell() == 0:
167+
# Reached the beginning of the file
168+
break
169+
170+
ch = fh.read(1)
171+
172+
if ch == b'\n':
173+
break
174+
175+
fh.seek(-2, 1)
176+
177+
# Now, keep reading single chars until EOF or until the next newline
178+
ch = fh.read(1)
179+
while ch not in [b'\n', b'']:
180+
ret += ch
181+
ch = fh.read(1)
182+
183+
return ret.decode("utf-8")

0 commit comments

Comments
 (0)